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