relay_event_normalization/normalize/
mod.rs

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