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