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