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