relay_event_normalization/normalize/
mod.rs

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