Skip to main content

relay_event_normalization/normalize/
mod.rs

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