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