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