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_tags() {
1288 let mut event = Annotated::new(Event {
1289 tags: Annotated::new(Tags(PairList(
1290 vec![Annotated::new(TagEntry(
1291 Annotated::new("foobar".to_owned()),
1292 Annotated::new("...........................................................................................................................................................................................................".to_owned()),
1293 )), Annotated::new(TagEntry(
1294 Annotated::new("foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo".to_owned()),
1295 Annotated::new("bar".to_owned()),
1296 ))]),
1297 )),
1298 ..Event::default()
1299 });
1300
1301 normalize_event(&mut event, &NormalizationConfig::default());
1302
1303 assert_eq!(
1304 get_value!(event.tags!),
1305 &Tags(PairList(vec![
1306 Annotated::new(TagEntry(
1307 Annotated::new("foobar".to_owned()),
1308 Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None),
1309 )),
1310 Annotated::new(TagEntry(
1311 Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None),
1312 Annotated::new("bar".to_owned()),
1313 )),
1314 ]))
1315 );
1316 }
1317
1318 #[test]
1319 fn test_too_long_distribution() {
1320 let json = r#"{
1321 "event_id": "52df9022835246eeb317dbd739ccd059",
1322 "fingerprint": [
1323 "{{ default }}"
1324 ],
1325 "platform": "other",
1326 "dist": "52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059"
1327}"#;
1328
1329 let mut event = Annotated::<Event>::from_json(json).unwrap();
1330
1331 normalize_event(&mut event, &NormalizationConfig::default());
1332
1333 let dist = &event.value().unwrap().dist;
1334 let result = &Annotated::<String>::from_error(
1335 Error::new(ErrorKind::ValueTooLong),
1336 Some(Value::String("52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059".to_owned()))
1337 );
1338 assert_eq!(dist, result);
1339 }
1340
1341 #[test]
1342 fn test_regression_backfills_abs_path_even_when_moving_stacktrace() {
1343 let mut event = Annotated::new(Event {
1344 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
1345 ty: Annotated::new("FooDivisionError".to_owned()),
1346 value: Annotated::new("hi".to_owned().into()),
1347 ..Exception::default()
1348 })])),
1349 stacktrace: Annotated::new(
1350 RawStacktrace {
1351 frames: Annotated::new(vec![Annotated::new(Frame {
1352 module: Annotated::new("MyModule".to_owned()),
1353 filename: Annotated::new("MyFilename".into()),
1354 function: Annotated::new("Void FooBar()".to_owned()),
1355 ..Frame::default()
1356 })]),
1357 ..RawStacktrace::default()
1358 }
1359 .into(),
1360 ),
1361 ..Event::default()
1362 });
1363
1364 normalize_event(&mut event, &NormalizationConfig::default());
1365
1366 assert_eq!(
1367 get_value!(event.exceptions.values[0].stacktrace!),
1368 &Stacktrace(RawStacktrace {
1369 frames: Annotated::new(vec![Annotated::new(Frame {
1370 module: Annotated::new("MyModule".to_owned()),
1371 filename: Annotated::new("MyFilename".into()),
1372 abs_path: Annotated::new("MyFilename".into()),
1373 function: Annotated::new("Void FooBar()".to_owned()),
1374 ..Frame::default()
1375 })]),
1376 ..RawStacktrace::default()
1377 })
1378 );
1379 }
1380
1381 #[test]
1382 fn test_parses_sdk_info_from_header() {
1383 let mut event = Annotated::new(Event::default());
1384
1385 normalize_event(
1386 &mut event,
1387 &NormalizationConfig {
1388 client: Some("_fooBar/0.0.0".to_owned()),
1389 ..Default::default()
1390 },
1391 );
1392
1393 assert_eq!(
1394 get_path!(event.client_sdk!),
1395 &Annotated::new(ClientSdkInfo {
1396 name: Annotated::new("_fooBar".to_owned()),
1397 version: Annotated::new("0.0.0".to_owned()),
1398 ..ClientSdkInfo::default()
1399 })
1400 );
1401 }
1402
1403 #[test]
1404 fn test_discards_received() {
1405 let mut event = Annotated::new(Event {
1406 received: FromValue::from_value(Annotated::new(Value::U64(696_969_696_969))),
1407 ..Default::default()
1408 });
1409
1410 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1411 normalize_event(&mut event, &NormalizationConfig::default());
1412
1413 assert_eq!(get_value!(event.received!), get_value!(event.timestamp!));
1414 }
1415
1416 #[test]
1417 fn test_grouping_config() {
1418 let mut event = Annotated::new(Event {
1419 logentry: Annotated::from(LogEntry {
1420 message: Annotated::new("Hello World!".to_owned().into()),
1421 ..Default::default()
1422 }),
1423 ..Default::default()
1424 });
1425
1426 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1427 normalize_event(
1428 &mut event,
1429 &NormalizationConfig {
1430 grouping_config: Some(json!({
1431 "id": "legacy:1234-12-12".to_owned(),
1432 })),
1433 ..Default::default()
1434 },
1435 );
1436
1437 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1438 ".event_id" => "[event-id]",
1439 ".received" => "[received]",
1440 ".timestamp" => "[timestamp]"
1441 }, @r###"
1442 {
1443 "event_id": "[event-id]",
1444 "level": "error",
1445 "type": "default",
1446 "logentry": {
1447 "formatted": "Hello World!",
1448 },
1449 "logger": "",
1450 "platform": "other",
1451 "timestamp": "[timestamp]",
1452 "received": "[received]",
1453 "grouping_config": {
1454 "id": "legacy:1234-12-12",
1455 },
1456 }
1457 "###);
1458 }
1459
1460 #[test]
1461 fn test_logentry_error() {
1462 let json = r#"
1463{
1464 "event_id": "74ad1301f4df489ead37d757295442b1",
1465 "timestamp": 1668148328.308933,
1466 "received": 1668148328.308933,
1467 "level": "error",
1468 "platform": "python",
1469 "logentry": {
1470 "params": [
1471 "bogus"
1472 ],
1473 "formatted": 42
1474 }
1475}
1476"#;
1477 let mut event = Annotated::<Event>::from_json(json).unwrap();
1478
1479 normalize_event(&mut event, &NormalizationConfig::default());
1480
1481 assert_json_snapshot!(SerializableAnnotated(&event), {".received" => "[received]"}, @r###"
1482 {
1483 "event_id": "74ad1301f4df489ead37d757295442b1",
1484 "level": "error",
1485 "type": "default",
1486 "logentry": null,
1487 "logger": "",
1488 "platform": "python",
1489 "timestamp": 1668148328.308933,
1490 "received": "[received]",
1491 "_meta": {
1492 "logentry": {
1493 "": {
1494 "err": [
1495 [
1496 "invalid_data",
1497 {
1498 "reason": "no message present"
1499 }
1500 ]
1501 ],
1502 "val": {
1503 "formatted": null,
1504 "message": null,
1505 "params": [
1506 "bogus"
1507 ]
1508 }
1509 }
1510 }
1511 }
1512 }
1513 "###)
1514 }
1515
1516 #[test]
1517 fn test_future_timestamp() {
1518 let mut event = Annotated::new(Event {
1519 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 2, 0).unwrap().into()),
1520 ..Default::default()
1521 });
1522
1523 let received_at = Some(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap());
1524 let max_secs_in_past = Some(30 * 24 * 3600);
1525 let max_secs_in_future = Some(60);
1526
1527 validate_event(
1528 &mut event,
1529 &EventValidationConfig {
1530 received_at,
1531 max_secs_in_past,
1532 max_secs_in_future,
1533 is_validated: false,
1534 ..Default::default()
1535 },
1536 )
1537 .unwrap();
1538 normalize_event(&mut event, &NormalizationConfig::default());
1539
1540 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1541 ".event_id" => "[event-id]",
1542 }, @r###"
1543 {
1544 "event_id": "[event-id]",
1545 "level": "error",
1546 "type": "default",
1547 "logger": "",
1548 "platform": "other",
1549 "timestamp": 946857600.0,
1550 "received": 946857600.0,
1551 "_meta": {
1552 "timestamp": {
1553 "": Meta(Some(MetaInner(
1554 err: [
1555 [
1556 "future_timestamp",
1557 {
1558 "sdk_time": "2000-01-03T00:02:00+00:00",
1559 "server_time": "2000-01-03T00:00:00+00:00",
1560 },
1561 ],
1562 ],
1563 ))),
1564 },
1565 },
1566 }
1567 "###);
1568 }
1569
1570 #[test]
1571 fn test_past_timestamp() {
1572 let mut event = Annotated::new(Event {
1573 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap().into()),
1574 ..Default::default()
1575 });
1576
1577 let received_at = Some(Utc.with_ymd_and_hms(2000, 3, 3, 0, 0, 0).unwrap());
1578 let max_secs_in_past = Some(30 * 24 * 3600);
1579 let max_secs_in_future = Some(60);
1580
1581 validate_event(
1582 &mut event,
1583 &EventValidationConfig {
1584 received_at,
1585 max_secs_in_past,
1586 max_secs_in_future,
1587 is_validated: false,
1588 ..Default::default()
1589 },
1590 )
1591 .unwrap();
1592 normalize_event(&mut event, &NormalizationConfig::default());
1593
1594 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1595 ".event_id" => "[event-id]",
1596 }, @r###"
1597 {
1598 "event_id": "[event-id]",
1599 "level": "error",
1600 "type": "default",
1601 "logger": "",
1602 "platform": "other",
1603 "timestamp": 952041600.0,
1604 "received": 952041600.0,
1605 "_meta": {
1606 "timestamp": {
1607 "": Meta(Some(MetaInner(
1608 err: [
1609 [
1610 "past_timestamp",
1611 {
1612 "sdk_time": "2000-01-03T00:00:00+00:00",
1613 "server_time": "2000-03-03T00:00:00+00:00",
1614 },
1615 ],
1616 ],
1617 ))),
1618 },
1619 },
1620 }
1621 "###);
1622 }
1623
1624 #[test]
1625 fn test_normalize_logger_empty() {
1626 let mut event = Event::from_value(
1627 serde_json::json!({
1628 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1629 "platform": "java",
1630 "logger": "",
1631 })
1632 .into(),
1633 );
1634
1635 normalize_event(&mut event, &NormalizationConfig::default());
1636 assert_annotated_snapshot!(event);
1637 }
1638
1639 #[test]
1640 fn test_normalize_logger_trimmed() {
1641 let mut event = Event::from_value(
1642 serde_json::json!({
1643 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1644 "platform": "java",
1645 "logger": " \t \t ",
1646 })
1647 .into(),
1648 );
1649
1650 normalize_event(&mut event, &NormalizationConfig::default());
1651 assert_annotated_snapshot!(event);
1652 }
1653
1654 #[test]
1655 fn test_normalize_logger_short_no_trimming() {
1656 let mut event = Event::from_value(
1657 serde_json::json!({
1658 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1659 "platform": "java",
1660 "logger": "my.short-logger.isnt_trimmed",
1661 })
1662 .into(),
1663 );
1664
1665 normalize_event(&mut event, &NormalizationConfig::default());
1666 assert_annotated_snapshot!(event);
1667 }
1668
1669 #[test]
1670 fn test_normalize_logger_exact_length() {
1671 let mut event = Event::from_value(
1672 serde_json::json!({
1673 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1674 "platform": "java",
1675 "logger": "this_is-exactly-the_max_len.012345678901234567890123456789012345",
1676 })
1677 .into(),
1678 );
1679
1680 normalize_event(&mut event, &NormalizationConfig::default());
1681 assert_annotated_snapshot!(event);
1682 }
1683
1684 #[test]
1685 fn test_normalize_logger_too_long_single_word() {
1686 let mut event = Event::from_value(
1687 serde_json::json!({
1688 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1689 "platform": "java",
1690 "logger": "this_is-way_too_long-and_we_only_have_one_word-so_we_cant_smart_trim",
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_trimmed_at_max() {
1701 let mut event = Event::from_value(
1702 serde_json::json!({
1703 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1704 "platform": "java",
1705 "logger": "already_out.out.in.this_part-is-kept.this_right_here-is_an-extremely_long_word",
1706 })
1707 .into(),
1708 );
1709
1710 normalize_event(&mut event, &NormalizationConfig::default());
1711 assert_annotated_snapshot!(event);
1712 }
1713
1714 #[test]
1715 fn test_normalize_logger_word_trimmed_before_max() {
1716 let mut event = Event::from_value(
1719 serde_json::json!({
1720 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1721 "platform": "java",
1722 "logger": "super_out.this_is_already_out_too.this_part-is-kept.this_right_here-is_a-very_long_word",
1723 })
1724 .into(),
1725 );
1726
1727 normalize_event(&mut event, &NormalizationConfig::default());
1728 assert_annotated_snapshot!(event);
1729 }
1730
1731 #[test]
1732 fn test_normalize_logger_word_leading_dots() {
1733 let mut event = Event::from_value(
1734 serde_json::json!({
1735 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1736 "platform": "java",
1737 "logger": "io.this-tests-the-smart-trimming-and-word-removal-around-dot.words",
1738 })
1739 .into(),
1740 );
1741
1742 normalize_event(&mut event, &NormalizationConfig::default());
1743 assert_annotated_snapshot!(event);
1744 }
1745
1746 #[test]
1747 fn test_normalization_is_idempotent() {
1748 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1750 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1751 let mut event = Annotated::new(Event {
1752 ty: Annotated::new(EventType::Transaction),
1753 transaction: Annotated::new("/".to_owned()),
1754 timestamp: Annotated::new(end.into()),
1755 start_timestamp: Annotated::new(start.into()),
1756 contexts: {
1757 let mut contexts = Contexts::new();
1758 contexts.add(TraceContext {
1759 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1760 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1761 op: Annotated::new("http.server".to_owned()),
1762 ..Default::default()
1763 });
1764 Annotated::new(contexts)
1765 },
1766 spans: Annotated::new(vec![Annotated::new(Span {
1767 timestamp: Annotated::new(
1768 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1769 ),
1770 start_timestamp: Annotated::new(
1771 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1772 ),
1773 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1774 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1775
1776 ..Default::default()
1777 })]),
1778 ..Default::default()
1779 });
1780
1781 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1782 relay_event_schema::processor::apply(event, |e, _m| {
1783 e.received = Annotated::empty();
1784 Ok(())
1785 })
1786 .unwrap();
1787 event
1788 }
1789
1790 normalize_event(&mut event, &NormalizationConfig::default());
1791 let first = remove_received_from_event(&mut event.clone())
1792 .to_json()
1793 .unwrap();
1794 normalize_event(&mut event, &NormalizationConfig::default());
1797 let second = remove_received_from_event(&mut event.clone())
1798 .to_json()
1799 .unwrap();
1800 assert_eq!(&first, &second, "idempotency check failed");
1801
1802 normalize_event(&mut event, &NormalizationConfig::default());
1803 let third = remove_received_from_event(&mut event.clone())
1804 .to_json()
1805 .unwrap();
1806 assert_eq!(&second, &third, "idempotency check failed");
1807 }
1808
1809 #[test]
1816 fn test_full_normalization_is_idempotent() {
1817 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1818 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1819 let mut event = Annotated::new(Event {
1820 ty: Annotated::new(EventType::Transaction),
1821 transaction: Annotated::new("/".to_owned()),
1822 timestamp: Annotated::new(end.into()),
1823 start_timestamp: Annotated::new(start.into()),
1824 contexts: {
1825 let mut contexts = Contexts::new();
1826 contexts.add(TraceContext {
1827 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1828 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1829 op: Annotated::new("http.server".to_owned()),
1830 ..Default::default()
1831 });
1832 Annotated::new(contexts)
1833 },
1834 spans: Annotated::new(vec![Annotated::new(Span {
1835 timestamp: Annotated::new(
1836 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1837 ),
1838 start_timestamp: Annotated::new(
1839 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1840 ),
1841 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1842 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1843
1844 ..Default::default()
1845 })]),
1846 ..Default::default()
1847 });
1848
1849 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1850 relay_event_schema::processor::apply(event, |e, _m| {
1851 e.received = Annotated::empty();
1852 Ok(())
1853 })
1854 .unwrap();
1855 event
1856 }
1857
1858 let full_normalization_config = NormalizationConfig {
1859 is_renormalize: false,
1860 enable_trimming: true,
1861 remove_other: true,
1862 emit_event_errors: true,
1863 ..Default::default()
1864 };
1865
1866 normalize_event(&mut event, &full_normalization_config);
1867 let first = remove_received_from_event(&mut event.clone())
1868 .to_json()
1869 .unwrap();
1870 normalize_event(&mut event, &full_normalization_config);
1873 let second = remove_received_from_event(&mut event.clone())
1874 .to_json()
1875 .unwrap();
1876 assert_eq!(&first, &second, "idempotency check failed");
1877
1878 normalize_event(&mut event, &full_normalization_config);
1879 let third = remove_received_from_event(&mut event.clone())
1880 .to_json()
1881 .unwrap();
1882 assert_eq!(&second, &third, "idempotency check failed");
1883 }
1884
1885 #[test]
1886 fn test_normalize_validates_spans() {
1887 let event = Annotated::<Event>::from_json(
1888 r#"
1889 {
1890 "type": "transaction",
1891 "transaction": "/",
1892 "timestamp": 946684810.0,
1893 "start_timestamp": 946684800.0,
1894 "contexts": {
1895 "trace": {
1896 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1897 "span_id": "fa90fdead5f74053",
1898 "op": "http.server",
1899 "type": "trace"
1900 }
1901 },
1902 "spans": []
1903 }
1904 "#,
1905 )
1906 .unwrap();
1907
1908 for span in [
1910 r#"null"#,
1911 r#"{
1912 "timestamp": 946684810.0,
1913 "start_timestamp": 946684900.0,
1914 "span_id": "fa90fdead5f74053",
1915 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1916 }"#,
1917 r#"{
1918 "timestamp": 946684810.0,
1919 "span_id": "fa90fdead5f74053",
1920 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1921 }"#,
1922 r#"{
1923 "timestamp": 946684810.0,
1924 "start_timestamp": 946684800.0,
1925 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1926 }"#,
1927 r#"{
1928 "timestamp": 946684810.0,
1929 "start_timestamp": 946684800.0,
1930 "span_id": "fa90fdead5f74053"
1931 }"#,
1932 ] {
1933 let mut modified_event = event.clone();
1934 let event_ref = modified_event.value_mut().as_mut().unwrap();
1935 event_ref
1936 .spans
1937 .set_value(Some(vec![Annotated::<Span>::from_json(span).unwrap()]));
1938
1939 let res = validate_event(&mut modified_event, &EventValidationConfig::default());
1940
1941 assert!(res.is_err(), "{span:?}");
1942 }
1943 }
1944
1945 #[test]
1946 fn test_normalization_respects_is_renormalize() {
1947 let mut event = Annotated::<Event>::from_json(
1948 r#"
1949 {
1950 "type": "default",
1951 "tags": [["environment", "some_environment"]]
1952 }
1953 "#,
1954 )
1955 .unwrap();
1956
1957 normalize_event(
1958 &mut event,
1959 &NormalizationConfig {
1960 is_renormalize: true,
1961 ..Default::default()
1962 },
1963 );
1964
1965 assert_debug_snapshot!(event.value().unwrap().tags, @r###"
1966 Tags(
1967 PairList(
1968 [
1969 TagEntry(
1970 "environment",
1971 "some_environment",
1972 ),
1973 ],
1974 ),
1975 )
1976 "###);
1977 }
1978
1979 #[test]
1980 fn test_geo_in_normalize() {
1981 let mut event = Annotated::<Event>::from_json(
1982 r#"
1983 {
1984 "type": "transaction",
1985 "transaction": "/foo/",
1986 "timestamp": 946684810.0,
1987 "start_timestamp": 946684800.0,
1988 "contexts": {
1989 "trace": {
1990 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1991 "span_id": "fa90fdead5f74053",
1992 "op": "http.server",
1993 "type": "trace"
1994 }
1995 },
1996 "transaction_info": {
1997 "source": "url"
1998 },
1999 "user": {
2000 "ip_address": "2.125.160.216"
2001 }
2002 }
2003 "#,
2004 )
2005 .unwrap();
2006
2007 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
2008
2009 let user_geo = event.value().unwrap().user.value().unwrap().geo.value();
2011 assert!(user_geo.is_none());
2012
2013 normalize_event(
2014 &mut event,
2015 &NormalizationConfig {
2016 geoip_lookup: Some(&lookup),
2017 ..Default::default()
2018 },
2019 );
2020
2021 let user_geo = event
2023 .value()
2024 .unwrap()
2025 .user
2026 .value()
2027 .unwrap()
2028 .geo
2029 .value()
2030 .unwrap();
2031
2032 assert_eq!(user_geo.country_code.value().unwrap(), "GB");
2033 assert_eq!(user_geo.city.value().unwrap(), "Boxford");
2034 }
2035
2036 #[test]
2037 fn test_normalize_app_start_spans_only_for_react_native_3_to_4_4() {
2038 let mut event = Event {
2039 spans: Annotated::new(vec![Annotated::new(Span {
2040 op: Annotated::new("app_start_cold".to_owned()),
2041 ..Default::default()
2042 })]),
2043 client_sdk: Annotated::new(ClientSdkInfo {
2044 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2045 version: Annotated::new("4.5.0".to_owned()),
2046 ..Default::default()
2047 }),
2048 ..Default::default()
2049 };
2050 normalize_app_start_spans(&mut event);
2051 assert_debug_snapshot!(event.spans, @r###"
2052 [
2053 Span {
2054 timestamp: ~,
2055 start_timestamp: ~,
2056 exclusive_time: ~,
2057 op: "app_start_cold",
2058 span_id: ~,
2059 parent_span_id: ~,
2060 trace_id: ~,
2061 segment_id: ~,
2062 is_segment: ~,
2063 is_remote: ~,
2064 status: ~,
2065 description: ~,
2066 tags: ~,
2067 origin: ~,
2068 profile_id: ~,
2069 data: ~,
2070 links: ~,
2071 sentry_tags: ~,
2072 received: ~,
2073 measurements: ~,
2074 platform: ~,
2075 was_transaction: ~,
2076 kind: ~,
2077 _performance_issues_spans: ~,
2078 other: {},
2079 },
2080 ]
2081 "###);
2082 }
2083
2084 #[test]
2085 fn test_normalize_app_start_cold_spans_for_react_native() {
2086 let mut event = Event {
2087 spans: Annotated::new(vec![Annotated::new(Span {
2088 op: Annotated::new("app_start_cold".to_owned()),
2089 ..Default::default()
2090 })]),
2091 client_sdk: Annotated::new(ClientSdkInfo {
2092 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2093 version: Annotated::new("4.4.0".to_owned()),
2094 ..Default::default()
2095 }),
2096 ..Default::default()
2097 };
2098 normalize_app_start_spans(&mut event);
2099 assert_debug_snapshot!(event.spans, @r###"
2100 [
2101 Span {
2102 timestamp: ~,
2103 start_timestamp: ~,
2104 exclusive_time: ~,
2105 op: "app.start.cold",
2106 span_id: ~,
2107 parent_span_id: ~,
2108 trace_id: ~,
2109 segment_id: ~,
2110 is_segment: ~,
2111 is_remote: ~,
2112 status: ~,
2113 description: ~,
2114 tags: ~,
2115 origin: ~,
2116 profile_id: ~,
2117 data: ~,
2118 links: ~,
2119 sentry_tags: ~,
2120 received: ~,
2121 measurements: ~,
2122 platform: ~,
2123 was_transaction: ~,
2124 kind: ~,
2125 _performance_issues_spans: ~,
2126 other: {},
2127 },
2128 ]
2129 "###);
2130 }
2131
2132 #[test]
2133 fn test_normalize_app_start_warm_spans_for_react_native() {
2134 let mut event = Event {
2135 spans: Annotated::new(vec![Annotated::new(Span {
2136 op: Annotated::new("app_start_warm".to_owned()),
2137 ..Default::default()
2138 })]),
2139 client_sdk: Annotated::new(ClientSdkInfo {
2140 name: Annotated::new("sentry.javascript.react-native".to_owned()),
2141 version: Annotated::new("4.4.0".to_owned()),
2142 ..Default::default()
2143 }),
2144 ..Default::default()
2145 };
2146 normalize_app_start_spans(&mut event);
2147 assert_debug_snapshot!(event.spans, @r###"
2148 [
2149 Span {
2150 timestamp: ~,
2151 start_timestamp: ~,
2152 exclusive_time: ~,
2153 op: "app.start.warm",
2154 span_id: ~,
2155 parent_span_id: ~,
2156 trace_id: ~,
2157 segment_id: ~,
2158 is_segment: ~,
2159 is_remote: ~,
2160 status: ~,
2161 description: ~,
2162 tags: ~,
2163 origin: ~,
2164 profile_id: ~,
2165 data: ~,
2166 links: ~,
2167 sentry_tags: ~,
2168 received: ~,
2169 measurements: ~,
2170 platform: ~,
2171 was_transaction: ~,
2172 kind: ~,
2173 _performance_issues_spans: ~,
2174 other: {},
2175 },
2176 ]
2177 "###);
2178 }
2179}