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
221static VERSION_AND_DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
232 Regex::new(
233 r"(?x)
234 (
235 ([-_@]) # Required separator before date (-, _, or @)
236 (\d{4}[-/.]\d{2}[-/.]\d{2}|\d{8}) # Date: YYYY-MM-DD/YYYY/MM/DD/YYYY.MM.DD or YYYYMMDD
237 )? # Date is optional
238 (
239 [-_] # Required separator before version (- or _)
240 v\d+[:.]?\d* # Version: v1, v1.0, v1:0
241 ([-:].*)? # Optional trailing content
242 )? # Version is optional
243 $ # Must be at the end of the string
244 ",
245 )
246 .unwrap()
247});
248
249fn normalize_ai_model_name(model_id: &str) -> &str {
280 if let Some(captures) = VERSION_AND_DATE_REGEX.captures(model_id) {
281 let match_idx = captures.get(0).map(|m| m.start()).unwrap_or(model_id.len());
282 &model_id[..match_idx]
283 } else {
284 model_id
285 }
286}
287
288#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
290#[serde(rename_all = "camelCase")]
291pub struct ModelCostV2 {
292 pub input_per_token: f64,
294 pub output_per_token: f64,
296 pub output_reasoning_per_token: f64,
298 pub input_cached_per_token: f64,
300 pub input_cache_write_per_token: f64,
302}
303
304#[derive(Clone, Default, Debug, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ModelMetadata {
327 pub version: u16,
329
330 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
332 pub models: HashMap<Pattern, ModelMetadataEntry>,
333}
334
335impl ModelMetadata {
336 const SUPPORTED_VERSION: u16 = 1;
337
338 pub fn is_empty(&self) -> bool {
340 self.models.is_empty() || !self.is_enabled()
341 }
342
343 pub fn is_enabled(&self) -> bool {
345 self.version == Self::SUPPORTED_VERSION
346 }
347
348 pub fn cost_per_token(&self, model_id: &str) -> Option<&ModelCostV2> {
350 self.get(model_id).and_then(|entry| entry.costs.as_ref())
351 }
352
353 pub fn context_size(&self, model_id: &str) -> Option<u64> {
357 self.get(model_id)
358 .and_then(|entry| entry.context_size)
359 .filter(|&size| size > 0)
360 }
361
362 pub fn get(&self, model_id: &str) -> Option<&ModelMetadataEntry> {
364 if !self.is_enabled() {
365 return None;
366 }
367
368 let normalized_model_id = normalize_ai_model_name(model_id);
369
370 if let Some(value) = self.models.get(normalized_model_id) {
372 return Some(value);
373 }
374
375 self.models.iter().find_map(|(key, value)| {
377 if key.is_match(normalized_model_id) {
378 Some(value)
379 } else {
380 None
381 }
382 })
383 }
384}
385
386#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
388#[serde(rename_all = "camelCase")]
389pub struct ModelMetadataEntry {
390 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub costs: Option<ModelCostV2>,
393
394 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub context_size: Option<u64>,
397}
398
399#[cfg(test)]
400mod tests {
401 use chrono::{TimeZone, Utc};
402 use insta::{assert_debug_snapshot, assert_json_snapshot};
403 use itertools::Itertools;
404 use relay_base_schema::events::EventType;
405 use relay_base_schema::metrics::DurationUnit;
406 use relay_base_schema::spans::SpanStatus;
407 use relay_event_schema::protocol::{
408 ClientSdkInfo, Context, ContextInner, Contexts, DebugImage, DebugMeta, EventId, Exception,
409 Frame, Geo, IpAddr, LenientString, Level, LogEntry, PairList, RawStacktrace, ReplayContext,
410 Request, Span, Stacktrace, TagEntry, Tags, TraceContext, User, Values,
411 };
412 use relay_protocol::{
413 Annotated, Error, ErrorKind, FromValue, Object, SerializableAnnotated, Value,
414 assert_annotated_snapshot, get_path, get_value,
415 };
416 use serde_json::json;
417 use similar_asserts::assert_eq;
418 use uuid::Uuid;
419
420 use crate::stacktrace::normalize_non_raw_frame;
421 use crate::validation::validate_event;
422 use crate::{EventValidationConfig, GeoIpLookup, NormalizationConfig, normalize_event};
423
424 use super::*;
425
426 #[test]
427 fn test_normalize_ai_model_name() {
428 assert_eq!(
430 normalize_ai_model_name("claude-4-sonnet-20250522"),
431 "claude-4-sonnet"
432 );
433 assert_eq!(normalize_ai_model_name("o3-pro-2025-06-10"), "o3-pro");
434 assert_eq!(
435 normalize_ai_model_name("claude-3-5-haiku@20241022"),
436 "claude-3-5-haiku"
437 );
438
439 assert_eq!(
441 normalize_ai_model_name("claude-opus-4-1-20250805-v1:0"),
442 "claude-opus-4-1"
443 );
444 assert_eq!(
445 normalize_ai_model_name("us.anthropic.claude-sonnet-4-20250514-v1:0"),
446 "us.anthropic.claude-sonnet-4"
447 );
448
449 assert_eq!(normalize_ai_model_name("gpt-4-v1.0"), "gpt-4");
451 assert_eq!(normalize_ai_model_name("gpt-4-v1:0"), "gpt-4");
452 assert_eq!(normalize_ai_model_name("gpt-4-v1"), "gpt-4");
453 assert_eq!(normalize_ai_model_name("model_v2"), "model");
454
455 assert_eq!(normalize_ai_model_name("gpt-4.5"), "gpt-4.5");
457
458 assert_eq!(
461 normalize_ai_model_name("anthropic.claude-3-5-sonnet-20241022-v2:0"),
462 "anthropic.claude-3-5-sonnet"
463 );
464
465 assert_eq!(normalize_ai_model_name("gpt-4"), "gpt-4");
467 assert_eq!(normalize_ai_model_name("claude-3-opus"), "claude-3-opus");
468 }
469
470 #[test]
471 fn test_merge_builtin_measurement_keys() {
472 let foo = BuiltinMeasurementKey::new("foo", MetricUnit::Duration(DurationUnit::Hour));
473 let bar = BuiltinMeasurementKey::new("bar", MetricUnit::Duration(DurationUnit::Day));
474 let baz = BuiltinMeasurementKey::new("baz", MetricUnit::Duration(DurationUnit::Week));
475
476 let proj = MeasurementsConfig {
477 builtin_measurements: vec![foo.clone(), bar.clone()],
478 max_custom_measurements: 4,
479 };
480
481 let glob = MeasurementsConfig {
482 builtin_measurements: vec![baz.clone(), bar.clone()],
484 max_custom_measurements: 4,
485 };
486 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
487
488 let keys = dynamic_config.builtin_measurement_keys().collect_vec();
489
490 assert_eq!(keys, vec![&foo, &bar, &baz]);
491 }
492
493 #[test]
494 fn test_max_custom_measurement() {
495 let dynamic_config = CombinedMeasurementsConfig::new(None, None);
497 assert!(dynamic_config.max_custom_measurements().is_none());
498
499 let proj = MeasurementsConfig {
500 builtin_measurements: vec![],
501 max_custom_measurements: 3,
502 };
503
504 let glob = MeasurementsConfig {
505 builtin_measurements: vec![],
506 max_custom_measurements: 4,
507 };
508
509 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), None);
511 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
512
513 let dynamic_config = CombinedMeasurementsConfig::new(None, Some(&glob));
515 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 4);
516
517 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
519 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
520 }
521
522 #[test]
523 fn test_geo_from_ip_address() {
524 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
525
526 let json = r#"{
527 "user": {
528 "ip_address": "2.125.160.216"
529 }
530 }"#;
531 let mut event = Annotated::<Event>::from_json(json).unwrap();
532
533 normalize_event(
534 &mut event,
535 &NormalizationConfig {
536 geoip_lookup: Some(&lookup),
537 ..Default::default()
538 },
539 );
540
541 let expected = Annotated::new(Geo {
542 country_code: Annotated::new("GB".to_owned()),
543 city: Annotated::new("Boxford".to_owned()),
544 subdivision: Annotated::new("England".to_owned()),
545 region: Annotated::new("United Kingdom".to_owned()),
546 ..Geo::default()
547 });
548 assert_eq!(get_value!(event.user!).geo, expected);
549 }
550
551 #[test]
552 fn test_user_ip_from_remote_addr() {
553 let mut event = Annotated::new(Event {
554 request: Annotated::from(Request {
555 env: Annotated::new({
556 let mut map = Object::new();
557 map.insert(
558 "REMOTE_ADDR".to_owned(),
559 Annotated::new(Value::String("2.125.160.216".to_owned())),
560 );
561 map
562 }),
563 ..Request::default()
564 }),
565 platform: Annotated::new("javascript".to_owned()),
566 ..Event::default()
567 });
568
569 normalize_event(&mut event, &NormalizationConfig::default());
570
571 let ip_addr = get_value!(event.user.ip_address!);
572 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
573 }
574
575 #[test]
576 fn test_user_ip_from_invalid_remote_addr() {
577 let mut event = Annotated::new(Event {
578 request: Annotated::from(Request {
579 env: Annotated::new({
580 let mut map = Object::new();
581 map.insert(
582 "REMOTE_ADDR".to_owned(),
583 Annotated::new(Value::String("whoops".to_owned())),
584 );
585 map
586 }),
587 ..Request::default()
588 }),
589 platform: Annotated::new("javascript".to_owned()),
590 ..Event::default()
591 });
592
593 normalize_event(&mut event, &NormalizationConfig::default());
594
595 assert_eq!(Annotated::empty(), event.value().unwrap().user);
596 }
597
598 #[test]
599 fn test_user_ip_from_client_ip_without_auto() {
600 let mut event = Annotated::new(Event {
601 platform: Annotated::new("javascript".to_owned()),
602 ..Default::default()
603 });
604
605 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
606
607 normalize_event(
608 &mut event,
609 &NormalizationConfig {
610 client_ip: Some(&ip_address),
611 infer_ip_address: true,
612 ..Default::default()
613 },
614 );
615
616 let ip_addr = get_value!(event.user.ip_address!);
617 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
618 }
619
620 #[test]
621 fn test_user_ip_from_client_ip_with_auto() {
622 let mut event = Annotated::new(Event {
623 user: Annotated::new(User {
624 ip_address: Annotated::new(IpAddr::auto()),
625 ..Default::default()
626 }),
627 ..Default::default()
628 });
629
630 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
631
632 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
633 normalize_event(
634 &mut event,
635 &NormalizationConfig {
636 client_ip: Some(&ip_address),
637 geoip_lookup: Some(&geo),
638 infer_ip_address: true,
639 ..Default::default()
640 },
641 );
642
643 let user = get_value!(event.user!);
644 let ip_addr = user.ip_address.value().expect("ip address missing");
645
646 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
647 assert!(user.geo.value().is_some());
648 }
649
650 #[test]
651 fn test_user_ip_from_client_ip_without_appropriate_platform() {
652 let mut event = Annotated::new(Event::default());
653
654 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
655 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
656 normalize_event(
657 &mut event,
658 &NormalizationConfig {
659 client_ip: Some(&ip_address),
660 geoip_lookup: Some(&geo),
661 ..Default::default()
662 },
663 );
664
665 let user = get_value!(event.user!);
666 assert!(user.ip_address.value().is_none());
667 assert!(user.geo.value().is_some());
668 }
669
670 #[test]
671 fn test_geo_present_if_ip_inferring_disabled() {
672 let mut event = Annotated::new(Event {
673 user: Annotated::new(User {
674 ip_address: Annotated::new(IpAddr::auto()),
675 ..Default::default()
676 }),
677 ..Default::default()
678 });
679
680 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
681 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
682
683 normalize_event(
684 &mut event,
685 &NormalizationConfig {
686 client_ip: Some(&ip_address),
687 geoip_lookup: Some(&geo),
688 infer_ip_address: false,
689 ..Default::default()
690 },
691 );
692
693 let user = get_value!(event.user!);
694 assert!(user.ip_address.value().unwrap().is_auto());
695 assert!(user.geo.value().is_some());
696 }
697
698 #[test]
699 fn test_geo_and_ip_present_if_ip_inferring_enabled() {
700 let mut event = Annotated::new(Event {
701 user: Annotated::new(User {
702 ip_address: Annotated::new(IpAddr::auto()),
703 ..Default::default()
704 }),
705 ..Default::default()
706 });
707
708 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
709 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
710
711 normalize_event(
712 &mut event,
713 &NormalizationConfig {
714 client_ip: Some(&ip_address),
715 geoip_lookup: Some(&geo),
716 infer_ip_address: true,
717 ..Default::default()
718 },
719 );
720
721 let user = get_value!(event.user!);
722 assert_eq!(
723 user.ip_address.value(),
724 Some(&IpAddr::parse("2.125.160.216").unwrap())
725 );
726 assert!(user.geo.value().is_some());
727 }
728
729 #[test]
730 fn test_event_level_defaulted() {
731 let mut event = Annotated::new(Event::default());
732 normalize_event(&mut event, &NormalizationConfig::default());
733 assert_eq!(get_value!(event.level), Some(&Level::Error));
734 }
735
736 #[test]
737 fn test_transaction_level_untouched() {
738 let mut event = Annotated::new(Event {
739 ty: Annotated::new(EventType::Transaction),
740 timestamp: Annotated::new(Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into()),
741 start_timestamp: Annotated::new(
742 Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into(),
743 ),
744 contexts: {
745 let mut contexts = Contexts::new();
746 contexts.add(TraceContext {
747 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
748 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
749 op: Annotated::new("http.server".to_owned()),
750 ..Default::default()
751 });
752 Annotated::new(contexts)
753 },
754 ..Event::default()
755 });
756 normalize_event(&mut event, &NormalizationConfig::default());
757 assert_eq!(get_value!(event.level), Some(&Level::Info));
758 }
759
760 #[test]
761 fn test_environment_tag_is_moved() {
762 let mut event = Annotated::new(Event {
763 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
764 Annotated::new("environment".to_owned()),
765 Annotated::new("despacito".to_owned()),
766 ))]))),
767 ..Event::default()
768 });
769
770 normalize_event(&mut event, &NormalizationConfig::default());
771
772 let event = event.value().unwrap();
773
774 assert_eq!(event.environment.as_str(), Some("despacito"));
775 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
776 }
777
778 #[test]
779 fn test_empty_environment_is_removed_and_overwritten_with_tag() {
780 let mut event = Annotated::new(Event {
781 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
782 Annotated::new("environment".to_owned()),
783 Annotated::new("despacito".to_owned()),
784 ))]))),
785 environment: Annotated::new("".to_owned()),
786 ..Event::default()
787 });
788
789 normalize_event(&mut event, &NormalizationConfig::default());
790
791 let event = event.value().unwrap();
792
793 assert_eq!(event.environment.as_str(), Some("despacito"));
794 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
795 }
796
797 #[test]
798 fn test_empty_environment_is_removed() {
799 let mut event = Annotated::new(Event {
800 environment: Annotated::new("".to_owned()),
801 ..Event::default()
802 });
803
804 normalize_event(&mut event, &NormalizationConfig::default());
805 assert_eq!(get_value!(event.environment), None);
806 }
807 #[test]
808 fn test_replay_id_added_from_dsc() {
809 let replay_id = Uuid::new_v4();
810 let mut event = Annotated::new(Event {
811 contexts: Annotated::new(Contexts(Object::new())),
812 ..Event::default()
813 });
814 normalize_event(
815 &mut event,
816 &NormalizationConfig {
817 replay_id: Some(replay_id),
818 ..Default::default()
819 },
820 );
821
822 let event = event.value().unwrap();
823
824 assert_eq!(event.contexts, {
825 let mut contexts = Contexts::new();
826 contexts.add(ReplayContext {
827 replay_id: Annotated::new(EventId(replay_id)),
828 other: Object::default(),
829 });
830 Annotated::new(contexts)
831 })
832 }
833
834 #[test]
835 fn test_none_environment_errors() {
836 let mut event = Annotated::new(Event {
837 environment: Annotated::new("none".to_owned()),
838 ..Event::default()
839 });
840
841 normalize_event(&mut event, &NormalizationConfig::default());
842
843 let environment = get_path!(event.environment!);
844 let expected_original = &Value::String("none".to_owned());
845
846 assert_eq!(
847 environment.meta().iter_errors().collect::<Vec<&Error>>(),
848 vec![&Error::new(ErrorKind::InvalidData)],
849 );
850 assert_eq!(
851 environment.meta().original_value().unwrap(),
852 expected_original
853 );
854 assert_eq!(environment.value(), None);
855 }
856
857 #[test]
858 fn test_invalid_release_removed() {
859 let mut event = Annotated::new(Event {
860 release: Annotated::new(LenientString("Latest".to_owned())),
861 ..Event::default()
862 });
863
864 normalize_event(&mut event, &NormalizationConfig::default());
865
866 let release = get_path!(event.release!);
867 let expected_original = &Value::String("Latest".to_owned());
868
869 assert_eq!(
870 release.meta().iter_errors().collect::<Vec<&Error>>(),
871 vec![&Error::new(ErrorKind::InvalidData)],
872 );
873 assert_eq!(release.meta().original_value().unwrap(), expected_original);
874 assert_eq!(release.value(), None);
875 }
876
877 #[test]
878 fn test_top_level_keys_moved_into_tags() {
879 let mut event = Annotated::new(Event {
880 server_name: Annotated::new("foo".to_owned()),
881 site: Annotated::new("foo".to_owned()),
882 tags: Annotated::new(Tags(PairList(vec![
883 Annotated::new(TagEntry(
884 Annotated::new("site".to_owned()),
885 Annotated::new("old".to_owned()),
886 )),
887 Annotated::new(TagEntry(
888 Annotated::new("server_name".to_owned()),
889 Annotated::new("old".to_owned()),
890 )),
891 ]))),
892 ..Event::default()
893 });
894
895 normalize_event(&mut event, &NormalizationConfig::default());
896
897 assert_eq!(get_value!(event.site), None);
898 assert_eq!(get_value!(event.server_name), None);
899
900 assert_eq!(
901 get_value!(event.tags!),
902 &Tags(PairList(vec![
903 Annotated::new(TagEntry(
904 Annotated::new("site".to_owned()),
905 Annotated::new("foo".to_owned()),
906 )),
907 Annotated::new(TagEntry(
908 Annotated::new("server_name".to_owned()),
909 Annotated::new("foo".to_owned()),
910 )),
911 ]))
912 );
913 }
914
915 #[test]
916 fn test_internal_tags_removed() {
917 let mut event = Annotated::new(Event {
918 tags: Annotated::new(Tags(PairList(vec![
919 Annotated::new(TagEntry(
920 Annotated::new("release".to_owned()),
921 Annotated::new("foo".to_owned()),
922 )),
923 Annotated::new(TagEntry(
924 Annotated::new("dist".to_owned()),
925 Annotated::new("foo".to_owned()),
926 )),
927 Annotated::new(TagEntry(
928 Annotated::new("user".to_owned()),
929 Annotated::new("foo".to_owned()),
930 )),
931 Annotated::new(TagEntry(
932 Annotated::new("filename".to_owned()),
933 Annotated::new("foo".to_owned()),
934 )),
935 Annotated::new(TagEntry(
936 Annotated::new("function".to_owned()),
937 Annotated::new("foo".to_owned()),
938 )),
939 Annotated::new(TagEntry(
940 Annotated::new("something".to_owned()),
941 Annotated::new("else".to_owned()),
942 )),
943 ]))),
944 ..Event::default()
945 });
946
947 normalize_event(&mut event, &NormalizationConfig::default());
948
949 assert_eq!(get_value!(event.tags!).len(), 1);
950 }
951
952 #[test]
953 fn test_empty_tags_removed() {
954 let mut event = Annotated::new(Event {
955 tags: Annotated::new(Tags(PairList(vec![
956 Annotated::new(TagEntry(
957 Annotated::new("".to_owned()),
958 Annotated::new("foo".to_owned()),
959 )),
960 Annotated::new(TagEntry(
961 Annotated::new("foo".to_owned()),
962 Annotated::new("".to_owned()),
963 )),
964 Annotated::new(TagEntry(
965 Annotated::new("something".to_owned()),
966 Annotated::new("else".to_owned()),
967 )),
968 ]))),
969 ..Event::default()
970 });
971
972 normalize_event(&mut event, &NormalizationConfig::default());
973
974 assert_eq!(
975 get_value!(event.tags!),
976 &Tags(PairList(vec![
977 Annotated::new(TagEntry(
978 Annotated::from_error(Error::nonempty(), None),
979 Annotated::new("foo".to_owned()),
980 )),
981 Annotated::new(TagEntry(
982 Annotated::new("foo".to_owned()),
983 Annotated::from_error(Error::nonempty(), None),
984 )),
985 Annotated::new(TagEntry(
986 Annotated::new("something".to_owned()),
987 Annotated::new("else".to_owned()),
988 )),
989 ]))
990 );
991 }
992
993 #[test]
994 fn test_tags_deduplicated() {
995 let mut event = Annotated::new(Event {
996 tags: Annotated::new(Tags(PairList(vec![
997 Annotated::new(TagEntry(
998 Annotated::new("foo".to_owned()),
999 Annotated::new("1".to_owned()),
1000 )),
1001 Annotated::new(TagEntry(
1002 Annotated::new("bar".to_owned()),
1003 Annotated::new("1".to_owned()),
1004 )),
1005 Annotated::new(TagEntry(
1006 Annotated::new("foo".to_owned()),
1007 Annotated::new("2".to_owned()),
1008 )),
1009 Annotated::new(TagEntry(
1010 Annotated::new("bar".to_owned()),
1011 Annotated::new("2".to_owned()),
1012 )),
1013 Annotated::new(TagEntry(
1014 Annotated::new("foo".to_owned()),
1015 Annotated::new("3".to_owned()),
1016 )),
1017 ]))),
1018 ..Event::default()
1019 });
1020
1021 normalize_event(&mut event, &NormalizationConfig::default());
1022
1023 assert_eq!(
1025 get_value!(event.tags!),
1026 &Tags(PairList(vec![
1027 Annotated::new(TagEntry(
1028 Annotated::new("foo".to_owned()),
1029 Annotated::new("1".to_owned()),
1030 )),
1031 Annotated::new(TagEntry(
1032 Annotated::new("bar".to_owned()),
1033 Annotated::new("1".to_owned()),
1034 )),
1035 ]))
1036 );
1037 }
1038
1039 #[test]
1040 fn test_transaction_status_defaulted_to_unknown() {
1041 let mut object = Object::new();
1042 let trace_context = TraceContext {
1043 status: Annotated::empty(),
1045 ..TraceContext::default()
1046 };
1047 object.insert(
1048 "trace".to_owned(),
1049 Annotated::new(ContextInner(Context::Trace(Box::new(trace_context)))),
1050 );
1051
1052 let mut event = Annotated::new(Event {
1053 contexts: Annotated::new(Contexts(object)),
1054 ..Event::default()
1055 });
1056 normalize_event(&mut event, &NormalizationConfig::default());
1057
1058 let event = event.value().unwrap();
1059
1060 let event_trace_context = event.context::<TraceContext>().unwrap();
1061 assert_eq!(
1062 event_trace_context.status,
1063 Annotated::new(SpanStatus::Unknown)
1064 )
1065 }
1066
1067 #[test]
1068 fn test_unknown_debug_image() {
1069 let mut event = Annotated::new(Event {
1070 debug_meta: Annotated::new(DebugMeta {
1071 images: Annotated::new(vec![Annotated::new(DebugImage::Other(Object::default()))]),
1072 ..DebugMeta::default()
1073 }),
1074 ..Event::default()
1075 });
1076
1077 normalize_event(&mut event, &NormalizationConfig::default());
1078
1079 assert_eq!(
1080 get_path!(event.debug_meta!),
1081 &Annotated::new(DebugMeta {
1082 images: Annotated::new(vec![Annotated::from_error(
1083 Error::invalid("unsupported debug image type"),
1084 Some(Value::Object(Object::default())),
1085 )]),
1086 ..DebugMeta::default()
1087 })
1088 );
1089 }
1090
1091 #[test]
1092 fn test_context_line_default() {
1093 let mut frame = Annotated::new(Frame {
1094 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1095 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1096 ..Frame::default()
1097 });
1098
1099 normalize_non_raw_frame(&mut frame);
1100
1101 let frame = frame.value().unwrap();
1102 assert_eq!(frame.context_line.as_str(), Some(""));
1103 }
1104
1105 #[test]
1106 fn test_context_line_retain() {
1107 let mut frame = Annotated::new(Frame {
1108 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1109 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1110 context_line: Annotated::new("some line".to_owned()),
1111 ..Frame::default()
1112 });
1113
1114 normalize_non_raw_frame(&mut frame);
1115
1116 let frame = frame.value().unwrap();
1117 assert_eq!(frame.context_line.as_str(), Some("some line"));
1118 }
1119
1120 #[test]
1121 fn test_frame_null_context_lines() {
1122 let mut frame = Annotated::new(Frame {
1123 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1124 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1125 ..Frame::default()
1126 });
1127
1128 normalize_non_raw_frame(&mut frame);
1129
1130 assert_eq!(
1131 *get_value!(frame.pre_context!),
1132 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1133 );
1134 assert_eq!(
1135 *get_value!(frame.post_context!),
1136 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1137 );
1138 }
1139
1140 #[test]
1141 fn test_too_long_distribution() {
1142 let json = r#"{
1143 "event_id": "52df9022835246eeb317dbd739ccd059",
1144 "fingerprint": [
1145 "{{ default }}"
1146 ],
1147 "platform": "other",
1148 "dist": "52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059"
1149}"#;
1150
1151 let mut event = Annotated::<Event>::from_json(json).unwrap();
1152
1153 normalize_event(&mut event, &NormalizationConfig::default());
1154
1155 let dist = &event.value().unwrap().dist;
1156 let result = &Annotated::<String>::from_error(
1157 Error::new(ErrorKind::ValueTooLong),
1158 Some(Value::String("52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059".to_owned()))
1159 );
1160 assert_eq!(dist, result);
1161 }
1162
1163 #[test]
1164 fn test_regression_backfills_abs_path_even_when_moving_stacktrace() {
1165 let mut event = Annotated::new(Event {
1166 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
1167 ty: Annotated::new("FooDivisionError".to_owned()),
1168 value: Annotated::new("hi".to_owned().into()),
1169 ..Exception::default()
1170 })])),
1171 stacktrace: Annotated::new(
1172 RawStacktrace {
1173 frames: Annotated::new(vec![Annotated::new(Frame {
1174 module: Annotated::new("MyModule".to_owned()),
1175 filename: Annotated::new("MyFilename".into()),
1176 function: Annotated::new("Void FooBar()".to_owned()),
1177 ..Frame::default()
1178 })]),
1179 ..RawStacktrace::default()
1180 }
1181 .into(),
1182 ),
1183 ..Event::default()
1184 });
1185
1186 normalize_event(&mut event, &NormalizationConfig::default());
1187
1188 assert_eq!(
1189 get_value!(event.exceptions.values[0].stacktrace!),
1190 &Stacktrace(RawStacktrace {
1191 frames: Annotated::new(vec![Annotated::new(Frame {
1192 module: Annotated::new("MyModule".to_owned()),
1193 filename: Annotated::new("MyFilename".into()),
1194 abs_path: Annotated::new("MyFilename".into()),
1195 function: Annotated::new("Void FooBar()".to_owned()),
1196 ..Frame::default()
1197 })]),
1198 ..RawStacktrace::default()
1199 })
1200 );
1201 }
1202
1203 #[test]
1204 fn test_parses_sdk_info_from_header() {
1205 let mut event = Annotated::new(Event::default());
1206
1207 normalize_event(
1208 &mut event,
1209 &NormalizationConfig {
1210 client: Some("_fooBar/0.0.0".to_owned()),
1211 ..Default::default()
1212 },
1213 );
1214
1215 assert_eq!(
1216 get_path!(event.client_sdk!),
1217 &Annotated::new(ClientSdkInfo {
1218 name: Annotated::new("_fooBar".to_owned()),
1219 version: Annotated::new("0.0.0".to_owned()),
1220 ..ClientSdkInfo::default()
1221 })
1222 );
1223 }
1224
1225 #[test]
1226 fn test_discards_received() {
1227 let mut event = Annotated::new(Event {
1228 received: FromValue::from_value(Annotated::new(Value::U64(696_969_696_969))),
1229 ..Default::default()
1230 });
1231
1232 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1233 normalize_event(&mut event, &NormalizationConfig::default());
1234
1235 assert_eq!(get_value!(event.received!), get_value!(event.timestamp!));
1236 }
1237
1238 #[test]
1239 fn test_grouping_config() {
1240 let mut event = Annotated::new(Event {
1241 logentry: Annotated::from(LogEntry {
1242 message: Annotated::new("Hello World!".to_owned().into()),
1243 ..Default::default()
1244 }),
1245 ..Default::default()
1246 });
1247
1248 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1249 normalize_event(
1250 &mut event,
1251 &NormalizationConfig {
1252 grouping_config: Some(json!({
1253 "id": "legacy:1234-12-12".to_owned(),
1254 })),
1255 ..Default::default()
1256 },
1257 );
1258
1259 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1260 ".event_id" => "[event-id]",
1261 ".received" => "[received]",
1262 ".timestamp" => "[timestamp]"
1263 }, @r###"
1264 {
1265 "event_id": "[event-id]",
1266 "level": "error",
1267 "type": "default",
1268 "logentry": {
1269 "formatted": "Hello World!",
1270 },
1271 "logger": "",
1272 "platform": "other",
1273 "timestamp": "[timestamp]",
1274 "received": "[received]",
1275 "grouping_config": {
1276 "id": "legacy:1234-12-12",
1277 },
1278 }
1279 "###);
1280 }
1281
1282 #[test]
1283 fn test_logentry_error() {
1284 let json = r#"
1285{
1286 "event_id": "74ad1301f4df489ead37d757295442b1",
1287 "timestamp": 1668148328.308933,
1288 "received": 1668148328.308933,
1289 "level": "error",
1290 "platform": "python",
1291 "logentry": {
1292 "params": [
1293 "bogus"
1294 ],
1295 "formatted": 42
1296 }
1297}
1298"#;
1299 let mut event = Annotated::<Event>::from_json(json).unwrap();
1300
1301 normalize_event(&mut event, &NormalizationConfig::default());
1302
1303 assert_json_snapshot!(SerializableAnnotated(&event), {".received" => "[received]"}, @r###"
1304 {
1305 "event_id": "74ad1301f4df489ead37d757295442b1",
1306 "level": "error",
1307 "type": "default",
1308 "logentry": null,
1309 "logger": "",
1310 "platform": "python",
1311 "timestamp": 1668148328.308933,
1312 "received": "[received]",
1313 "_meta": {
1314 "logentry": {
1315 "": {
1316 "err": [
1317 [
1318 "invalid_data",
1319 {
1320 "reason": "no message present"
1321 }
1322 ]
1323 ],
1324 "val": {
1325 "formatted": null,
1326 "message": null,
1327 "params": [
1328 "bogus"
1329 ]
1330 }
1331 }
1332 }
1333 }
1334 }
1335 "###)
1336 }
1337
1338 #[test]
1339 fn test_future_timestamp() {
1340 let mut event = Annotated::new(Event {
1341 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 2, 0).unwrap().into()),
1342 ..Default::default()
1343 });
1344
1345 let received_at = Some(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap());
1346 let max_secs_in_past = Some(30 * 24 * 3600);
1347 let max_secs_in_future = Some(60);
1348
1349 validate_event(
1350 &mut event,
1351 &EventValidationConfig {
1352 received_at,
1353 max_secs_in_past,
1354 max_secs_in_future,
1355 is_validated: false,
1356 ..Default::default()
1357 },
1358 )
1359 .unwrap();
1360 normalize_event(&mut event, &NormalizationConfig::default());
1361
1362 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1363 ".event_id" => "[event-id]",
1364 }, @r###"
1365 {
1366 "event_id": "[event-id]",
1367 "level": "error",
1368 "type": "default",
1369 "logger": "",
1370 "platform": "other",
1371 "timestamp": 946857600.0,
1372 "received": 946857600.0,
1373 "_meta": {
1374 "timestamp": {
1375 "": Meta(Some(MetaInner(
1376 err: [
1377 [
1378 "future_timestamp",
1379 {
1380 "sdk_time": "2000-01-03T00:02:00+00:00",
1381 "server_time": "2000-01-03T00:00:00+00:00",
1382 },
1383 ],
1384 ],
1385 ))),
1386 },
1387 },
1388 }
1389 "###);
1390 }
1391
1392 #[test]
1393 fn test_past_timestamp() {
1394 let mut event = Annotated::new(Event {
1395 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap().into()),
1396 ..Default::default()
1397 });
1398
1399 let received_at = Some(Utc.with_ymd_and_hms(2000, 3, 3, 0, 0, 0).unwrap());
1400 let max_secs_in_past = Some(30 * 24 * 3600);
1401 let max_secs_in_future = Some(60);
1402
1403 validate_event(
1404 &mut event,
1405 &EventValidationConfig {
1406 received_at,
1407 max_secs_in_past,
1408 max_secs_in_future,
1409 is_validated: false,
1410 ..Default::default()
1411 },
1412 )
1413 .unwrap();
1414 normalize_event(&mut event, &NormalizationConfig::default());
1415
1416 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1417 ".event_id" => "[event-id]",
1418 }, @r###"
1419 {
1420 "event_id": "[event-id]",
1421 "level": "error",
1422 "type": "default",
1423 "logger": "",
1424 "platform": "other",
1425 "timestamp": 952041600.0,
1426 "received": 952041600.0,
1427 "_meta": {
1428 "timestamp": {
1429 "": Meta(Some(MetaInner(
1430 err: [
1431 [
1432 "past_timestamp",
1433 {
1434 "sdk_time": "2000-01-03T00:00:00+00:00",
1435 "server_time": "2000-03-03T00:00:00+00:00",
1436 },
1437 ],
1438 ],
1439 ))),
1440 },
1441 },
1442 }
1443 "###);
1444 }
1445
1446 #[test]
1447 fn test_normalize_logger_empty() {
1448 let mut event = Event::from_value(
1449 serde_json::json!({
1450 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1451 "platform": "java",
1452 "logger": "",
1453 })
1454 .into(),
1455 );
1456
1457 normalize_event(&mut event, &NormalizationConfig::default());
1458 assert_annotated_snapshot!(event);
1459 }
1460
1461 #[test]
1462 fn test_normalize_logger_trimmed() {
1463 let mut event = Event::from_value(
1464 serde_json::json!({
1465 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1466 "platform": "java",
1467 "logger": " \t \t ",
1468 })
1469 .into(),
1470 );
1471
1472 normalize_event(&mut event, &NormalizationConfig::default());
1473 assert_annotated_snapshot!(event);
1474 }
1475
1476 #[test]
1477 fn test_normalize_logger_short_no_trimming() {
1478 let mut event = Event::from_value(
1479 serde_json::json!({
1480 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1481 "platform": "java",
1482 "logger": "my.short-logger.isnt_trimmed",
1483 })
1484 .into(),
1485 );
1486
1487 normalize_event(&mut event, &NormalizationConfig::default());
1488 assert_annotated_snapshot!(event);
1489 }
1490
1491 #[test]
1492 fn test_normalize_logger_exact_length() {
1493 let mut event = Event::from_value(
1494 serde_json::json!({
1495 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1496 "platform": "java",
1497 "logger": "this_is-exactly-the_max_len.012345678901234567890123456789012345",
1498 })
1499 .into(),
1500 );
1501
1502 normalize_event(&mut event, &NormalizationConfig::default());
1503 assert_annotated_snapshot!(event);
1504 }
1505
1506 #[test]
1507 fn test_normalize_logger_too_long_single_word() {
1508 let mut event = Event::from_value(
1509 serde_json::json!({
1510 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1511 "platform": "java",
1512 "logger": "this_is-way_too_long-and_we_only_have_one_word-so_we_cant_smart_trim",
1513 })
1514 .into(),
1515 );
1516
1517 normalize_event(&mut event, &NormalizationConfig::default());
1518 assert_annotated_snapshot!(event);
1519 }
1520
1521 #[test]
1522 fn test_normalize_logger_word_trimmed_at_max() {
1523 let mut event = Event::from_value(
1524 serde_json::json!({
1525 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1526 "platform": "java",
1527 "logger": "already_out.out.in.this_part-is-kept.this_right_here-is_an-extremely_long_word",
1528 })
1529 .into(),
1530 );
1531
1532 normalize_event(&mut event, &NormalizationConfig::default());
1533 assert_annotated_snapshot!(event);
1534 }
1535
1536 #[test]
1537 fn test_normalize_logger_word_trimmed_before_max() {
1538 let mut event = Event::from_value(
1541 serde_json::json!({
1542 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1543 "platform": "java",
1544 "logger": "super_out.this_is_already_out_too.this_part-is-kept.this_right_here-is_a-very_long_word",
1545 })
1546 .into(),
1547 );
1548
1549 normalize_event(&mut event, &NormalizationConfig::default());
1550 assert_annotated_snapshot!(event);
1551 }
1552
1553 #[test]
1554 fn test_normalize_logger_word_leading_dots() {
1555 let mut event = Event::from_value(
1556 serde_json::json!({
1557 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1558 "platform": "java",
1559 "logger": "io.this-tests-the-smart-trimming-and-word-removal-around-dot.words",
1560 })
1561 .into(),
1562 );
1563
1564 normalize_event(&mut event, &NormalizationConfig::default());
1565 assert_annotated_snapshot!(event);
1566 }
1567
1568 #[test]
1569 fn test_normalization_is_idempotent() {
1570 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1572 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1573 let mut event = Annotated::new(Event {
1574 ty: Annotated::new(EventType::Transaction),
1575 transaction: Annotated::new("/".to_owned()),
1576 timestamp: Annotated::new(end.into()),
1577 start_timestamp: Annotated::new(start.into()),
1578 contexts: {
1579 let mut contexts = Contexts::new();
1580 contexts.add(TraceContext {
1581 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1582 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1583 op: Annotated::new("http.server".to_owned()),
1584 ..Default::default()
1585 });
1586 Annotated::new(contexts)
1587 },
1588 spans: Annotated::new(vec![Annotated::new(Span {
1589 timestamp: Annotated::new(
1590 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1591 ),
1592 start_timestamp: Annotated::new(
1593 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1594 ),
1595 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1596 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1597
1598 ..Default::default()
1599 })]),
1600 ..Default::default()
1601 });
1602
1603 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1604 relay_event_schema::processor::apply(event, |e, _m| {
1605 e.received = Annotated::empty();
1606 Ok(())
1607 })
1608 .unwrap();
1609 event
1610 }
1611
1612 normalize_event(&mut event, &NormalizationConfig::default());
1613 let first = remove_received_from_event(&mut event.clone())
1614 .to_json()
1615 .unwrap();
1616 normalize_event(&mut event, &NormalizationConfig::default());
1619 let second = remove_received_from_event(&mut event.clone())
1620 .to_json()
1621 .unwrap();
1622 assert_eq!(&first, &second, "idempotency check failed");
1623
1624 normalize_event(&mut event, &NormalizationConfig::default());
1625 let third = remove_received_from_event(&mut event.clone())
1626 .to_json()
1627 .unwrap();
1628 assert_eq!(&second, &third, "idempotency check failed");
1629 }
1630
1631 #[test]
1638 fn test_full_normalization_is_idempotent() {
1639 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1640 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1641 let mut event = Annotated::new(Event {
1642 ty: Annotated::new(EventType::Transaction),
1643 transaction: Annotated::new("/".to_owned()),
1644 timestamp: Annotated::new(end.into()),
1645 start_timestamp: Annotated::new(start.into()),
1646 contexts: {
1647 let mut contexts = Contexts::new();
1648 contexts.add(TraceContext {
1649 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1650 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1651 op: Annotated::new("http.server".to_owned()),
1652 ..Default::default()
1653 });
1654 Annotated::new(contexts)
1655 },
1656 spans: Annotated::new(vec![Annotated::new(Span {
1657 timestamp: Annotated::new(
1658 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1659 ),
1660 start_timestamp: Annotated::new(
1661 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1662 ),
1663 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1664 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1665
1666 ..Default::default()
1667 })]),
1668 ..Default::default()
1669 });
1670
1671 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1672 relay_event_schema::processor::apply(event, |e, _m| {
1673 e.received = Annotated::empty();
1674 Ok(())
1675 })
1676 .unwrap();
1677 event
1678 }
1679
1680 let full_normalization_config = NormalizationConfig {
1681 is_renormalize: false,
1682 enable_trimming: true,
1683 remove_other: true,
1684 emit_event_errors: true,
1685 ..Default::default()
1686 };
1687
1688 normalize_event(&mut event, &full_normalization_config);
1689 let first = remove_received_from_event(&mut event.clone())
1690 .to_json()
1691 .unwrap();
1692 normalize_event(&mut event, &full_normalization_config);
1695 let second = remove_received_from_event(&mut event.clone())
1696 .to_json()
1697 .unwrap();
1698 assert_eq!(&first, &second, "idempotency check failed");
1699
1700 normalize_event(&mut event, &full_normalization_config);
1701 let third = remove_received_from_event(&mut event.clone())
1702 .to_json()
1703 .unwrap();
1704 assert_eq!(&second, &third, "idempotency check failed");
1705 }
1706
1707 #[test]
1708 fn test_normalize_validates_spans() {
1709 let event = Annotated::<Event>::from_json(
1710 r#"
1711 {
1712 "type": "transaction",
1713 "transaction": "/",
1714 "timestamp": 946684810.0,
1715 "start_timestamp": 946684800.0,
1716 "contexts": {
1717 "trace": {
1718 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1719 "span_id": "fa90fdead5f74053",
1720 "op": "http.server",
1721 "type": "trace"
1722 }
1723 },
1724 "spans": []
1725 }
1726 "#,
1727 )
1728 .unwrap();
1729
1730 for span in [
1732 r#"null"#,
1733 r#"{
1734 "timestamp": 946684810.0,
1735 "start_timestamp": 946684900.0,
1736 "span_id": "fa90fdead5f74053",
1737 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1738 }"#,
1739 r#"{
1740 "timestamp": 946684810.0,
1741 "span_id": "fa90fdead5f74053",
1742 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1743 }"#,
1744 r#"{
1745 "timestamp": 946684810.0,
1746 "start_timestamp": 946684800.0,
1747 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1748 }"#,
1749 r#"{
1750 "timestamp": 946684810.0,
1751 "start_timestamp": 946684800.0,
1752 "span_id": "fa90fdead5f74053"
1753 }"#,
1754 ] {
1755 let mut modified_event = event.clone();
1756 let event_ref = modified_event.value_mut().as_mut().unwrap();
1757 event_ref
1758 .spans
1759 .set_value(Some(vec![Annotated::<Span>::from_json(span).unwrap()]));
1760
1761 let res = validate_event(&mut modified_event, &EventValidationConfig::default());
1762
1763 assert!(res.is_err(), "{span:?}");
1764 }
1765 }
1766
1767 #[test]
1768 fn test_normalization_respects_is_renormalize() {
1769 let mut event = Annotated::<Event>::from_json(
1770 r#"
1771 {
1772 "type": "default",
1773 "tags": [["environment", "some_environment"]]
1774 }
1775 "#,
1776 )
1777 .unwrap();
1778
1779 normalize_event(
1780 &mut event,
1781 &NormalizationConfig {
1782 is_renormalize: true,
1783 ..Default::default()
1784 },
1785 );
1786
1787 assert_debug_snapshot!(event.value().unwrap().tags, @r###"
1788 Tags(
1789 PairList(
1790 [
1791 TagEntry(
1792 "environment",
1793 "some_environment",
1794 ),
1795 ],
1796 ),
1797 )
1798 "###);
1799 }
1800
1801 #[test]
1802 fn test_geo_in_normalize() {
1803 let mut event = Annotated::<Event>::from_json(
1804 r#"
1805 {
1806 "type": "transaction",
1807 "transaction": "/foo/",
1808 "timestamp": 946684810.0,
1809 "start_timestamp": 946684800.0,
1810 "contexts": {
1811 "trace": {
1812 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1813 "span_id": "fa90fdead5f74053",
1814 "op": "http.server",
1815 "type": "trace"
1816 }
1817 },
1818 "transaction_info": {
1819 "source": "url"
1820 },
1821 "user": {
1822 "ip_address": "2.125.160.216"
1823 }
1824 }
1825 "#,
1826 )
1827 .unwrap();
1828
1829 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
1830
1831 let user_geo = event.value().unwrap().user.value().unwrap().geo.value();
1833 assert!(user_geo.is_none());
1834
1835 normalize_event(
1836 &mut event,
1837 &NormalizationConfig {
1838 geoip_lookup: Some(&lookup),
1839 ..Default::default()
1840 },
1841 );
1842
1843 let user_geo = event
1845 .value()
1846 .unwrap()
1847 .user
1848 .value()
1849 .unwrap()
1850 .geo
1851 .value()
1852 .unwrap();
1853
1854 assert_eq!(user_geo.country_code.value().unwrap(), "GB");
1855 assert_eq!(user_geo.city.value().unwrap(), "Boxford");
1856 }
1857
1858 #[test]
1859 fn test_normalize_app_start_spans_only_for_react_native_3_to_4_4() {
1860 let mut event = Event {
1861 spans: Annotated::new(vec![Annotated::new(Span {
1862 op: Annotated::new("app_start_cold".to_owned()),
1863 ..Default::default()
1864 })]),
1865 client_sdk: Annotated::new(ClientSdkInfo {
1866 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1867 version: Annotated::new("4.5.0".to_owned()),
1868 ..Default::default()
1869 }),
1870 ..Default::default()
1871 };
1872 normalize_app_start_spans(&mut event);
1873 assert_debug_snapshot!(event.spans, @r###"
1874 [
1875 Span {
1876 timestamp: ~,
1877 start_timestamp: ~,
1878 exclusive_time: ~,
1879 op: "app_start_cold",
1880 span_id: ~,
1881 parent_span_id: ~,
1882 trace_id: ~,
1883 segment_id: ~,
1884 is_segment: ~,
1885 is_remote: ~,
1886 status: ~,
1887 description: ~,
1888 tags: ~,
1889 origin: ~,
1890 profile_id: ~,
1891 data: ~,
1892 links: ~,
1893 sentry_tags: ~,
1894 received: ~,
1895 measurements: ~,
1896 platform: ~,
1897 was_transaction: ~,
1898 kind: ~,
1899 performance_issues_spans: ~,
1900 other: {},
1901 },
1902 ]
1903 "###);
1904 }
1905
1906 #[test]
1907 fn test_normalize_app_start_cold_spans_for_react_native() {
1908 let mut event = Event {
1909 spans: Annotated::new(vec![Annotated::new(Span {
1910 op: Annotated::new("app_start_cold".to_owned()),
1911 ..Default::default()
1912 })]),
1913 client_sdk: Annotated::new(ClientSdkInfo {
1914 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1915 version: Annotated::new("4.4.0".to_owned()),
1916 ..Default::default()
1917 }),
1918 ..Default::default()
1919 };
1920 normalize_app_start_spans(&mut event);
1921 assert_debug_snapshot!(event.spans, @r###"
1922 [
1923 Span {
1924 timestamp: ~,
1925 start_timestamp: ~,
1926 exclusive_time: ~,
1927 op: "app.start.cold",
1928 span_id: ~,
1929 parent_span_id: ~,
1930 trace_id: ~,
1931 segment_id: ~,
1932 is_segment: ~,
1933 is_remote: ~,
1934 status: ~,
1935 description: ~,
1936 tags: ~,
1937 origin: ~,
1938 profile_id: ~,
1939 data: ~,
1940 links: ~,
1941 sentry_tags: ~,
1942 received: ~,
1943 measurements: ~,
1944 platform: ~,
1945 was_transaction: ~,
1946 kind: ~,
1947 performance_issues_spans: ~,
1948 other: {},
1949 },
1950 ]
1951 "###);
1952 }
1953
1954 #[test]
1955 fn test_normalize_app_start_warm_spans_for_react_native() {
1956 let mut event = Event {
1957 spans: Annotated::new(vec![Annotated::new(Span {
1958 op: Annotated::new("app_start_warm".to_owned()),
1959 ..Default::default()
1960 })]),
1961 client_sdk: Annotated::new(ClientSdkInfo {
1962 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1963 version: Annotated::new("4.4.0".to_owned()),
1964 ..Default::default()
1965 }),
1966 ..Default::default()
1967 };
1968 normalize_app_start_spans(&mut event);
1969 assert_debug_snapshot!(event.spans, @r###"
1970 [
1971 Span {
1972 timestamp: ~,
1973 start_timestamp: ~,
1974 exclusive_time: ~,
1975 op: "app.start.warm",
1976 span_id: ~,
1977 parent_span_id: ~,
1978 trace_id: ~,
1979 segment_id: ~,
1980 is_segment: ~,
1981 is_remote: ~,
1982 status: ~,
1983 description: ~,
1984 tags: ~,
1985 origin: ~,
1986 profile_id: ~,
1987 data: ~,
1988 links: ~,
1989 sentry_tags: ~,
1990 received: ~,
1991 measurements: ~,
1992 platform: ~,
1993 was_transaction: ~,
1994 kind: ~,
1995 performance_issues_spans: ~,
1996 other: {},
1997 },
1998 ]
1999 "###);
2000 }
2001}