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