relay_event_normalization/normalize/
mod.rs

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