relay_event_normalization/normalize/
mod.rs

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/// Defines a builtin measurement.
19#[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    /// Creates a new [`BuiltinMeasurementKey`].
34    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    /// Returns the name of the built in measurement key.
43    pub fn name(&self) -> &str {
44        &self.name
45    }
46
47    /// Returns the unit of the built in measurement key.
48    pub fn unit(&self) -> &MetricUnit {
49        &self.unit
50    }
51
52    /// Return true if the built in measurement key allows negative values.
53    pub fn allow_negative(&self) -> &bool {
54        &self.allow_negative
55    }
56}
57
58/// Configuration for measurements normalization.
59#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Hash)]
60#[serde(default, rename_all = "camelCase")]
61pub struct MeasurementsConfig {
62    /// A list of measurements that are built-in and are not subject to custom measurement limits.
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub builtin_measurements: Vec<BuiltinMeasurementKey>,
65
66    /// The maximum number of measurements allowed per event that are not known measurements.
67    pub max_custom_measurements: usize,
68}
69
70impl MeasurementsConfig {
71    /// The length of a full measurement MRI, minus the name and the unit. This length is the same
72    /// for every measurement-mri.
73    pub const MEASUREMENT_MRI_OVERHEAD: usize = 29;
74}
75
76/// Returns `true` if the given platform string is a known platform identifier.
77///
78/// See [`VALID_PLATFORMS`] for a list of all known platforms.
79pub fn is_valid_platform(platform: &str) -> bool {
80    VALID_PLATFORMS.contains(&platform)
81}
82
83/// Replaces snake_case app start spans op with dot.case op.
84///
85/// This is done for the affected React Native SDK versions (from 3 to 4.4).
86pub 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/// Container for global and project level [`MeasurementsConfig`]. The purpose is to handle
116/// the merging logic.
117#[derive(Clone, Debug)]
118pub struct CombinedMeasurementsConfig<'a> {
119    project: Option<&'a MeasurementsConfig>,
120    global: Option<&'a MeasurementsConfig>,
121}
122
123impl<'a> CombinedMeasurementsConfig<'a> {
124    /// Constructor for [`CombinedMeasurementsConfig`].
125    pub fn new(
126        project: Option<&'a MeasurementsConfig>,
127        global: Option<&'a MeasurementsConfig>,
128    ) -> Self {
129        CombinedMeasurementsConfig { project, global }
130    }
131
132    /// Returns an iterator over the merged builtin measurement keys.
133    ///
134    /// Items from the project config are prioritized over global config, and
135    /// there are no duplicates.
136    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    /// Gets the max custom measurements value from the [`MeasurementsConfig`] from project level or
155    /// global level. If both of them are available, it will choose the most restrictive.
156    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/// Defines a weighted component for a performance score.
170///
171/// Weight is the % of score it can take up (eg. LCP is a max of 35% weight for desktops)
172/// Currently also contains (p10, p50) which are used for log CDF normalization of the weight score
173#[derive(Debug, Default, Clone, Serialize, Deserialize)]
174pub struct PerformanceScoreWeightedComponent {
175    /// Measurement (eg. measurements.lcp) to be matched against. If this measurement is missing the entire
176    /// profile will be discarded.
177    pub measurement: String,
178    /// Weight [0,1.0] of this component in the performance score
179    pub weight: FiniteF64,
180    /// p10 used to define the log-normal for calculation
181    pub p10: FiniteF64,
182    /// Median used to define the log-normal for calculation
183    pub p50: FiniteF64,
184    /// Whether the measurement is optional. If the measurement is missing, performance score processing
185    /// may still continue, and the weight will be 0.
186    #[serde(default)]
187    pub optional: bool,
188}
189
190/// Defines a profile for performance score.
191///
192/// A profile contains weights for a score of 100% and match against an event using a condition.
193/// eg. Desktop vs. Mobile(web) profiles for better web vital score calculation.
194#[derive(Debug, Default, Clone, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct PerformanceScoreProfile {
197    /// Name of the profile, used for debugging and faceting multiple profiles
198    pub name: Option<String>,
199    /// Score components
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub score_components: Vec<PerformanceScoreWeightedComponent>,
202    /// See [`RuleCondition`] for all available options to specify and combine conditions.
203    pub condition: Option<RuleCondition>,
204    /// The version of the profile, used to isolate changes to score calculations.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub version: Option<String>,
207}
208
209/// Defines the performance configuration for the project.
210///
211/// Includes profiles matching different behaviour (desktop / mobile) and weights matching those
212/// specific conditions.
213#[derive(Debug, Default, Clone, Serialize, Deserialize)]
214pub struct PerformanceScoreConfig {
215    /// List of performance profiles, only the first with matching conditions will be applied.
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub profiles: Vec<PerformanceScoreProfile>,
218}
219
220/// A mapping of AI model types (like GPT-4) to their respective costs.
221///
222/// This struct uses a dictionary-based cost structure with exact model ID keys and granular
223/// token pricing.
224///
225/// Example JSON:
226/// ```json
227/// {
228///   "version": 2,
229///   "models": {
230///     "gpt-4": {
231///       "inputPerToken": 0.03,
232///       "outputPerToken": 0.06,
233///       "outputReasoningPerToken": 0.12,
234///       "inputCachedPerToken": 0.015
235///     }
236///   }
237/// }
238/// ```
239#[derive(Clone, Default, Debug, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct ModelCosts {
242    /// The version of the model cost struct
243    pub version: u16,
244
245    /// The mappings of model ID => cost as a dictionary
246    #[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    /// `true` if the model costs are empty and the version is supported.
254    pub fn is_empty(&self) -> bool {
255        self.models.is_empty() || !self.is_enabled()
256    }
257
258    /// `false` if measurement and metrics extraction should be skipped.
259    pub fn is_enabled(&self) -> bool {
260        self.version == Self::SUPPORTED_VERSION
261    }
262
263    /// Gets the cost per token, if defined for the given model.
264    pub fn cost_per_token(&self, model_id: &str) -> Option<&ModelCostV2> {
265        if !self.is_enabled() {
266            return None;
267        }
268
269        // First try exact match by creating a Pattern from the model_id
270        if let Some(value) = self.models.get(model_id) {
271            return Some(value);
272        }
273
274        // if there is not a direct match, try to find the match using a pattern
275        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/// Version 2 of a mapping of AI model types (like GPT-4) to their respective costs.
286#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
287#[serde(rename_all = "camelCase")]
288pub struct ModelCostV2 {
289    /// The cost per input token
290    pub input_per_token: f64,
291    /// The cost per output token
292    pub output_per_token: f64,
293    /// The cost per output reasoning token
294    pub output_reasoning_per_token: f64,
295    /// The cost per input cached token
296    pub input_cached_per_token: f64,
297}
298/// A mapping of AI operation types from span.op to gen_ai.operation.type.
299///
300/// This struct uses a dictionary-based mapping structure with pattern-based span operation keys
301/// and corresponding AI operation type values.
302///
303/// Example JSON:
304/// ```json
305/// {
306///   "version": 1,
307///   "operation_types": {
308///     "gen_ai.execute_tool": "tool",
309///     "gen_ai.handoff": "handoff",
310///     "gen_ai.invoke_agent": "agent",
311///   }
312/// }
313/// ```
314#[derive(Clone, Default, Debug, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct AiOperationTypeMap {
317    /// The version of the operation type mapping struct
318    pub version: u16,
319
320    /// The mappings of span.op => gen_ai.operation.type as a dictionary
321    #[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    /// `true` if the operation type mapping is empty and the version is supported.
329    pub fn is_empty(&self) -> bool {
330        self.operation_types.is_empty() || !self.is_enabled()
331    }
332
333    /// `false` if operation type mapping should be skipped.
334    pub fn is_enabled(&self) -> bool {
335        self.version == Self::SUPPORTED_VERSION
336    }
337
338    /// Gets the AI operation type for the given span operation, if defined.
339    pub fn get_operation_type(&self, span_op: &str) -> Option<&str> {
340        if !self.is_enabled() {
341            return None;
342        }
343
344        // try first direct match with span_op
345        if let Some(value) = self.operation_types.get(span_op) {
346            return Some(value.as_str());
347        }
348
349        // if there is not a direct match, try to find the match using a pattern
350        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 that integer versions are handled correctly in the struct format
389    #[test]
390    fn test_model_cost_version_sent_as_number() {
391        // Test integer version 2
392        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        // Test unknown integer version
422        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        // Test V2 functionality
463        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        // Test glob matching functionality in cost_per_token
493        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        // Test glob matching with gpt-4 variants (prefix matching)
520        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        // Test that unknown versions are handled properly
548        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        // Test version 0 (invalid)
555        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        // Test serialization and deserialization with patterns
564        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            // The 'bar' here will be ignored since it's a duplicate from the project level.
629            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        // Empty configs will return a None value for max measurements.
642        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        // If only project level measurement config is there, return its max custom measurement variable.
656        let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), None);
657        assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
658
659        // Same logic for when only global level measurement config exists.
660        let dynamic_config = CombinedMeasurementsConfig::new(None, Some(&glob));
661        assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 4);
662
663        // If both is available, pick the smallest number.
664        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        // should keep the first occurrence of every tag
1170        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            // We assume the status to be null.
1190            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        // This test verifies the "smart" trimming on words -- the logger name
1717        // should be cut before the max limit, removing entire words.
1718        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        // get an event, normalize it. the result of that must be the same as normalizing it once more
1749        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        // Expected some fields (such as timestamps) exist after first normalization.
1795
1796        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    /// Validate full normalization is idempotent.
1810    ///
1811    /// Both PoPs and processing relays will temporarily run full normalization
1812    /// in events, during the rollout of running normalization once in internal
1813    /// relays.
1814    // TODO(iker): remove this test after the rollout is done.
1815    #[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        // Expected some fields (such as timestamps) exist after first normalization.
1871
1872        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        // All incomplete spans should be caught by normalization:
1909        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        // Extract user's geo information before normalization.
2010        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        // Extract user's geo information after normalization.
2022        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}