1use std::hash::Hash;
2use std::{collections::HashMap, sync::LazyLock};
3
4use regex::Regex;
5use relay_base_schema::metrics::MetricUnit;
6use relay_event_schema::protocol::{Event, VALID_PLATFORMS};
7use relay_pattern::Pattern;
8use relay_protocol::{FiniteF64, RuleCondition};
9use serde::{Deserialize, Serialize};
10
11pub mod breakdowns;
12pub mod contexts;
13pub mod nel;
14pub mod request;
15pub mod span;
16pub mod user_agent;
17pub mod utils;
18
19#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)]
21#[serde(default, rename_all = "camelCase")]
22pub struct BuiltinMeasurementKey {
23 name: String,
24 unit: MetricUnit,
25 #[serde(skip_serializing_if = "is_false")]
26 allow_negative: bool,
27}
28
29fn is_false(b: &bool) -> bool {
30 !b
31}
32
33impl BuiltinMeasurementKey {
34 pub fn new(name: impl Into<String>, unit: MetricUnit) -> Self {
36 Self {
37 name: name.into(),
38 unit,
39 allow_negative: false,
40 }
41 }
42
43 pub fn name(&self) -> &str {
45 &self.name
46 }
47
48 pub fn unit(&self) -> &MetricUnit {
50 &self.unit
51 }
52
53 pub fn allow_negative(&self) -> &bool {
55 &self.allow_negative
56 }
57}
58
59#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Hash)]
61#[serde(default, rename_all = "camelCase")]
62pub struct MeasurementsConfig {
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub builtin_measurements: Vec<BuiltinMeasurementKey>,
66
67 pub max_custom_measurements: usize,
69}
70
71impl MeasurementsConfig {
72 pub const MEASUREMENT_MRI_OVERHEAD: usize = 29;
75}
76
77pub fn is_valid_platform(platform: &str) -> bool {
81 VALID_PLATFORMS.contains(&platform)
82}
83
84pub fn normalize_app_start_spans(event: &mut Event) {
88 if !event.sdk_name().eq("sentry.javascript.react-native")
89 || !(event.sdk_version().starts_with("4.4")
90 || event.sdk_version().starts_with("4.3")
91 || event.sdk_version().starts_with("4.2")
92 || event.sdk_version().starts_with("4.1")
93 || event.sdk_version().starts_with("4.0")
94 || event.sdk_version().starts_with('3'))
95 {
96 return;
97 }
98
99 if let Some(spans) = event.spans.value_mut() {
100 for span in spans {
101 if let Some(span) = span.value_mut()
102 && let Some(op) = span.op.value()
103 {
104 if op == "app_start_cold" {
105 span.op.set_value(Some("app.start.cold".to_owned()));
106 break;
107 } else if op == "app_start_warm" {
108 span.op.set_value(Some("app.start.warm".to_owned()));
109 break;
110 }
111 }
112 }
113 }
114}
115
116#[derive(Clone, Debug)]
119pub struct CombinedMeasurementsConfig<'a> {
120 project: Option<&'a MeasurementsConfig>,
121 global: Option<&'a MeasurementsConfig>,
122}
123
124impl<'a> CombinedMeasurementsConfig<'a> {
125 pub fn new(
127 project: Option<&'a MeasurementsConfig>,
128 global: Option<&'a MeasurementsConfig>,
129 ) -> Self {
130 CombinedMeasurementsConfig { project, global }
131 }
132
133 pub fn builtin_measurement_keys(
138 &'a self,
139 ) -> impl Iterator<Item = &'a BuiltinMeasurementKey> + 'a {
140 let project = self
141 .project
142 .map(|p| p.builtin_measurements.as_slice())
143 .unwrap_or_default();
144
145 let global = self
146 .global
147 .map(|g| g.builtin_measurements.as_slice())
148 .unwrap_or_default();
149
150 project
151 .iter()
152 .chain(global.iter().filter(|key| !project.contains(key)))
153 }
154
155 pub fn max_custom_measurements(&'a self) -> Option<usize> {
158 match (&self.project, &self.global) {
159 (None, None) => None,
160 (None, Some(global)) => Some(global.max_custom_measurements),
161 (Some(project), None) => Some(project.max_custom_measurements),
162 (Some(project), Some(global)) => Some(std::cmp::min(
163 project.max_custom_measurements,
164 global.max_custom_measurements,
165 )),
166 }
167 }
168}
169
170#[derive(Debug, Default, Clone, Serialize, Deserialize)]
175pub struct PerformanceScoreWeightedComponent {
176 pub measurement: String,
179 pub weight: FiniteF64,
181 pub p10: FiniteF64,
183 pub p50: FiniteF64,
185 #[serde(default)]
188 pub optional: bool,
189}
190
191#[derive(Debug, Default, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct PerformanceScoreProfile {
198 pub name: Option<String>,
200 #[serde(default, skip_serializing_if = "Vec::is_empty")]
202 pub score_components: Vec<PerformanceScoreWeightedComponent>,
203 pub condition: Option<RuleCondition>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub version: Option<String>,
208}
209
210#[derive(Debug, Default, Clone, Serialize, Deserialize)]
215pub struct PerformanceScoreConfig {
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
218 pub profiles: Vec<PerformanceScoreProfile>,
219}
220
221#[derive(Clone, Default, Debug, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct ModelCosts {
243 pub version: u16,
245
246 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
248 pub models: HashMap<Pattern, ModelCostV2>,
249}
250
251impl ModelCosts {
252 const SUPPORTED_VERSION: u16 = 2;
253
254 pub fn is_empty(&self) -> bool {
256 self.models.is_empty() || !self.is_enabled()
257 }
258
259 pub fn is_enabled(&self) -> bool {
261 self.version == Self::SUPPORTED_VERSION
262 }
263
264 pub fn cost_per_token(&self, model_id: &str) -> Option<&ModelCostV2> {
266 if !self.is_enabled() {
267 return None;
268 }
269
270 let normalized_model_id = normalize_ai_model_name(model_id);
271
272 if let Some(value) = self.models.get(normalized_model_id) {
274 return Some(value);
275 }
276
277 self.models.iter().find_map(|(key, value)| {
282 if key.is_match(normalized_model_id) {
283 Some(value)
284 } else {
285 None
286 }
287 })
288 }
289}
290
291static VERSION_AND_DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
302 Regex::new(
303 r"(?x)
304 (
305 ([-_@]) # Required separator before date (-, _, or @)
306 (\d{4}[-/.]\d{2}[-/.]\d{2}|\d{8}) # Date: YYYY-MM-DD/YYYY/MM/DD/YYYY.MM.DD or YYYYMMDD
307 )? # Date is optional
308 (
309 [-_] # Required separator before version (- or _)
310 v\d+[:.]?\d* # Version: v1, v1.0, v1:0
311 ([-:].*)? # Optional trailing content
312 )? # Version is optional
313 $ # Must be at the end of the string
314 ",
315 )
316 .unwrap()
317});
318
319fn normalize_ai_model_name(model_id: &str) -> &str {
350 if let Some(captures) = VERSION_AND_DATE_REGEX.captures(model_id) {
351 let match_idx = captures.get(0).map(|m| m.start()).unwrap_or(model_id.len());
352 &model_id[..match_idx]
353 } else {
354 model_id
355 }
356}
357
358#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
360#[serde(rename_all = "camelCase")]
361pub struct ModelCostV2 {
362 pub input_per_token: f64,
364 pub output_per_token: f64,
366 pub output_reasoning_per_token: f64,
368 pub input_cached_per_token: f64,
370}
371
372#[derive(Clone, Default, Debug, Serialize, Deserialize)]
389#[serde(rename_all = "camelCase")]
390pub struct AiOperationTypeMap {
391 pub version: u16,
393
394 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
396 pub operation_types: HashMap<Pattern, String>,
397}
398
399impl AiOperationTypeMap {
400 const SUPPORTED_VERSION: u16 = 1;
401
402 pub fn is_empty(&self) -> bool {
404 self.operation_types.is_empty() || !self.is_enabled()
405 }
406
407 pub fn is_enabled(&self) -> bool {
409 self.version == Self::SUPPORTED_VERSION
410 }
411
412 pub fn get_operation_type(&self, span_op: &str) -> Option<&str> {
414 if !self.is_enabled() {
415 return None;
416 }
417
418 if let Some(value) = self.operation_types.get(span_op) {
420 return Some(value.as_str());
421 }
422
423 let operation_type = self.operation_types.iter().find_map(|(key, value)| {
425 if key.is_match(span_op) {
426 Some(value)
427 } else {
428 None
429 }
430 });
431
432 operation_type.map(String::as_str)
433 }
434}
435#[cfg(test)]
436mod tests {
437 use chrono::{TimeZone, Utc};
438 use insta::{assert_debug_snapshot, assert_json_snapshot};
439 use itertools::Itertools;
440 use relay_base_schema::events::EventType;
441 use relay_base_schema::metrics::DurationUnit;
442 use relay_base_schema::spans::SpanStatus;
443 use relay_event_schema::protocol::{
444 ClientSdkInfo, Context, ContextInner, Contexts, DebugImage, DebugMeta, EventId, Exception,
445 Frame, Geo, IpAddr, LenientString, Level, LogEntry, PairList, RawStacktrace, ReplayContext,
446 Request, Span, Stacktrace, TagEntry, Tags, TraceContext, User, Values,
447 };
448 use relay_protocol::{
449 Annotated, Error, ErrorKind, FromValue, Object, SerializableAnnotated, Value,
450 assert_annotated_snapshot, get_path, get_value,
451 };
452 use serde_json::json;
453 use similar_asserts::assert_eq;
454 use uuid::Uuid;
455
456 use crate::stacktrace::normalize_non_raw_frame;
457 use crate::validation::validate_event;
458 use crate::{EventValidationConfig, GeoIpLookup, NormalizationConfig, normalize_event};
459
460 use super::*;
461
462 #[test]
464 fn test_model_cost_version_sent_as_number() {
465 let original_v2 = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#;
467 let deserialized_v2: ModelCosts = serde_json::from_str(original_v2).unwrap();
468 assert_debug_snapshot!(
469 deserialized_v2,
470 @r#"
471 ModelCosts {
472 version: 2,
473 models: {
474 Pattern {
475 pattern: "gpt-4",
476 options: Options {
477 case_insensitive: false,
478 },
479 strategy: Literal(
480 Literal(
481 "gpt-4",
482 ),
483 ),
484 }: ModelCostV2 {
485 input_per_token: 0.03,
486 output_per_token: 0.06,
487 output_reasoning_per_token: 0.12,
488 input_cached_per_token: 0.015,
489 },
490 },
491 }
492 "#,
493 );
494
495 let original_unknown = r#"{"version":99,"models":{}}"#;
497 let deserialized_unknown: ModelCosts = serde_json::from_str(original_unknown).unwrap();
498 assert_eq!(deserialized_unknown.version, 99);
499 assert!(!deserialized_unknown.is_enabled());
500 }
501
502 #[test]
503 fn test_model_cost_config_v2() {
504 let original = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#;
505 let deserialized: ModelCosts = serde_json::from_str(original).unwrap();
506 assert_debug_snapshot!(deserialized, @r#"
507 ModelCosts {
508 version: 2,
509 models: {
510 Pattern {
511 pattern: "gpt-4",
512 options: Options {
513 case_insensitive: false,
514 },
515 strategy: Literal(
516 Literal(
517 "gpt-4",
518 ),
519 ),
520 }: ModelCostV2 {
521 input_per_token: 0.03,
522 output_per_token: 0.06,
523 output_reasoning_per_token: 0.12,
524 input_cached_per_token: 0.015,
525 },
526 },
527 }
528 "#);
529
530 let serialized = serde_json::to_string(&deserialized).unwrap();
531 assert_eq!(&serialized, original);
532 }
533
534 #[test]
535 fn test_model_cost_functionality_v2() {
536 let mut models_map = HashMap::new();
538 models_map.insert(
539 Pattern::new("gpt-4").unwrap(),
540 ModelCostV2 {
541 input_per_token: 0.03,
542 output_per_token: 0.06,
543 output_reasoning_per_token: 0.12,
544 input_cached_per_token: 0.015,
545 },
546 );
547 let v2_config = ModelCosts {
548 version: 2,
549 models: models_map,
550 };
551 assert!(v2_config.is_enabled());
552 let cost = v2_config.cost_per_token("gpt-4").unwrap();
553 assert_eq!(
554 cost,
555 &ModelCostV2 {
556 input_per_token: 0.03,
557 output_per_token: 0.06,
558 output_reasoning_per_token: 0.12,
559 input_cached_per_token: 0.015,
560 }
561 );
562 }
563
564 #[test]
565 fn test_model_cost_glob_matching() {
566 let mut models_map = HashMap::new();
568 models_map.insert(
569 Pattern::new("gpt-4*").unwrap(),
570 ModelCostV2 {
571 input_per_token: 0.03,
572 output_per_token: 0.06,
573 output_reasoning_per_token: 0.12,
574 input_cached_per_token: 0.015,
575 },
576 );
577 models_map.insert(
578 Pattern::new("gpt-4-2xxx").unwrap(),
579 ModelCostV2 {
580 input_per_token: 0.0007,
581 output_per_token: 0.0008,
582 output_reasoning_per_token: 0.0016,
583 input_cached_per_token: 0.00035,
584 },
585 );
586
587 let v2_config = ModelCosts {
588 version: 2,
589 models: models_map,
590 };
591 assert!(v2_config.is_enabled());
592
593 let cost = v2_config.cost_per_token("gpt-4-v1").unwrap();
595 assert_eq!(
596 cost,
597 &ModelCostV2 {
598 input_per_token: 0.03,
599 output_per_token: 0.06,
600 output_reasoning_per_token: 0.12,
601 input_cached_per_token: 0.015,
602 }
603 );
604
605 let cost = v2_config.cost_per_token("gpt-4-2xxx").unwrap();
606 assert_eq!(
607 cost,
608 &ModelCostV2 {
609 input_per_token: 0.0007,
610 output_per_token: 0.0008,
611 output_reasoning_per_token: 0.0016,
612 input_cached_per_token: 0.00035,
613 }
614 );
615
616 assert_eq!(v2_config.cost_per_token("unknown-model"), None);
617 }
618
619 #[test]
620 fn test_model_cost_unknown_version() {
621 let unknown_version_json = r#"{"version":3,"models":{"some-model":{"inputPerToken":0.01,"outputPerToken":0.02,"outputReasoningPerToken":0.03,"inputCachedPerToken":0.005}}}"#;
623 let deserialized: ModelCosts = serde_json::from_str(unknown_version_json).unwrap();
624 assert_eq!(deserialized.version, 3);
625 assert!(!deserialized.is_enabled());
626 assert_eq!(deserialized.cost_per_token("some-model"), None);
627
628 let version_zero_json = r#"{"version":0,"models":{}}"#;
630 let deserialized: ModelCosts = serde_json::from_str(version_zero_json).unwrap();
631 assert_eq!(deserialized.version, 0);
632 assert!(!deserialized.is_enabled());
633 }
634
635 #[test]
636 fn test_ai_operation_type_map_serialization() {
637 let mut operation_types = HashMap::new();
639 operation_types.insert(
640 Pattern::new("gen_ai.chat*").unwrap(),
641 "Inference".to_owned(),
642 );
643 operation_types.insert(
644 Pattern::new("gen_ai.execute_tool").unwrap(),
645 "Tool".to_owned(),
646 );
647
648 let original = AiOperationTypeMap {
649 version: 1,
650 operation_types,
651 };
652
653 let json = serde_json::to_string(&original).unwrap();
654 let deserialized: AiOperationTypeMap = serde_json::from_str(&json).unwrap();
655
656 assert!(deserialized.is_enabled());
657 assert_eq!(
658 deserialized.get_operation_type("gen_ai.chat.completions"),
659 Some("Inference")
660 );
661 assert_eq!(
662 deserialized.get_operation_type("gen_ai.execute_tool"),
663 Some("Tool")
664 );
665 assert_eq!(deserialized.get_operation_type("unknown_op"), None);
666 }
667
668 #[test]
669 fn test_ai_operation_type_map_pattern_matching() {
670 let mut operation_types = HashMap::new();
671 operation_types.insert(Pattern::new("gen_ai.*").unwrap(), "default".to_owned());
672 operation_types.insert(Pattern::new("gen_ai.chat").unwrap(), "chat".to_owned());
673
674 let map = AiOperationTypeMap {
675 version: 1,
676 operation_types,
677 };
678
679 let result = map.get_operation_type("gen_ai.chat");
680 assert!(Some("chat") == result);
681
682 let result = map.get_operation_type("gen_ai.chat.completions");
683 assert!(Some("default") == result);
684
685 assert_eq!(map.get_operation_type("gen_ai.other"), Some("default"));
686
687 assert_eq!(map.get_operation_type("other.operation"), None);
688 }
689
690 #[test]
691 fn test_normalize_ai_model_name() {
692 assert_eq!(
694 normalize_ai_model_name("claude-4-sonnet-20250522"),
695 "claude-4-sonnet"
696 );
697 assert_eq!(normalize_ai_model_name("o3-pro-2025-06-10"), "o3-pro");
698 assert_eq!(
699 normalize_ai_model_name("claude-3-5-haiku@20241022"),
700 "claude-3-5-haiku"
701 );
702
703 assert_eq!(
705 normalize_ai_model_name("claude-opus-4-1-20250805-v1:0"),
706 "claude-opus-4-1"
707 );
708 assert_eq!(
709 normalize_ai_model_name("us.anthropic.claude-sonnet-4-20250514-v1:0"),
710 "us.anthropic.claude-sonnet-4"
711 );
712
713 assert_eq!(normalize_ai_model_name("gpt-4-v1.0"), "gpt-4");
715 assert_eq!(normalize_ai_model_name("gpt-4-v1:0"), "gpt-4");
716 assert_eq!(normalize_ai_model_name("gpt-4-v1"), "gpt-4");
717 assert_eq!(normalize_ai_model_name("model_v2"), "model");
718
719 assert_eq!(normalize_ai_model_name("gpt-4.5"), "gpt-4.5");
721
722 assert_eq!(
725 normalize_ai_model_name("anthropic.claude-3-5-sonnet-20241022-v2:0"),
726 "anthropic.claude-3-5-sonnet"
727 );
728
729 assert_eq!(normalize_ai_model_name("gpt-4"), "gpt-4");
731 assert_eq!(normalize_ai_model_name("claude-3-opus"), "claude-3-opus");
732 }
733
734 #[test]
735 fn test_merge_builtin_measurement_keys() {
736 let foo = BuiltinMeasurementKey::new("foo", MetricUnit::Duration(DurationUnit::Hour));
737 let bar = BuiltinMeasurementKey::new("bar", MetricUnit::Duration(DurationUnit::Day));
738 let baz = BuiltinMeasurementKey::new("baz", MetricUnit::Duration(DurationUnit::Week));
739
740 let proj = MeasurementsConfig {
741 builtin_measurements: vec![foo.clone(), bar.clone()],
742 max_custom_measurements: 4,
743 };
744
745 let glob = MeasurementsConfig {
746 builtin_measurements: vec![baz.clone(), bar.clone()],
748 max_custom_measurements: 4,
749 };
750 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
751
752 let keys = dynamic_config.builtin_measurement_keys().collect_vec();
753
754 assert_eq!(keys, vec![&foo, &bar, &baz]);
755 }
756
757 #[test]
758 fn test_max_custom_measurement() {
759 let dynamic_config = CombinedMeasurementsConfig::new(None, None);
761 assert!(dynamic_config.max_custom_measurements().is_none());
762
763 let proj = MeasurementsConfig {
764 builtin_measurements: vec![],
765 max_custom_measurements: 3,
766 };
767
768 let glob = MeasurementsConfig {
769 builtin_measurements: vec![],
770 max_custom_measurements: 4,
771 };
772
773 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), None);
775 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
776
777 let dynamic_config = CombinedMeasurementsConfig::new(None, Some(&glob));
779 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 4);
780
781 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
783 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
784 }
785
786 #[test]
787 fn test_geo_from_ip_address() {
788 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
789
790 let json = r#"{
791 "user": {
792 "ip_address": "2.125.160.216"
793 }
794 }"#;
795 let mut event = Annotated::<Event>::from_json(json).unwrap();
796
797 normalize_event(
798 &mut event,
799 &NormalizationConfig {
800 geoip_lookup: Some(&lookup),
801 ..Default::default()
802 },
803 );
804
805 let expected = Annotated::new(Geo {
806 country_code: Annotated::new("GB".to_owned()),
807 city: Annotated::new("Boxford".to_owned()),
808 subdivision: Annotated::new("England".to_owned()),
809 region: Annotated::new("United Kingdom".to_owned()),
810 ..Geo::default()
811 });
812 assert_eq!(get_value!(event.user!).geo, expected);
813 }
814
815 #[test]
816 fn test_user_ip_from_remote_addr() {
817 let mut event = Annotated::new(Event {
818 request: Annotated::from(Request {
819 env: Annotated::new({
820 let mut map = Object::new();
821 map.insert(
822 "REMOTE_ADDR".to_owned(),
823 Annotated::new(Value::String("2.125.160.216".to_owned())),
824 );
825 map
826 }),
827 ..Request::default()
828 }),
829 platform: Annotated::new("javascript".to_owned()),
830 ..Event::default()
831 });
832
833 normalize_event(&mut event, &NormalizationConfig::default());
834
835 let ip_addr = get_value!(event.user.ip_address!);
836 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
837 }
838
839 #[test]
840 fn test_user_ip_from_invalid_remote_addr() {
841 let mut event = Annotated::new(Event {
842 request: Annotated::from(Request {
843 env: Annotated::new({
844 let mut map = Object::new();
845 map.insert(
846 "REMOTE_ADDR".to_owned(),
847 Annotated::new(Value::String("whoops".to_owned())),
848 );
849 map
850 }),
851 ..Request::default()
852 }),
853 platform: Annotated::new("javascript".to_owned()),
854 ..Event::default()
855 });
856
857 normalize_event(&mut event, &NormalizationConfig::default());
858
859 assert_eq!(Annotated::empty(), event.value().unwrap().user);
860 }
861
862 #[test]
863 fn test_user_ip_from_client_ip_without_auto() {
864 let mut event = Annotated::new(Event {
865 platform: Annotated::new("javascript".to_owned()),
866 ..Default::default()
867 });
868
869 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
870
871 normalize_event(
872 &mut event,
873 &NormalizationConfig {
874 client_ip: Some(&ip_address),
875 infer_ip_address: true,
876 ..Default::default()
877 },
878 );
879
880 let ip_addr = get_value!(event.user.ip_address!);
881 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
882 }
883
884 #[test]
885 fn test_user_ip_from_client_ip_with_auto() {
886 let mut event = Annotated::new(Event {
887 user: Annotated::new(User {
888 ip_address: Annotated::new(IpAddr::auto()),
889 ..Default::default()
890 }),
891 ..Default::default()
892 });
893
894 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
895
896 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
897 normalize_event(
898 &mut event,
899 &NormalizationConfig {
900 client_ip: Some(&ip_address),
901 geoip_lookup: Some(&geo),
902 infer_ip_address: true,
903 ..Default::default()
904 },
905 );
906
907 let user = get_value!(event.user!);
908 let ip_addr = user.ip_address.value().expect("ip address missing");
909
910 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
911 assert!(user.geo.value().is_some());
912 }
913
914 #[test]
915 fn test_user_ip_from_client_ip_without_appropriate_platform() {
916 let mut event = Annotated::new(Event::default());
917
918 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
919 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
920 normalize_event(
921 &mut event,
922 &NormalizationConfig {
923 client_ip: Some(&ip_address),
924 geoip_lookup: Some(&geo),
925 ..Default::default()
926 },
927 );
928
929 let user = get_value!(event.user!);
930 assert!(user.ip_address.value().is_none());
931 assert!(user.geo.value().is_some());
932 }
933
934 #[test]
935 fn test_geo_present_if_ip_inferring_disabled() {
936 let mut event = Annotated::new(Event {
937 user: Annotated::new(User {
938 ip_address: Annotated::new(IpAddr::auto()),
939 ..Default::default()
940 }),
941 ..Default::default()
942 });
943
944 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
945 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
946
947 normalize_event(
948 &mut event,
949 &NormalizationConfig {
950 client_ip: Some(&ip_address),
951 geoip_lookup: Some(&geo),
952 infer_ip_address: false,
953 ..Default::default()
954 },
955 );
956
957 let user = get_value!(event.user!);
958 assert!(user.ip_address.value().unwrap().is_auto());
959 assert!(user.geo.value().is_some());
960 }
961
962 #[test]
963 fn test_geo_and_ip_present_if_ip_inferring_enabled() {
964 let mut event = Annotated::new(Event {
965 user: Annotated::new(User {
966 ip_address: Annotated::new(IpAddr::auto()),
967 ..Default::default()
968 }),
969 ..Default::default()
970 });
971
972 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
973 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
974
975 normalize_event(
976 &mut event,
977 &NormalizationConfig {
978 client_ip: Some(&ip_address),
979 geoip_lookup: Some(&geo),
980 infer_ip_address: true,
981 ..Default::default()
982 },
983 );
984
985 let user = get_value!(event.user!);
986 assert_eq!(
987 user.ip_address.value(),
988 Some(&IpAddr::parse("2.125.160.216").unwrap())
989 );
990 assert!(user.geo.value().is_some());
991 }
992
993 #[test]
994 fn test_event_level_defaulted() {
995 let mut event = Annotated::new(Event::default());
996 normalize_event(&mut event, &NormalizationConfig::default());
997 assert_eq!(get_value!(event.level), Some(&Level::Error));
998 }
999
1000 #[test]
1001 fn test_transaction_level_untouched() {
1002 let mut event = Annotated::new(Event {
1003 ty: Annotated::new(EventType::Transaction),
1004 timestamp: Annotated::new(Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into()),
1005 start_timestamp: Annotated::new(
1006 Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into(),
1007 ),
1008 contexts: {
1009 let mut contexts = Contexts::new();
1010 contexts.add(TraceContext {
1011 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1012 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1013 op: Annotated::new("http.server".to_owned()),
1014 ..Default::default()
1015 });
1016 Annotated::new(contexts)
1017 },
1018 ..Event::default()
1019 });
1020 normalize_event(&mut event, &NormalizationConfig::default());
1021 assert_eq!(get_value!(event.level), Some(&Level::Info));
1022 }
1023
1024 #[test]
1025 fn test_environment_tag_is_moved() {
1026 let mut event = Annotated::new(Event {
1027 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
1028 Annotated::new("environment".to_owned()),
1029 Annotated::new("despacito".to_owned()),
1030 ))]))),
1031 ..Event::default()
1032 });
1033
1034 normalize_event(&mut event, &NormalizationConfig::default());
1035
1036 let event = event.value().unwrap();
1037
1038 assert_eq!(event.environment.as_str(), Some("despacito"));
1039 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
1040 }
1041
1042 #[test]
1043 fn test_empty_environment_is_removed_and_overwritten_with_tag() {
1044 let mut event = Annotated::new(Event {
1045 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
1046 Annotated::new("environment".to_owned()),
1047 Annotated::new("despacito".to_owned()),
1048 ))]))),
1049 environment: Annotated::new("".to_owned()),
1050 ..Event::default()
1051 });
1052
1053 normalize_event(&mut event, &NormalizationConfig::default());
1054
1055 let event = event.value().unwrap();
1056
1057 assert_eq!(event.environment.as_str(), Some("despacito"));
1058 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
1059 }
1060
1061 #[test]
1062 fn test_empty_environment_is_removed() {
1063 let mut event = Annotated::new(Event {
1064 environment: Annotated::new("".to_owned()),
1065 ..Event::default()
1066 });
1067
1068 normalize_event(&mut event, &NormalizationConfig::default());
1069 assert_eq!(get_value!(event.environment), None);
1070 }
1071 #[test]
1072 fn test_replay_id_added_from_dsc() {
1073 let replay_id = Uuid::new_v4();
1074 let mut event = Annotated::new(Event {
1075 contexts: Annotated::new(Contexts(Object::new())),
1076 ..Event::default()
1077 });
1078 normalize_event(
1079 &mut event,
1080 &NormalizationConfig {
1081 replay_id: Some(replay_id),
1082 ..Default::default()
1083 },
1084 );
1085
1086 let event = event.value().unwrap();
1087
1088 assert_eq!(event.contexts, {
1089 let mut contexts = Contexts::new();
1090 contexts.add(ReplayContext {
1091 replay_id: Annotated::new(EventId(replay_id)),
1092 other: Object::default(),
1093 });
1094 Annotated::new(contexts)
1095 })
1096 }
1097
1098 #[test]
1099 fn test_none_environment_errors() {
1100 let mut event = Annotated::new(Event {
1101 environment: Annotated::new("none".to_owned()),
1102 ..Event::default()
1103 });
1104
1105 normalize_event(&mut event, &NormalizationConfig::default());
1106
1107 let environment = get_path!(event.environment!);
1108 let expected_original = &Value::String("none".to_owned());
1109
1110 assert_eq!(
1111 environment.meta().iter_errors().collect::<Vec<&Error>>(),
1112 vec![&Error::new(ErrorKind::InvalidData)],
1113 );
1114 assert_eq!(
1115 environment.meta().original_value().unwrap(),
1116 expected_original
1117 );
1118 assert_eq!(environment.value(), None);
1119 }
1120
1121 #[test]
1122 fn test_invalid_release_removed() {
1123 let mut event = Annotated::new(Event {
1124 release: Annotated::new(LenientString("Latest".to_owned())),
1125 ..Event::default()
1126 });
1127
1128 normalize_event(&mut event, &NormalizationConfig::default());
1129
1130 let release = get_path!(event.release!);
1131 let expected_original = &Value::String("Latest".to_owned());
1132
1133 assert_eq!(
1134 release.meta().iter_errors().collect::<Vec<&Error>>(),
1135 vec![&Error::new(ErrorKind::InvalidData)],
1136 );
1137 assert_eq!(release.meta().original_value().unwrap(), expected_original);
1138 assert_eq!(release.value(), None);
1139 }
1140
1141 #[test]
1142 fn test_top_level_keys_moved_into_tags() {
1143 let mut event = Annotated::new(Event {
1144 server_name: Annotated::new("foo".to_owned()),
1145 site: Annotated::new("foo".to_owned()),
1146 tags: Annotated::new(Tags(PairList(vec![
1147 Annotated::new(TagEntry(
1148 Annotated::new("site".to_owned()),
1149 Annotated::new("old".to_owned()),
1150 )),
1151 Annotated::new(TagEntry(
1152 Annotated::new("server_name".to_owned()),
1153 Annotated::new("old".to_owned()),
1154 )),
1155 ]))),
1156 ..Event::default()
1157 });
1158
1159 normalize_event(&mut event, &NormalizationConfig::default());
1160
1161 assert_eq!(get_value!(event.site), None);
1162 assert_eq!(get_value!(event.server_name), None);
1163
1164 assert_eq!(
1165 get_value!(event.tags!),
1166 &Tags(PairList(vec![
1167 Annotated::new(TagEntry(
1168 Annotated::new("site".to_owned()),
1169 Annotated::new("foo".to_owned()),
1170 )),
1171 Annotated::new(TagEntry(
1172 Annotated::new("server_name".to_owned()),
1173 Annotated::new("foo".to_owned()),
1174 )),
1175 ]))
1176 );
1177 }
1178
1179 #[test]
1180 fn test_internal_tags_removed() {
1181 let mut event = Annotated::new(Event {
1182 tags: Annotated::new(Tags(PairList(vec![
1183 Annotated::new(TagEntry(
1184 Annotated::new("release".to_owned()),
1185 Annotated::new("foo".to_owned()),
1186 )),
1187 Annotated::new(TagEntry(
1188 Annotated::new("dist".to_owned()),
1189 Annotated::new("foo".to_owned()),
1190 )),
1191 Annotated::new(TagEntry(
1192 Annotated::new("user".to_owned()),
1193 Annotated::new("foo".to_owned()),
1194 )),
1195 Annotated::new(TagEntry(
1196 Annotated::new("filename".to_owned()),
1197 Annotated::new("foo".to_owned()),
1198 )),
1199 Annotated::new(TagEntry(
1200 Annotated::new("function".to_owned()),
1201 Annotated::new("foo".to_owned()),
1202 )),
1203 Annotated::new(TagEntry(
1204 Annotated::new("something".to_owned()),
1205 Annotated::new("else".to_owned()),
1206 )),
1207 ]))),
1208 ..Event::default()
1209 });
1210
1211 normalize_event(&mut event, &NormalizationConfig::default());
1212
1213 assert_eq!(get_value!(event.tags!).len(), 1);
1214 }
1215
1216 #[test]
1217 fn test_empty_tags_removed() {
1218 let mut event = Annotated::new(Event {
1219 tags: Annotated::new(Tags(PairList(vec![
1220 Annotated::new(TagEntry(
1221 Annotated::new("".to_owned()),
1222 Annotated::new("foo".to_owned()),
1223 )),
1224 Annotated::new(TagEntry(
1225 Annotated::new("foo".to_owned()),
1226 Annotated::new("".to_owned()),
1227 )),
1228 Annotated::new(TagEntry(
1229 Annotated::new("something".to_owned()),
1230 Annotated::new("else".to_owned()),
1231 )),
1232 ]))),
1233 ..Event::default()
1234 });
1235
1236 normalize_event(&mut event, &NormalizationConfig::default());
1237
1238 assert_eq!(
1239 get_value!(event.tags!),
1240 &Tags(PairList(vec![
1241 Annotated::new(TagEntry(
1242 Annotated::from_error(Error::nonempty(), None),
1243 Annotated::new("foo".to_owned()),
1244 )),
1245 Annotated::new(TagEntry(
1246 Annotated::new("foo".to_owned()),
1247 Annotated::from_error(Error::nonempty(), None),
1248 )),
1249 Annotated::new(TagEntry(
1250 Annotated::new("something".to_owned()),
1251 Annotated::new("else".to_owned()),
1252 )),
1253 ]))
1254 );
1255 }
1256
1257 #[test]
1258 fn test_tags_deduplicated() {
1259 let mut event = Annotated::new(Event {
1260 tags: Annotated::new(Tags(PairList(vec![
1261 Annotated::new(TagEntry(
1262 Annotated::new("foo".to_owned()),
1263 Annotated::new("1".to_owned()),
1264 )),
1265 Annotated::new(TagEntry(
1266 Annotated::new("bar".to_owned()),
1267 Annotated::new("1".to_owned()),
1268 )),
1269 Annotated::new(TagEntry(
1270 Annotated::new("foo".to_owned()),
1271 Annotated::new("2".to_owned()),
1272 )),
1273 Annotated::new(TagEntry(
1274 Annotated::new("bar".to_owned()),
1275 Annotated::new("2".to_owned()),
1276 )),
1277 Annotated::new(TagEntry(
1278 Annotated::new("foo".to_owned()),
1279 Annotated::new("3".to_owned()),
1280 )),
1281 ]))),
1282 ..Event::default()
1283 });
1284
1285 normalize_event(&mut event, &NormalizationConfig::default());
1286
1287 assert_eq!(
1289 get_value!(event.tags!),
1290 &Tags(PairList(vec![
1291 Annotated::new(TagEntry(
1292 Annotated::new("foo".to_owned()),
1293 Annotated::new("1".to_owned()),
1294 )),
1295 Annotated::new(TagEntry(
1296 Annotated::new("bar".to_owned()),
1297 Annotated::new("1".to_owned()),
1298 )),
1299 ]))
1300 );
1301 }
1302
1303 #[test]
1304 fn test_transaction_status_defaulted_to_unknown() {
1305 let mut object = Object::new();
1306 let trace_context = TraceContext {
1307 status: Annotated::empty(),
1309 ..TraceContext::default()
1310 };
1311 object.insert(
1312 "trace".to_owned(),
1313 Annotated::new(ContextInner(Context::Trace(Box::new(trace_context)))),
1314 );
1315
1316 let mut event = Annotated::new(Event {
1317 contexts: Annotated::new(Contexts(object)),
1318 ..Event::default()
1319 });
1320 normalize_event(&mut event, &NormalizationConfig::default());
1321
1322 let event = event.value().unwrap();
1323
1324 let event_trace_context = event.context::<TraceContext>().unwrap();
1325 assert_eq!(
1326 event_trace_context.status,
1327 Annotated::new(SpanStatus::Unknown)
1328 )
1329 }
1330
1331 #[test]
1332 fn test_unknown_debug_image() {
1333 let mut event = Annotated::new(Event {
1334 debug_meta: Annotated::new(DebugMeta {
1335 images: Annotated::new(vec![Annotated::new(DebugImage::Other(Object::default()))]),
1336 ..DebugMeta::default()
1337 }),
1338 ..Event::default()
1339 });
1340
1341 normalize_event(&mut event, &NormalizationConfig::default());
1342
1343 assert_eq!(
1344 get_path!(event.debug_meta!),
1345 &Annotated::new(DebugMeta {
1346 images: Annotated::new(vec![Annotated::from_error(
1347 Error::invalid("unsupported debug image type"),
1348 Some(Value::Object(Object::default())),
1349 )]),
1350 ..DebugMeta::default()
1351 })
1352 );
1353 }
1354
1355 #[test]
1356 fn test_context_line_default() {
1357 let mut frame = Annotated::new(Frame {
1358 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1359 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1360 ..Frame::default()
1361 });
1362
1363 normalize_non_raw_frame(&mut frame);
1364
1365 let frame = frame.value().unwrap();
1366 assert_eq!(frame.context_line.as_str(), Some(""));
1367 }
1368
1369 #[test]
1370 fn test_context_line_retain() {
1371 let mut frame = Annotated::new(Frame {
1372 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1373 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1374 context_line: Annotated::new("some line".to_owned()),
1375 ..Frame::default()
1376 });
1377
1378 normalize_non_raw_frame(&mut frame);
1379
1380 let frame = frame.value().unwrap();
1381 assert_eq!(frame.context_line.as_str(), Some("some line"));
1382 }
1383
1384 #[test]
1385 fn test_frame_null_context_lines() {
1386 let mut frame = Annotated::new(Frame {
1387 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1388 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1389 ..Frame::default()
1390 });
1391
1392 normalize_non_raw_frame(&mut frame);
1393
1394 assert_eq!(
1395 *get_value!(frame.pre_context!),
1396 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1397 );
1398 assert_eq!(
1399 *get_value!(frame.post_context!),
1400 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1401 );
1402 }
1403
1404 #[test]
1405 fn test_too_long_distribution() {
1406 let json = r#"{
1407 "event_id": "52df9022835246eeb317dbd739ccd059",
1408 "fingerprint": [
1409 "{{ default }}"
1410 ],
1411 "platform": "other",
1412 "dist": "52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059"
1413}"#;
1414
1415 let mut event = Annotated::<Event>::from_json(json).unwrap();
1416
1417 normalize_event(&mut event, &NormalizationConfig::default());
1418
1419 let dist = &event.value().unwrap().dist;
1420 let result = &Annotated::<String>::from_error(
1421 Error::new(ErrorKind::ValueTooLong),
1422 Some(Value::String("52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059".to_owned()))
1423 );
1424 assert_eq!(dist, result);
1425 }
1426
1427 #[test]
1428 fn test_regression_backfills_abs_path_even_when_moving_stacktrace() {
1429 let mut event = Annotated::new(Event {
1430 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
1431 ty: Annotated::new("FooDivisionError".to_owned()),
1432 value: Annotated::new("hi".to_owned().into()),
1433 ..Exception::default()
1434 })])),
1435 stacktrace: Annotated::new(
1436 RawStacktrace {
1437 frames: Annotated::new(vec![Annotated::new(Frame {
1438 module: Annotated::new("MyModule".to_owned()),
1439 filename: Annotated::new("MyFilename".into()),
1440 function: Annotated::new("Void FooBar()".to_owned()),
1441 ..Frame::default()
1442 })]),
1443 ..RawStacktrace::default()
1444 }
1445 .into(),
1446 ),
1447 ..Event::default()
1448 });
1449
1450 normalize_event(&mut event, &NormalizationConfig::default());
1451
1452 assert_eq!(
1453 get_value!(event.exceptions.values[0].stacktrace!),
1454 &Stacktrace(RawStacktrace {
1455 frames: Annotated::new(vec![Annotated::new(Frame {
1456 module: Annotated::new("MyModule".to_owned()),
1457 filename: Annotated::new("MyFilename".into()),
1458 abs_path: Annotated::new("MyFilename".into()),
1459 function: Annotated::new("Void FooBar()".to_owned()),
1460 ..Frame::default()
1461 })]),
1462 ..RawStacktrace::default()
1463 })
1464 );
1465 }
1466
1467 #[test]
1468 fn test_parses_sdk_info_from_header() {
1469 let mut event = Annotated::new(Event::default());
1470
1471 normalize_event(
1472 &mut event,
1473 &NormalizationConfig {
1474 client: Some("_fooBar/0.0.0".to_owned()),
1475 ..Default::default()
1476 },
1477 );
1478
1479 assert_eq!(
1480 get_path!(event.client_sdk!),
1481 &Annotated::new(ClientSdkInfo {
1482 name: Annotated::new("_fooBar".to_owned()),
1483 version: Annotated::new("0.0.0".to_owned()),
1484 ..ClientSdkInfo::default()
1485 })
1486 );
1487 }
1488
1489 #[test]
1490 fn test_discards_received() {
1491 let mut event = Annotated::new(Event {
1492 received: FromValue::from_value(Annotated::new(Value::U64(696_969_696_969))),
1493 ..Default::default()
1494 });
1495
1496 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1497 normalize_event(&mut event, &NormalizationConfig::default());
1498
1499 assert_eq!(get_value!(event.received!), get_value!(event.timestamp!));
1500 }
1501
1502 #[test]
1503 fn test_grouping_config() {
1504 let mut event = Annotated::new(Event {
1505 logentry: Annotated::from(LogEntry {
1506 message: Annotated::new("Hello World!".to_owned().into()),
1507 ..Default::default()
1508 }),
1509 ..Default::default()
1510 });
1511
1512 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1513 normalize_event(
1514 &mut event,
1515 &NormalizationConfig {
1516 grouping_config: Some(json!({
1517 "id": "legacy:1234-12-12".to_owned(),
1518 })),
1519 ..Default::default()
1520 },
1521 );
1522
1523 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1524 ".event_id" => "[event-id]",
1525 ".received" => "[received]",
1526 ".timestamp" => "[timestamp]"
1527 }, @r###"
1528 {
1529 "event_id": "[event-id]",
1530 "level": "error",
1531 "type": "default",
1532 "logentry": {
1533 "formatted": "Hello World!",
1534 },
1535 "logger": "",
1536 "platform": "other",
1537 "timestamp": "[timestamp]",
1538 "received": "[received]",
1539 "grouping_config": {
1540 "id": "legacy:1234-12-12",
1541 },
1542 }
1543 "###);
1544 }
1545
1546 #[test]
1547 fn test_logentry_error() {
1548 let json = r#"
1549{
1550 "event_id": "74ad1301f4df489ead37d757295442b1",
1551 "timestamp": 1668148328.308933,
1552 "received": 1668148328.308933,
1553 "level": "error",
1554 "platform": "python",
1555 "logentry": {
1556 "params": [
1557 "bogus"
1558 ],
1559 "formatted": 42
1560 }
1561}
1562"#;
1563 let mut event = Annotated::<Event>::from_json(json).unwrap();
1564
1565 normalize_event(&mut event, &NormalizationConfig::default());
1566
1567 assert_json_snapshot!(SerializableAnnotated(&event), {".received" => "[received]"}, @r###"
1568 {
1569 "event_id": "74ad1301f4df489ead37d757295442b1",
1570 "level": "error",
1571 "type": "default",
1572 "logentry": null,
1573 "logger": "",
1574 "platform": "python",
1575 "timestamp": 1668148328.308933,
1576 "received": "[received]",
1577 "_meta": {
1578 "logentry": {
1579 "": {
1580 "err": [
1581 [
1582 "invalid_data",
1583 {
1584 "reason": "no message present"
1585 }
1586 ]
1587 ],
1588 "val": {
1589 "formatted": null,
1590 "message": null,
1591 "params": [
1592 "bogus"
1593 ]
1594 }
1595 }
1596 }
1597 }
1598 }
1599 "###)
1600 }
1601
1602 #[test]
1603 fn test_future_timestamp() {
1604 let mut event = Annotated::new(Event {
1605 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 2, 0).unwrap().into()),
1606 ..Default::default()
1607 });
1608
1609 let received_at = Some(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap());
1610 let max_secs_in_past = Some(30 * 24 * 3600);
1611 let max_secs_in_future = Some(60);
1612
1613 validate_event(
1614 &mut event,
1615 &EventValidationConfig {
1616 received_at,
1617 max_secs_in_past,
1618 max_secs_in_future,
1619 is_validated: false,
1620 ..Default::default()
1621 },
1622 )
1623 .unwrap();
1624 normalize_event(&mut event, &NormalizationConfig::default());
1625
1626 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1627 ".event_id" => "[event-id]",
1628 }, @r###"
1629 {
1630 "event_id": "[event-id]",
1631 "level": "error",
1632 "type": "default",
1633 "logger": "",
1634 "platform": "other",
1635 "timestamp": 946857600.0,
1636 "received": 946857600.0,
1637 "_meta": {
1638 "timestamp": {
1639 "": Meta(Some(MetaInner(
1640 err: [
1641 [
1642 "future_timestamp",
1643 {
1644 "sdk_time": "2000-01-03T00:02:00+00:00",
1645 "server_time": "2000-01-03T00:00:00+00:00",
1646 },
1647 ],
1648 ],
1649 ))),
1650 },
1651 },
1652 }
1653 "###);
1654 }
1655
1656 #[test]
1657 fn test_past_timestamp() {
1658 let mut event = Annotated::new(Event {
1659 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap().into()),
1660 ..Default::default()
1661 });
1662
1663 let received_at = Some(Utc.with_ymd_and_hms(2000, 3, 3, 0, 0, 0).unwrap());
1664 let max_secs_in_past = Some(30 * 24 * 3600);
1665 let max_secs_in_future = Some(60);
1666
1667 validate_event(
1668 &mut event,
1669 &EventValidationConfig {
1670 received_at,
1671 max_secs_in_past,
1672 max_secs_in_future,
1673 is_validated: false,
1674 ..Default::default()
1675 },
1676 )
1677 .unwrap();
1678 normalize_event(&mut event, &NormalizationConfig::default());
1679
1680 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1681 ".event_id" => "[event-id]",
1682 }, @r###"
1683 {
1684 "event_id": "[event-id]",
1685 "level": "error",
1686 "type": "default",
1687 "logger": "",
1688 "platform": "other",
1689 "timestamp": 952041600.0,
1690 "received": 952041600.0,
1691 "_meta": {
1692 "timestamp": {
1693 "": Meta(Some(MetaInner(
1694 err: [
1695 [
1696 "past_timestamp",
1697 {
1698 "sdk_time": "2000-01-03T00:00:00+00:00",
1699 "server_time": "2000-03-03T00:00:00+00:00",
1700 },
1701 ],
1702 ],
1703 ))),
1704 },
1705 },
1706 }
1707 "###);
1708 }
1709
1710 #[test]
1711 fn test_normalize_logger_empty() {
1712 let mut event = Event::from_value(
1713 serde_json::json!({
1714 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1715 "platform": "java",
1716 "logger": "",
1717 })
1718 .into(),
1719 );
1720
1721 normalize_event(&mut event, &NormalizationConfig::default());
1722 assert_annotated_snapshot!(event);
1723 }
1724
1725 #[test]
1726 fn test_normalize_logger_trimmed() {
1727 let mut event = Event::from_value(
1728 serde_json::json!({
1729 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1730 "platform": "java",
1731 "logger": " \t \t ",
1732 })
1733 .into(),
1734 );
1735
1736 normalize_event(&mut event, &NormalizationConfig::default());
1737 assert_annotated_snapshot!(event);
1738 }
1739
1740 #[test]
1741 fn test_normalize_logger_short_no_trimming() {
1742 let mut event = Event::from_value(
1743 serde_json::json!({
1744 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1745 "platform": "java",
1746 "logger": "my.short-logger.isnt_trimmed",
1747 })
1748 .into(),
1749 );
1750
1751 normalize_event(&mut event, &NormalizationConfig::default());
1752 assert_annotated_snapshot!(event);
1753 }
1754
1755 #[test]
1756 fn test_normalize_logger_exact_length() {
1757 let mut event = Event::from_value(
1758 serde_json::json!({
1759 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1760 "platform": "java",
1761 "logger": "this_is-exactly-the_max_len.012345678901234567890123456789012345",
1762 })
1763 .into(),
1764 );
1765
1766 normalize_event(&mut event, &NormalizationConfig::default());
1767 assert_annotated_snapshot!(event);
1768 }
1769
1770 #[test]
1771 fn test_normalize_logger_too_long_single_word() {
1772 let mut event = Event::from_value(
1773 serde_json::json!({
1774 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1775 "platform": "java",
1776 "logger": "this_is-way_too_long-and_we_only_have_one_word-so_we_cant_smart_trim",
1777 })
1778 .into(),
1779 );
1780
1781 normalize_event(&mut event, &NormalizationConfig::default());
1782 assert_annotated_snapshot!(event);
1783 }
1784
1785 #[test]
1786 fn test_normalize_logger_word_trimmed_at_max() {
1787 let mut event = Event::from_value(
1788 serde_json::json!({
1789 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1790 "platform": "java",
1791 "logger": "already_out.out.in.this_part-is-kept.this_right_here-is_an-extremely_long_word",
1792 })
1793 .into(),
1794 );
1795
1796 normalize_event(&mut event, &NormalizationConfig::default());
1797 assert_annotated_snapshot!(event);
1798 }
1799
1800 #[test]
1801 fn test_normalize_logger_word_trimmed_before_max() {
1802 let mut event = Event::from_value(
1805 serde_json::json!({
1806 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1807 "platform": "java",
1808 "logger": "super_out.this_is_already_out_too.this_part-is-kept.this_right_here-is_a-very_long_word",
1809 })
1810 .into(),
1811 );
1812
1813 normalize_event(&mut event, &NormalizationConfig::default());
1814 assert_annotated_snapshot!(event);
1815 }
1816
1817 #[test]
1818 fn test_normalize_logger_word_leading_dots() {
1819 let mut event = Event::from_value(
1820 serde_json::json!({
1821 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1822 "platform": "java",
1823 "logger": "io.this-tests-the-smart-trimming-and-word-removal-around-dot.words",
1824 })
1825 .into(),
1826 );
1827
1828 normalize_event(&mut event, &NormalizationConfig::default());
1829 assert_annotated_snapshot!(event);
1830 }
1831
1832 #[test]
1833 fn test_normalization_is_idempotent() {
1834 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1836 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1837 let mut event = Annotated::new(Event {
1838 ty: Annotated::new(EventType::Transaction),
1839 transaction: Annotated::new("/".to_owned()),
1840 timestamp: Annotated::new(end.into()),
1841 start_timestamp: Annotated::new(start.into()),
1842 contexts: {
1843 let mut contexts = Contexts::new();
1844 contexts.add(TraceContext {
1845 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1846 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1847 op: Annotated::new("http.server".to_owned()),
1848 ..Default::default()
1849 });
1850 Annotated::new(contexts)
1851 },
1852 spans: Annotated::new(vec![Annotated::new(Span {
1853 timestamp: Annotated::new(
1854 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1855 ),
1856 start_timestamp: Annotated::new(
1857 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1858 ),
1859 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1860 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1861
1862 ..Default::default()
1863 })]),
1864 ..Default::default()
1865 });
1866
1867 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1868 relay_event_schema::processor::apply(event, |e, _m| {
1869 e.received = Annotated::empty();
1870 Ok(())
1871 })
1872 .unwrap();
1873 event
1874 }
1875
1876 normalize_event(&mut event, &NormalizationConfig::default());
1877 let first = remove_received_from_event(&mut event.clone())
1878 .to_json()
1879 .unwrap();
1880 normalize_event(&mut event, &NormalizationConfig::default());
1883 let second = remove_received_from_event(&mut event.clone())
1884 .to_json()
1885 .unwrap();
1886 assert_eq!(&first, &second, "idempotency check failed");
1887
1888 normalize_event(&mut event, &NormalizationConfig::default());
1889 let third = remove_received_from_event(&mut event.clone())
1890 .to_json()
1891 .unwrap();
1892 assert_eq!(&second, &third, "idempotency check failed");
1893 }
1894
1895 #[test]
1902 fn test_full_normalization_is_idempotent() {
1903 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1904 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1905 let mut event = Annotated::new(Event {
1906 ty: Annotated::new(EventType::Transaction),
1907 transaction: Annotated::new("/".to_owned()),
1908 timestamp: Annotated::new(end.into()),
1909 start_timestamp: Annotated::new(start.into()),
1910 contexts: {
1911 let mut contexts = Contexts::new();
1912 contexts.add(TraceContext {
1913 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1914 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1915 op: Annotated::new("http.server".to_owned()),
1916 ..Default::default()
1917 });
1918 Annotated::new(contexts)
1919 },
1920 spans: Annotated::new(vec![Annotated::new(Span {
1921 timestamp: Annotated::new(
1922 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1923 ),
1924 start_timestamp: Annotated::new(
1925 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1926 ),
1927 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1928 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1929
1930 ..Default::default()
1931 })]),
1932 ..Default::default()
1933 });
1934
1935 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1936 relay_event_schema::processor::apply(event, |e, _m| {
1937 e.received = Annotated::empty();
1938 Ok(())
1939 })
1940 .unwrap();
1941 event
1942 }
1943
1944 let full_normalization_config = NormalizationConfig {
1945 is_renormalize: false,
1946 enable_trimming: true,
1947 remove_other: true,
1948 emit_event_errors: true,
1949 ..Default::default()
1950 };
1951
1952 normalize_event(&mut event, &full_normalization_config);
1953 let first = remove_received_from_event(&mut event.clone())
1954 .to_json()
1955 .unwrap();
1956 normalize_event(&mut event, &full_normalization_config);
1959 let second = remove_received_from_event(&mut event.clone())
1960 .to_json()
1961 .unwrap();
1962 assert_eq!(&first, &second, "idempotency check failed");
1963
1964 normalize_event(&mut event, &full_normalization_config);
1965 let third = remove_received_from_event(&mut event.clone())
1966 .to_json()
1967 .unwrap();
1968 assert_eq!(&second, &third, "idempotency check failed");
1969 }
1970
1971 #[test]
1972 fn test_normalize_validates_spans() {
1973 let event = Annotated::<Event>::from_json(
1974 r#"
1975 {
1976 "type": "transaction",
1977 "transaction": "/",
1978 "timestamp": 946684810.0,
1979 "start_timestamp": 946684800.0,
1980 "contexts": {
1981 "trace": {
1982 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1983 "span_id": "fa90fdead5f74053",
1984 "op": "http.server",
1985 "type": "trace"
1986 }
1987 },
1988 "spans": []
1989 }
1990 "#,
1991 )
1992 .unwrap();
1993
1994 for span in [
1996 r#"null"#,
1997 r#"{
1998 "timestamp": 946684810.0,
1999 "start_timestamp": 946684900.0,
2000 "span_id": "fa90fdead5f74053",
2001 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
2002 }"#,
2003 r#"{
2004 "timestamp": 946684810.0,
2005 "span_id": "fa90fdead5f74053",
2006 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
2007 }"#,
2008 r#"{
2009 "timestamp": 946684810.0,
2010 "start_timestamp": 946684800.0,
2011 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
2012 }"#,
2013 r#"{
2014 "timestamp": 946684810.0,
2015 "start_timestamp": 946684800.0,
2016 "span_id": "fa90fdead5f74053"
2017 }"#,
2018 ] {
2019 let mut modified_event = event.clone();
2020 let event_ref = modified_event.value_mut().as_mut().unwrap();
2021 event_ref
2022 .spans
2023 .set_value(Some(vec![Annotated::<Span>::from_json(span).unwrap()]));
2024
2025 let res = validate_event(&mut modified_event, &EventValidationConfig::default());
2026
2027 assert!(res.is_err(), "{span:?}");
2028 }
2029 }
2030
2031 #[test]
2032 fn test_normalization_respects_is_renormalize() {
2033 let mut event = Annotated::<Event>::from_json(
2034 r#"
2035 {
2036 "type": "default",
2037 "tags": [["environment", "some_environment"]]
2038 }
2039 "#,
2040 )
2041 .unwrap();
2042
2043 normalize_event(
2044 &mut event,
2045 &NormalizationConfig {
2046 is_renormalize: true,
2047 ..Default::default()
2048 },
2049 );
2050
2051 assert_debug_snapshot!(event.value().unwrap().tags, @r###"
2052 Tags(
2053 PairList(
2054 [
2055 TagEntry(
2056 "environment",
2057 "some_environment",
2058 ),
2059 ],
2060 ),
2061 )
2062 "###);
2063 }
2064
2065 #[test]
2066 fn test_geo_in_normalize() {
2067 let mut event = Annotated::<Event>::from_json(
2068 r#"
2069 {
2070 "type": "transaction",
2071 "transaction": "/foo/",
2072 "timestamp": 946684810.0,
2073 "start_timestamp": 946684800.0,
2074 "contexts": {
2075 "trace": {
2076 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
2077 "span_id": "fa90fdead5f74053",
2078 "op": "http.server",
2079 "type": "trace"
2080 }
2081 },
2082 "transaction_info": {
2083 "source": "url"
2084 },
2085 "user": {
2086 "ip_address": "2.125.160.216"
2087 }
2088 }
2089 "#,
2090 )
2091 .unwrap();
2092
2093 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
2094
2095 let user_geo = event.value().unwrap().user.value().unwrap().geo.value();
2097 assert!(user_geo.is_none());
2098
2099 normalize_event(
2100 &mut event,
2101 &NormalizationConfig {
2102 geoip_lookup: Some(&lookup),
2103 ..Default::default()
2104 },
2105 );
2106
2107 let user_geo = event
2109 .value()
2110 .unwrap()
2111 .user
2112 .value()
2113 .unwrap()
2114 .geo
2115 .value()
2116 .unwrap();
2117
2118 assert_eq!(user_geo.country_code.value().unwrap(), "GB");
2119 assert_eq!(user_geo.city.value().unwrap(), "Boxford");
2120 }
2121
2122 #[test]
2123 fn test_normalize_app_start_spans_only_for_react_native_3_to_4_4() {
2124 let mut event = Event {
2125 spans: Annotated::new(vec![Annotated::new(Span {
2126 op: Annotated::new("app_start_cold".to_owned()),
2127 ..Default::default()
2128 })]),
2129 client_sdk: Annotated::new(ClientSdkInfo {
2130 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2131 version: Annotated::new("4.5.0".to_owned()),
2132 ..Default::default()
2133 }),
2134 ..Default::default()
2135 };
2136 normalize_app_start_spans(&mut event);
2137 assert_debug_snapshot!(event.spans, @r###"
2138 [
2139 Span {
2140 timestamp: ~,
2141 start_timestamp: ~,
2142 exclusive_time: ~,
2143 op: "app_start_cold",
2144 span_id: ~,
2145 parent_span_id: ~,
2146 trace_id: ~,
2147 segment_id: ~,
2148 is_segment: ~,
2149 is_remote: ~,
2150 status: ~,
2151 description: ~,
2152 tags: ~,
2153 origin: ~,
2154 profile_id: ~,
2155 data: ~,
2156 links: ~,
2157 sentry_tags: ~,
2158 received: ~,
2159 measurements: ~,
2160 platform: ~,
2161 was_transaction: ~,
2162 kind: ~,
2163 performance_issues_spans: ~,
2164 other: {},
2165 },
2166 ]
2167 "###);
2168 }
2169
2170 #[test]
2171 fn test_normalize_app_start_cold_spans_for_react_native() {
2172 let mut event = Event {
2173 spans: Annotated::new(vec![Annotated::new(Span {
2174 op: Annotated::new("app_start_cold".to_owned()),
2175 ..Default::default()
2176 })]),
2177 client_sdk: Annotated::new(ClientSdkInfo {
2178 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2179 version: Annotated::new("4.4.0".to_owned()),
2180 ..Default::default()
2181 }),
2182 ..Default::default()
2183 };
2184 normalize_app_start_spans(&mut event);
2185 assert_debug_snapshot!(event.spans, @r###"
2186 [
2187 Span {
2188 timestamp: ~,
2189 start_timestamp: ~,
2190 exclusive_time: ~,
2191 op: "app.start.cold",
2192 span_id: ~,
2193 parent_span_id: ~,
2194 trace_id: ~,
2195 segment_id: ~,
2196 is_segment: ~,
2197 is_remote: ~,
2198 status: ~,
2199 description: ~,
2200 tags: ~,
2201 origin: ~,
2202 profile_id: ~,
2203 data: ~,
2204 links: ~,
2205 sentry_tags: ~,
2206 received: ~,
2207 measurements: ~,
2208 platform: ~,
2209 was_transaction: ~,
2210 kind: ~,
2211 performance_issues_spans: ~,
2212 other: {},
2213 },
2214 ]
2215 "###);
2216 }
2217
2218 #[test]
2219 fn test_normalize_app_start_warm_spans_for_react_native() {
2220 let mut event = Event {
2221 spans: Annotated::new(vec![Annotated::new(Span {
2222 op: Annotated::new("app_start_warm".to_owned()),
2223 ..Default::default()
2224 })]),
2225 client_sdk: Annotated::new(ClientSdkInfo {
2226 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2227 version: Annotated::new("4.4.0".to_owned()),
2228 ..Default::default()
2229 }),
2230 ..Default::default()
2231 };
2232 normalize_app_start_spans(&mut event);
2233 assert_debug_snapshot!(event.spans, @r###"
2234 [
2235 Span {
2236 timestamp: ~,
2237 start_timestamp: ~,
2238 exclusive_time: ~,
2239 op: "app.start.warm",
2240 span_id: ~,
2241 parent_span_id: ~,
2242 trace_id: ~,
2243 segment_id: ~,
2244 is_segment: ~,
2245 is_remote: ~,
2246 status: ~,
2247 description: ~,
2248 tags: ~,
2249 origin: ~,
2250 profile_id: ~,
2251 data: ~,
2252 links: ~,
2253 sentry_tags: ~,
2254 received: ~,
2255 measurements: ~,
2256 platform: ~,
2257 was_transaction: ~,
2258 kind: ~,
2259 performance_issues_spans: ~,
2260 other: {},
2261 },
2262 ]
2263 "###);
2264 }
2265}