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