relay_event_normalization/normalize/
mod.rs

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