relay_event_normalization/
event.rs

1//! Event normalization.
2//!
3//! This module provides a function to normalize events.
4
5use std::collections::hash_map::DefaultHasher;
6
7use std::hash::{Hash, Hasher};
8use std::mem;
9use std::sync::OnceLock;
10
11use itertools::Itertools;
12use regex::Regex;
13use relay_base_schema::metrics::{
14    DurationUnit, FractionUnit, MetricUnit, can_be_valid_metric_name,
15};
16use relay_event_schema::processor::{self, ProcessingAction, ProcessingState, Processor};
17use relay_event_schema::protocol::{
18    AsPair, AutoInferSetting, ClientSdkInfo, Context, ContextInner, Contexts, DebugImage,
19    DeviceClass, Event, EventId, EventType, Exception, Headers, IpAddr, Level, LogEntry,
20    Measurement, Measurements, NelContext, PerformanceScoreContext, ReplayContext, Request, Span,
21    SpanStatus, Tags, Timestamp, TraceContext, User, VALID_PLATFORMS,
22};
23use relay_protocol::{
24    Annotated, Empty, Error, ErrorKind, FiniteF64, FromValue, Getter, Meta, Object, Remark,
25    RemarkType, TryFromFloatError, Value,
26};
27use smallvec::SmallVec;
28use uuid::Uuid;
29
30use crate::normalize::request;
31use crate::span::ai::enrich_ai_span_data;
32use crate::span::tag_extraction::extract_span_tags_from_event;
33use crate::utils::{self, MAX_DURATION_MOBILE_MS, get_event_user_tag};
34use crate::{
35    BorrowedSpanOpDefaults, BreakdownsConfig, CombinedMeasurementsConfig, GeoIpLookup, MaxChars,
36    ModelCosts, PerformanceScoreConfig, RawUserAgentInfo, SpanDescriptionRule,
37    TransactionNameConfig, breakdowns, event_error, legacy, mechanism, remove_other, schema, span,
38    stacktrace, transactions, trimming, user_agent,
39};
40
41/// Configuration for [`normalize_event`].
42#[derive(Clone, Debug)]
43pub struct NormalizationConfig<'a> {
44    /// The identifier of the target project, which gets added to the payload.
45    pub project_id: Option<u64>,
46
47    /// The name and version of the SDK that sent the event.
48    pub client: Option<String>,
49
50    /// The internal identifier of the DSN, which gets added to the payload.
51    ///
52    /// Note that this is different from the DSN's public key. The ID is usually numeric.
53    pub key_id: Option<String>,
54
55    /// The version of the protocol.
56    ///
57    /// This is a deprecated field, as there is no more versioning of Relay event payloads.
58    pub protocol_version: Option<String>,
59
60    /// Configuration for issue grouping.
61    ///
62    /// This configuration is persisted into the event payload to achieve idempotency in the
63    /// processing pipeline and for reprocessing.
64    pub grouping_config: Option<serde_json::Value>,
65
66    /// The IP address of the SDK that sent the event.
67    ///
68    /// When `{{auto}}` is specified and there is no other IP address in the payload, such as in the
69    /// `request` context, this IP address gets added to the `user` context.
70    pub client_ip: Option<&'a IpAddr>,
71
72    /// Specifies whether the client_ip should be used to determine the ip address of the user.
73    pub infer_ip_address: bool,
74
75    /// The SDK's sample rate as communicated via envelope headers.
76    ///
77    /// It is persisted into the event payload.
78    pub client_sample_rate: Option<f64>,
79
80    /// The user-agent and client hints obtained from the submission request headers.
81    ///
82    /// Client hints are the preferred way to infer device, operating system, and browser
83    /// information should the event payload contain no such data. If no client hints are present,
84    /// normalization falls back to the user agent.
85    pub user_agent: RawUserAgentInfo<&'a str>,
86
87    /// The maximum length for names of custom measurements.
88    ///
89    /// Measurements with longer names are removed from the transaction event and replaced with a
90    /// metadata entry.
91    pub max_name_and_unit_len: Option<usize>,
92
93    /// Configuration for measurement normalization in transaction events.
94    ///
95    /// Has an optional [`crate::MeasurementsConfig`] from both the project and the global level.
96    /// If at least one is provided, then normalization will truncate custom measurements
97    /// and add units of known built-in measurements.
98    pub measurements: Option<CombinedMeasurementsConfig<'a>>,
99
100    /// Emit breakdowns based on given configuration.
101    pub breakdowns_config: Option<&'a BreakdownsConfig>,
102
103    /// When `Some(true)`, context information is extracted from the user agent.
104    pub normalize_user_agent: Option<bool>,
105
106    /// Configuration to apply to transaction names, especially around sanitizing.
107    pub transaction_name_config: TransactionNameConfig<'a>,
108
109    /// When `true`, it is assumed that the event has been normalized before.
110    ///
111    /// This disables certain normalizations, especially all that are not idempotent. The
112    /// renormalize mode is intended for the use in the processing pipeline, so an event modified
113    /// during ingestion can be validated against the schema and large data can be trimmed. However,
114    /// advanced normalizations such as inferring contexts or clock drift correction are disabled.
115    pub is_renormalize: bool,
116
117    /// Overrides the default flag for other removal.
118    pub remove_other: bool,
119
120    /// When enabled, adds errors in the meta to the event's errors.
121    pub emit_event_errors: bool,
122
123    /// When `true`, infers the device class from CPU and model.
124    pub device_class_synthesis_config: bool,
125
126    /// When `true`, extracts tags from event and spans and materializes them into `span.data`.
127    pub enrich_spans: bool,
128
129    /// The maximum allowed size of tag values in bytes. Longer values will be cropped.
130    pub max_tag_value_length: usize, // TODO: move span related fields into separate config.
131
132    /// Configuration for replacing identifiers in the span description with placeholders.
133    ///
134    /// This is similar to `transaction_name_config`, but applies to span descriptions.
135    pub span_description_rules: Option<&'a Vec<SpanDescriptionRule>>,
136
137    /// Configuration for generating performance score measurements for web vitals
138    pub performance_score: Option<&'a PerformanceScoreConfig>,
139
140    /// Configuration for calculating the cost of AI model runs
141    pub ai_model_costs: Option<&'a ModelCosts>,
142
143    /// An initialized GeoIP lookup.
144    pub geoip_lookup: Option<&'a GeoIpLookup>,
145
146    /// When `Some(true)`, individual parts of the event payload is trimmed to a maximum size.
147    ///
148    /// See the event schema for size declarations.
149    pub enable_trimming: bool,
150
151    /// Controls whether spans should be normalized (e.g. normalizing the exclusive time).
152    ///
153    /// To normalize spans, `is_renormalize` must be disabled _and_ `normalize_spans` enabled.
154    pub normalize_spans: bool,
155
156    /// The identifier of the Replay running while this event was created.
157    ///
158    /// It is persisted into the event payload for correlation.
159    pub replay_id: Option<Uuid>,
160
161    /// Controls list of hosts to be excluded from scrubbing
162    pub span_allowed_hosts: &'a [String],
163
164    /// Rules to infer `span.op` from other span fields.
165    pub span_op_defaults: BorrowedSpanOpDefaults<'a>,
166
167    /// Set a flag to enable performance issue detection on spans.
168    pub performance_issues_spans: bool,
169}
170
171impl Default for NormalizationConfig<'_> {
172    fn default() -> Self {
173        Self {
174            project_id: Default::default(),
175            client: Default::default(),
176            key_id: Default::default(),
177            protocol_version: Default::default(),
178            grouping_config: Default::default(),
179            client_ip: Default::default(),
180            infer_ip_address: true,
181            client_sample_rate: Default::default(),
182            user_agent: Default::default(),
183            max_name_and_unit_len: Default::default(),
184            breakdowns_config: Default::default(),
185            normalize_user_agent: Default::default(),
186            transaction_name_config: Default::default(),
187            is_renormalize: Default::default(),
188            remove_other: Default::default(),
189            emit_event_errors: Default::default(),
190            device_class_synthesis_config: Default::default(),
191            enrich_spans: Default::default(),
192            max_tag_value_length: usize::MAX,
193            span_description_rules: Default::default(),
194            performance_score: Default::default(),
195            geoip_lookup: Default::default(),
196            ai_model_costs: Default::default(),
197            enable_trimming: false,
198            measurements: None,
199            normalize_spans: true,
200            replay_id: Default::default(),
201            span_allowed_hosts: Default::default(),
202            span_op_defaults: Default::default(),
203            performance_issues_spans: Default::default(),
204        }
205    }
206}
207
208/// Normalizes an event.
209///
210/// Normalization consists of applying a series of transformations on the event
211/// payload based on the given configuration.
212pub fn normalize_event(event: &mut Annotated<Event>, config: &NormalizationConfig) {
213    let Annotated(Some(event), meta) = event else {
214        return;
215    };
216
217    let is_renormalize = config.is_renormalize;
218
219    // Convert legacy data structures to current format
220    let _ = legacy::LegacyProcessor.process_event(event, meta, ProcessingState::root());
221
222    if !is_renormalize {
223        // Check for required and non-empty values
224        let _ = schema::SchemaProcessor.process_event(event, meta, ProcessingState::root());
225
226        normalize(event, meta, config);
227    }
228
229    if config.enable_trimming {
230        // Trim large strings and databags down
231        let _ =
232            trimming::TrimmingProcessor::new().process_event(event, meta, ProcessingState::root());
233    }
234
235    if config.remove_other {
236        // Remove unknown attributes at every level
237        let _ =
238            remove_other::RemoveOtherProcessor.process_event(event, meta, ProcessingState::root());
239    }
240
241    if config.emit_event_errors {
242        // Add event errors for top-level keys
243        let _ =
244            event_error::EmitEventErrors::new().process_event(event, meta, ProcessingState::root());
245    }
246}
247
248/// Normalizes the given event based on the given config.
249fn normalize(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
250    // Normalize the transaction.
251    // (internally noops for non-transaction events).
252    // TODO: Parts of this processor should probably be a filter so we
253    // can revert some changes to ProcessingAction)
254    let mut transactions_processor = transactions::TransactionsProcessor::new(
255        config.transaction_name_config,
256        config.span_op_defaults,
257    );
258    let _ = transactions_processor.process_event(event, meta, ProcessingState::root());
259
260    let client_ip = config.client_ip.filter(|_| config.infer_ip_address);
261
262    // Process security reports first to ensure all props.
263    normalize_security_report(event, client_ip, &config.user_agent);
264
265    // Process NEL reports to ensure all props.
266    normalize_nel_report(event, client_ip);
267
268    // Insert IP addrs before recursing, since geo lookup depends on it.
269    normalize_ip_addresses(
270        &mut event.request,
271        &mut event.user,
272        event.platform.as_str(),
273        client_ip,
274        event.client_sdk.value(),
275    );
276
277    if let Some(geoip_lookup) = config.geoip_lookup {
278        normalize_user_geoinfo(geoip_lookup, &mut event.user, config.client_ip);
279    }
280
281    // Validate the basic attributes we extract metrics from
282    let _ = processor::apply(&mut event.release, |release, meta| {
283        if crate::validate_release(release).is_ok() {
284            Ok(())
285        } else {
286            meta.add_error(ErrorKind::InvalidData);
287            Err(ProcessingAction::DeleteValueSoft)
288        }
289    });
290    let _ = processor::apply(&mut event.environment, |environment, meta| {
291        if crate::validate_environment(environment).is_ok() {
292            Ok(())
293        } else {
294            meta.add_error(ErrorKind::InvalidData);
295            Err(ProcessingAction::DeleteValueSoft)
296        }
297    });
298
299    // Default required attributes, even if they have errors
300    normalize_user(event);
301    normalize_logentry(&mut event.logentry, meta);
302    normalize_debug_meta(event);
303    normalize_breadcrumbs(event);
304    normalize_release_dist(event); // dist is a tag extracted along with other metrics from transactions
305    normalize_event_tags(event); // Tags are added to every metric
306
307    // TODO: Consider moving to store normalization
308    if config.device_class_synthesis_config {
309        normalize_device_class(event);
310    }
311    normalize_stacktraces(event);
312    normalize_exceptions(event); // Browser extension filters look at the stacktrace
313    normalize_user_agent(event, config.normalize_user_agent); // Legacy browsers filter
314    normalize_event_measurements(
315        event,
316        config.measurements.clone(),
317        config.max_name_and_unit_len,
318    ); // Measurements are part of the metric extraction
319    if let Some(version) = normalize_performance_score(event, config.performance_score) {
320        event
321            .contexts
322            .get_or_insert_with(Contexts::new)
323            .get_or_default::<PerformanceScoreContext>()
324            .score_profile_version = Annotated::new(version);
325    }
326    enrich_ai_span_data(event, config.ai_model_costs);
327    normalize_breakdowns(event, config.breakdowns_config); // Breakdowns are part of the metric extraction too
328    normalize_default_attributes(event, meta, config);
329    normalize_trace_context_tags(event);
330
331    let _ = processor::apply(&mut event.request, |request, _| {
332        request::normalize_request(request);
333        Ok(())
334    });
335
336    // Some contexts need to be normalized before metrics extraction takes place.
337    normalize_contexts(&mut event.contexts);
338
339    if config.normalize_spans && event.ty.value() == Some(&EventType::Transaction) {
340        span::reparent_broken_spans::reparent_broken_spans(event);
341        crate::normalize::normalize_app_start_spans(event);
342        span::exclusive_time::compute_span_exclusive_time(event);
343    }
344
345    if config.performance_issues_spans && event.ty.value() == Some(&EventType::Transaction) {
346        event._performance_issues_spans = Annotated::new(true);
347    }
348
349    if config.enrich_spans {
350        extract_span_tags_from_event(
351            event,
352            config.max_tag_value_length,
353            config.span_allowed_hosts,
354        );
355    }
356
357    if let Some(context) = event.context_mut::<TraceContext>() {
358        context.client_sample_rate = Annotated::from(config.client_sample_rate);
359    }
360    normalize_replay_context(event, config.replay_id);
361}
362
363fn normalize_replay_context(event: &mut Event, replay_id: Option<Uuid>) {
364    if let Some(contexts) = event.contexts.value_mut() {
365        if let Some(replay_id) = replay_id {
366            contexts.add(ReplayContext {
367                replay_id: Annotated::new(EventId(replay_id)),
368                other: Object::default(),
369            });
370        }
371    }
372}
373
374/// Backfills the client IP address on for the NEL reports.
375fn normalize_nel_report(event: &mut Event, client_ip: Option<&IpAddr>) {
376    if event.context::<NelContext>().is_none() {
377        return;
378    }
379
380    if let Some(client_ip) = client_ip {
381        let user = event.user.value_mut().get_or_insert_with(User::default);
382        user.ip_address = Annotated::new(client_ip.to_owned());
383    }
384}
385
386/// Backfills common security report attributes.
387fn normalize_security_report(
388    event: &mut Event,
389    client_ip: Option<&IpAddr>,
390    user_agent: &RawUserAgentInfo<&str>,
391) {
392    if !is_security_report(event) {
393        // This event is not a security report, exit here.
394        return;
395    }
396
397    event.logger.get_or_insert_with(|| "csp".to_owned());
398
399    if let Some(client_ip) = client_ip {
400        let user = event.user.value_mut().get_or_insert_with(User::default);
401        user.ip_address = Annotated::new(client_ip.to_owned());
402    }
403
404    if !user_agent.is_empty() {
405        let headers = event
406            .request
407            .value_mut()
408            .get_or_insert_with(Request::default)
409            .headers
410            .value_mut()
411            .get_or_insert_with(Headers::default);
412
413        user_agent.populate_event_headers(headers);
414    }
415}
416
417fn is_security_report(event: &Event) -> bool {
418    event.csp.value().is_some()
419        || event.expectct.value().is_some()
420        || event.expectstaple.value().is_some()
421        || event.hpkp.value().is_some()
422}
423
424/// Backfills IP addresses in various places.
425pub fn normalize_ip_addresses(
426    request: &mut Annotated<Request>,
427    user: &mut Annotated<User>,
428    platform: Option<&str>,
429    client_ip: Option<&IpAddr>,
430    client_sdk_settings: Option<&ClientSdkInfo>,
431) {
432    let infer_ip = client_sdk_settings
433        .and_then(|c| c.settings.0.as_ref())
434        .map(|s| s.infer_ip())
435        .unwrap_or_default();
436
437    // If infer_ip is set to Never then we just remove auto and don't continue
438    if let AutoInferSetting::Never = infer_ip {
439        // No user means there is also no IP so we can stop here
440        let Some(user) = user.value_mut() else {
441            return;
442        };
443        // If there is no IP we can also stop
444        let Some(ip) = user.ip_address.value() else {
445            return;
446        };
447        if ip.is_auto() {
448            user.ip_address.0 = None;
449            return;
450        }
451    }
452
453    let remote_addr_ip = request
454        .value()
455        .and_then(|r| r.env.value())
456        .and_then(|env| env.get("REMOTE_ADDR"))
457        .and_then(Annotated::<Value>::as_str)
458        .and_then(|ip| IpAddr::parse(ip).ok());
459
460    // IP address in REMOTE_ADDR will have precedence over client_ip because it's explicitly
461    // sent while client_ip is taken from X-Forwarded-For headers or the connection IP.
462    let inferred_ip = remote_addr_ip.as_ref().or(client_ip);
463
464    // We will infer IP addresses if:
465    // * The IP address is {{auto}}
466    // * the infer_ip setting is set to "auto"
467    let should_be_inferred = match user.value() {
468        Some(user) => match user.ip_address.value() {
469            Some(ip) => ip.is_auto(),
470            None => matches!(infer_ip, AutoInferSetting::Auto),
471        },
472        None => matches!(infer_ip, AutoInferSetting::Auto),
473    };
474
475    if should_be_inferred {
476        if let Some(ip) = inferred_ip {
477            let user = user.get_or_insert_with(User::default);
478            user.ip_address.set_value(Some(ip.to_owned()));
479        }
480    }
481
482    // Legacy behaviour:
483    // * Backfill if there is a REMOTE_ADDR and the user.ip_address was not backfilled until now
484    // * Empty means {{auto}} for some SDKs
485    if infer_ip == AutoInferSetting::Legacy {
486        if let Some(http_ip) = remote_addr_ip {
487            let user = user.get_or_insert_with(User::default);
488            user.ip_address.value_mut().get_or_insert(http_ip);
489        } else if let Some(client_ip) = inferred_ip {
490            let user = user.get_or_insert_with(User::default);
491            // auto is already handled above
492            if user.ip_address.value().is_none() {
493                // Only assume that empty means {{auto}} if there is no remark that the IP address has been removed.
494                let scrubbed_before = user
495                    .ip_address
496                    .meta()
497                    .iter_remarks()
498                    .any(|r| r.ty == RemarkType::Removed);
499                if !scrubbed_before {
500                    // In an ideal world all SDKs would set {{auto}} explicitly.
501                    if let Some("javascript") | Some("cocoa") | Some("objc") = platform {
502                        user.ip_address = Annotated::new(client_ip.to_owned());
503                    }
504                }
505            }
506        }
507    }
508}
509
510/// Sets the user's GeoIp info based on user's IP address.
511pub fn normalize_user_geoinfo(
512    geoip_lookup: &GeoIpLookup,
513    user: &mut Annotated<User>,
514    ip_addr: Option<&IpAddr>,
515) {
516    let user = user.value_mut().get_or_insert_with(User::default);
517    if let Some(ip_address) = user
518        .ip_address
519        .value()
520        .filter(|ip| !ip.is_auto())
521        .or(ip_addr)
522    {
523        if let Ok(Some(geo)) = geoip_lookup.lookup(ip_address.as_str()) {
524            user.geo.set_value(Some(geo));
525        }
526    }
527}
528
529fn normalize_user(event: &mut Event) {
530    let Annotated(Some(user), _) = &mut event.user else {
531        return;
532    };
533
534    if !user.other.is_empty() {
535        let data = user.data.value_mut().get_or_insert_with(Object::new);
536        data.extend(std::mem::take(&mut user.other));
537    }
538
539    // We set the `sentry_user` field in the `Event` payload in order to have it ready for the extraction
540    // pipeline.
541    let event_user_tag = get_event_user_tag(user);
542    user.sentry_user.set_value(event_user_tag);
543}
544
545fn normalize_logentry(logentry: &mut Annotated<LogEntry>, _meta: &mut Meta) {
546    let _ = processor::apply(logentry, |logentry, meta| {
547        crate::logentry::normalize_logentry(logentry, meta)
548    });
549}
550
551/// Normalizes the debug images in the event's debug meta.
552fn normalize_debug_meta(event: &mut Event) {
553    let Annotated(Some(debug_meta), _) = &mut event.debug_meta else {
554        return;
555    };
556    let Annotated(Some(debug_images), _) = &mut debug_meta.images else {
557        return;
558    };
559
560    for annotated_image in debug_images {
561        let _ = processor::apply(annotated_image, |image, meta| match image {
562            DebugImage::Other(_) => {
563                meta.add_error(Error::invalid("unsupported debug image type"));
564                Err(ProcessingAction::DeleteValueSoft)
565            }
566            _ => Ok(()),
567        });
568    }
569}
570
571fn normalize_breadcrumbs(event: &mut Event) {
572    let Annotated(Some(breadcrumbs), _) = &mut event.breadcrumbs else {
573        return;
574    };
575    let Some(breadcrumbs) = breadcrumbs.values.value_mut() else {
576        return;
577    };
578
579    for annotated_breadcrumb in breadcrumbs {
580        let Annotated(Some(breadcrumb), _) = annotated_breadcrumb else {
581            continue;
582        };
583
584        if breadcrumb.ty.value().is_empty() {
585            breadcrumb.ty.set_value(Some("default".to_owned()));
586        }
587        if breadcrumb.level.value().is_none() {
588            breadcrumb.level.set_value(Some(Level::Info));
589        }
590    }
591}
592
593/// Ensures that the `release` and `dist` fields match up.
594fn normalize_release_dist(event: &mut Event) {
595    normalize_dist(&mut event.dist);
596}
597
598fn normalize_dist(distribution: &mut Annotated<String>) {
599    let _ = processor::apply(distribution, |dist, meta| {
600        let trimmed = dist.trim();
601        if trimmed.is_empty() {
602            return Err(ProcessingAction::DeleteValueHard);
603        } else if bytecount::num_chars(trimmed.as_bytes()) > MaxChars::Distribution.limit() {
604            meta.add_error(Error::new(ErrorKind::ValueTooLong));
605            return Err(ProcessingAction::DeleteValueSoft);
606        } else if trimmed != dist {
607            *dist = trimmed.to_owned();
608        }
609        Ok(())
610    });
611}
612
613struct DedupCache(SmallVec<[u64; 16]>);
614
615impl DedupCache {
616    pub fn new() -> Self {
617        Self(SmallVec::default())
618    }
619
620    pub fn probe<H: Hash>(&mut self, element: H) -> bool {
621        let mut hasher = DefaultHasher::new();
622        element.hash(&mut hasher);
623        let hash = hasher.finish();
624
625        if self.0.contains(&hash) {
626            false
627        } else {
628            self.0.push(hash);
629            true
630        }
631    }
632}
633
634/// Removes internal tags and adds tags for well-known attributes.
635fn normalize_event_tags(event: &mut Event) {
636    let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
637    let environment = &mut event.environment;
638    if environment.is_empty() {
639        *environment = Annotated::empty();
640    }
641
642    // Fix case where legacy apps pass environment as a tag instead of a top level key
643    if let Some(tag) = tags.remove("environment").and_then(Annotated::into_value) {
644        environment.get_or_insert_with(|| tag);
645    }
646
647    // Remove internal tags, that are generated with a `sentry:` prefix when saving the event.
648    // They are not allowed to be set by the client due to ambiguity. Also, deduplicate tags.
649    let mut tag_cache = DedupCache::new();
650    tags.retain(|entry| {
651        match entry.value() {
652            Some(tag) => match tag.key() {
653                Some("release") | Some("dist") | Some("user") | Some("filename")
654                | Some("function") => false,
655                name => tag_cache.probe(name),
656            },
657            // ToValue will decide if we should skip serializing Annotated::empty()
658            None => true,
659        }
660    });
661
662    for tag in tags.iter_mut() {
663        let _ = processor::apply(tag, |tag, _| {
664            if let Some(key) = tag.key() {
665                if key.is_empty() {
666                    tag.0 = Annotated::from_error(Error::nonempty(), None);
667                } else if bytecount::num_chars(key.as_bytes()) > MaxChars::TagKey.limit() {
668                    tag.0 = Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None);
669                }
670            }
671
672            if let Some(value) = tag.value() {
673                if value.is_empty() {
674                    tag.1 = Annotated::from_error(Error::nonempty(), None);
675                } else if bytecount::num_chars(value.as_bytes()) > MaxChars::TagValue.limit() {
676                    tag.1 = Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None);
677                }
678            }
679
680            Ok(())
681        });
682    }
683
684    let server_name = std::mem::take(&mut event.server_name);
685    if server_name.value().is_some() {
686        let tag_name = "server_name".to_owned();
687        tags.insert(tag_name, server_name);
688    }
689
690    let site = std::mem::take(&mut event.site);
691    if site.value().is_some() {
692        let tag_name = "site".to_owned();
693        tags.insert(tag_name, site);
694    }
695}
696
697// Reads device specs (family, memory, cpu, etc) from context and sets the device.class tag to high,
698// medium, or low.
699fn normalize_device_class(event: &mut Event) {
700    let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
701    let tag_name = "device.class".to_owned();
702    // Remove any existing device.class tag set by the client, since this should only be set by relay.
703    tags.remove("device.class");
704    if let Some(contexts) = event.contexts.value() {
705        if let Some(device_class) = DeviceClass::from_contexts(contexts) {
706            tags.insert(tag_name, Annotated::new(device_class.to_string()));
707        }
708    }
709}
710
711/// Normalizes all the stack traces in the given event.
712///
713/// Normalized stack traces are `event.stacktrace`, `event.exceptions.stacktrace`, and
714/// `event.thread.stacktrace`. Raw stack traces are not normalized.
715fn normalize_stacktraces(event: &mut Event) {
716    normalize_event_stacktrace(event);
717    normalize_exception_stacktraces(event);
718    normalize_thread_stacktraces(event);
719}
720
721/// Normalizes an event's stack trace, in `event.stacktrace`.
722fn normalize_event_stacktrace(event: &mut Event) {
723    let Annotated(Some(stacktrace), meta) = &mut event.stacktrace else {
724        return;
725    };
726    stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
727}
728
729/// Normalizes the stack traces in an event's exceptions, in `event.exceptions.stacktraces`.
730///
731/// Note: the raw stack traces, in `event.exceptions.raw_stacktraces` is not normalized.
732fn normalize_exception_stacktraces(event: &mut Event) {
733    let Some(event_exception) = event.exceptions.value_mut() else {
734        return;
735    };
736    let Some(exceptions) = event_exception.values.value_mut() else {
737        return;
738    };
739    for annotated_exception in exceptions {
740        let Some(exception) = annotated_exception.value_mut() else {
741            continue;
742        };
743        if let Annotated(Some(stacktrace), meta) = &mut exception.stacktrace {
744            stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
745        }
746    }
747}
748
749/// Normalizes the stack traces in an event's threads, in `event.threads.stacktraces`.
750///
751/// Note: the raw stack traces, in `event.threads.raw_stacktraces`, is not normalized.
752fn normalize_thread_stacktraces(event: &mut Event) {
753    let Some(event_threads) = event.threads.value_mut() else {
754        return;
755    };
756    let Some(threads) = event_threads.values.value_mut() else {
757        return;
758    };
759    for annotated_thread in threads {
760        let Some(thread) = annotated_thread.value_mut() else {
761            continue;
762        };
763        if let Annotated(Some(stacktrace), meta) = &mut thread.stacktrace {
764            stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
765        }
766    }
767}
768
769fn normalize_exceptions(event: &mut Event) {
770    let os_hint = mechanism::OsHint::from_event(event);
771
772    if let Some(exception_values) = event.exceptions.value_mut() {
773        if let Some(exceptions) = exception_values.values.value_mut() {
774            if exceptions.len() == 1 && event.stacktrace.value().is_some() {
775                if let Some(exception) = exceptions.get_mut(0) {
776                    if let Some(exception) = exception.value_mut() {
777                        mem::swap(&mut exception.stacktrace, &mut event.stacktrace);
778                        event.stacktrace = Annotated::empty();
779                    }
780                }
781            }
782
783            // Exception mechanism needs SDK information to resolve proper names in
784            // exception meta (such as signal names). "SDK Information" really means
785            // the operating system version the event was generated on. Some
786            // normalization still works without sdk_info, such as mach_exception
787            // names (they can only occur on macOS).
788            //
789            // We also want to validate some other aspects of it.
790            for exception in exceptions {
791                normalize_exception(exception);
792                if let Some(exception) = exception.value_mut() {
793                    if let Some(mechanism) = exception.mechanism.value_mut() {
794                        mechanism::normalize_mechanism(mechanism, os_hint);
795                    }
796                }
797            }
798        }
799    }
800}
801
802fn normalize_exception(exception: &mut Annotated<Exception>) {
803    static TYPE_VALUE_RE: OnceLock<Regex> = OnceLock::new();
804    let regex = TYPE_VALUE_RE.get_or_init(|| Regex::new(r"^(\w+):(.*)$").unwrap());
805
806    let _ = processor::apply(exception, |exception, meta| {
807        if exception.ty.value().is_empty() {
808            if let Some(value_str) = exception.value.value_mut() {
809                let new_values = regex
810                    .captures(value_str)
811                    .map(|cap| (cap[1].to_string(), cap[2].trim().to_owned().into()));
812
813                if let Some((new_type, new_value)) = new_values {
814                    exception.ty.set_value(Some(new_type));
815                    *value_str = new_value;
816                }
817            }
818        }
819
820        if exception.ty.value().is_empty() && exception.value.value().is_empty() {
821            meta.add_error(Error::with(ErrorKind::MissingAttribute, |error| {
822                error.insert("attribute", "type or value");
823            }));
824            return Err(ProcessingAction::DeleteValueSoft);
825        }
826
827        Ok(())
828    });
829}
830
831fn normalize_user_agent(_event: &mut Event, normalize_user_agent: Option<bool>) {
832    if normalize_user_agent.unwrap_or(false) {
833        user_agent::normalize_user_agent(_event);
834    }
835}
836
837/// Ensures measurements interface is only present for transaction events.
838fn normalize_event_measurements(
839    event: &mut Event,
840    measurements_config: Option<CombinedMeasurementsConfig>,
841    max_mri_len: Option<usize>,
842) {
843    if event.ty.value() != Some(&EventType::Transaction) {
844        // Only transaction events may have a measurements interface
845        event.measurements = Annotated::empty();
846    } else if let Annotated(Some(ref mut measurements), ref mut meta) = event.measurements {
847        normalize_measurements(
848            measurements,
849            meta,
850            measurements_config,
851            max_mri_len,
852            event.start_timestamp.0,
853            event.timestamp.0,
854        );
855    }
856}
857
858/// Ensure only valid measurements are ingested.
859pub fn normalize_measurements(
860    measurements: &mut Measurements,
861    meta: &mut Meta,
862    measurements_config: Option<CombinedMeasurementsConfig>,
863    max_mri_len: Option<usize>,
864    start_timestamp: Option<Timestamp>,
865    end_timestamp: Option<Timestamp>,
866) {
867    normalize_mobile_measurements(measurements);
868    normalize_units(measurements);
869
870    let duration_millis = start_timestamp.zip(end_timestamp).and_then(|(start, end)| {
871        FiniteF64::new(relay_common::time::chrono_to_positive_millis(end - start))
872    });
873
874    compute_measurements(duration_millis, measurements);
875    if let Some(measurements_config) = measurements_config {
876        remove_invalid_measurements(measurements, meta, measurements_config, max_mri_len);
877    }
878}
879
880pub trait MutMeasurements {
881    fn measurements(&mut self) -> &mut Annotated<Measurements>;
882}
883
884/// Computes performance score measurements for an event.
885///
886/// This computes score from vital measurements, using config options to define how it is
887/// calculated.
888pub fn normalize_performance_score(
889    event: &mut (impl Getter + MutMeasurements),
890    performance_score: Option<&PerformanceScoreConfig>,
891) -> Option<String> {
892    let mut version = None;
893    let Some(performance_score) = performance_score else {
894        return version;
895    };
896    for profile in &performance_score.profiles {
897        if let Some(condition) = &profile.condition {
898            if !condition.matches(event) {
899                continue;
900            }
901            if let Some(measurements) = event.measurements().value_mut() {
902                let mut should_add_total = false;
903                if profile.score_components.iter().any(|c| {
904                    !measurements.contains_key(c.measurement.as_str())
905                        && c.weight.abs() >= f64::EPSILON
906                        && !c.optional
907                }) {
908                    // All non-optional measurements with a profile weight greater than 0 are
909                    // required to exist on the event. Skip this profile if
910                    // a measurement with weight is missing.
911                    continue;
912                }
913                let mut score_total = FiniteF64::ZERO;
914                let mut weight_total = FiniteF64::ZERO;
915                for component in &profile.score_components {
916                    // Skip optional components if they are not present on the event.
917                    if component.optional
918                        && !measurements.contains_key(component.measurement.as_str())
919                    {
920                        continue;
921                    }
922                    weight_total += component.weight;
923                }
924                if weight_total.abs() < FiniteF64::EPSILON {
925                    // All components are optional or have a weight of `0`. We cannot compute
926                    // component weights, so we bail.
927                    continue;
928                }
929                for component in &profile.score_components {
930                    // Optional measurements that are not present are given a weight of 0.
931                    let mut normalized_component_weight = FiniteF64::ZERO;
932
933                    if let Some(value) = measurements.get_value(component.measurement.as_str()) {
934                        normalized_component_weight = component.weight.saturating_div(weight_total);
935                        let cdf = utils::calculate_cdf_score(
936                            value.to_f64().max(0.0), // Webvitals can't be negative, but we need to clamp in case of bad data.
937                            component.p10.to_f64(),
938                            component.p50.to_f64(),
939                        );
940
941                        let cdf = Annotated::try_from(cdf);
942
943                        measurements.insert(
944                            format!("score.ratio.{}", component.measurement),
945                            Measurement {
946                                value: cdf.clone(),
947                                unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
948                            }
949                            .into(),
950                        );
951
952                        let component_score =
953                            cdf.and_then(|cdf| match cdf * normalized_component_weight {
954                                Some(v) => Annotated::new(v),
955                                None => Annotated::from_error(TryFromFloatError, None),
956                            });
957
958                        if let Some(component_score) = component_score.value() {
959                            score_total += *component_score;
960                            should_add_total = true;
961                        }
962
963                        measurements.insert(
964                            format!("score.{}", component.measurement),
965                            Measurement {
966                                value: component_score,
967                                unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
968                            }
969                            .into(),
970                        );
971                    }
972
973                    measurements.insert(
974                        format!("score.weight.{}", component.measurement),
975                        Measurement {
976                            value: normalized_component_weight.into(),
977                            unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
978                        }
979                        .into(),
980                    );
981                }
982                if should_add_total {
983                    version.clone_from(&profile.version);
984                    measurements.insert(
985                        "score.total".to_owned(),
986                        Measurement {
987                            value: score_total.into(),
988                            unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
989                        }
990                        .into(),
991                    );
992                }
993            }
994            break; // Stop after the first matching profile.
995        }
996    }
997    version
998}
999
1000// Extracts lcp related tags from the trace context.
1001fn normalize_trace_context_tags(event: &mut Event) {
1002    let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
1003    if let Some(contexts) = event.contexts.value() {
1004        if let Some(trace_context) = contexts.get::<TraceContext>() {
1005            if let Some(data) = trace_context.data.value() {
1006                if let Some(lcp_element) = data.lcp_element.value() {
1007                    if !tags.contains("lcp.element") {
1008                        let tag_name = "lcp.element".to_owned();
1009                        tags.insert(tag_name, Annotated::new(lcp_element.clone()));
1010                    }
1011                }
1012                if let Some(lcp_size) = data.lcp_size.value() {
1013                    if !tags.contains("lcp.size") {
1014                        let tag_name = "lcp.size".to_owned();
1015                        tags.insert(tag_name, Annotated::new(lcp_size.to_string()));
1016                    }
1017                }
1018                if let Some(lcp_id) = data.lcp_id.value() {
1019                    let tag_name = "lcp.id".to_owned();
1020                    if !tags.contains("lcp.id") {
1021                        tags.insert(tag_name, Annotated::new(lcp_id.clone()));
1022                    }
1023                }
1024                if let Some(lcp_url) = data.lcp_url.value() {
1025                    let tag_name = "lcp.url".to_owned();
1026                    if !tags.contains("lcp.url") {
1027                        tags.insert(tag_name, Annotated::new(lcp_url.clone()));
1028                    }
1029                }
1030            }
1031        }
1032    }
1033}
1034
1035impl MutMeasurements for Event {
1036    fn measurements(&mut self) -> &mut Annotated<Measurements> {
1037        &mut self.measurements
1038    }
1039}
1040
1041impl MutMeasurements for Span {
1042    fn measurements(&mut self) -> &mut Annotated<Measurements> {
1043        &mut self.measurements
1044    }
1045}
1046
1047/// Compute additional measurements derived from existing ones.
1048///
1049/// The added measurements are:
1050///
1051/// ```text
1052/// frames_slow_rate := measurements.frames_slow / measurements.frames_total
1053/// frames_frozen_rate := measurements.frames_frozen / measurements.frames_total
1054/// stall_percentage := measurements.stall_total_time / transaction.duration
1055/// ```
1056fn compute_measurements(
1057    transaction_duration_ms: Option<FiniteF64>,
1058    measurements: &mut Measurements,
1059) {
1060    if let Some(frames_total) = measurements.get_value("frames_total") {
1061        if frames_total > 0.0 {
1062            if let Some(frames_frozen) = measurements.get_value("frames_frozen") {
1063                let frames_frozen_rate = Measurement {
1064                    value: (frames_frozen / frames_total).into(),
1065                    unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
1066                };
1067                measurements.insert("frames_frozen_rate".to_owned(), frames_frozen_rate.into());
1068            }
1069            if let Some(frames_slow) = measurements.get_value("frames_slow") {
1070                let frames_slow_rate = Measurement {
1071                    value: (frames_slow / frames_total).into(),
1072                    unit: MetricUnit::Fraction(FractionUnit::Ratio).into(),
1073                };
1074                measurements.insert("frames_slow_rate".to_owned(), frames_slow_rate.into());
1075            }
1076        }
1077    }
1078
1079    // Get stall_percentage
1080    if let Some(transaction_duration_ms) = transaction_duration_ms {
1081        if transaction_duration_ms > 0.0 {
1082            if let Some(stall_total_time) = measurements
1083                .get("stall_total_time")
1084                .and_then(Annotated::value)
1085            {
1086                if matches!(
1087                    stall_total_time.unit.value(),
1088                    // Accept milliseconds or None, but not other units
1089                    Some(&MetricUnit::Duration(DurationUnit::MilliSecond) | &MetricUnit::None)
1090                        | None
1091                ) {
1092                    if let Some(stall_total_time) = stall_total_time.value.0 {
1093                        let stall_percentage = Measurement {
1094                            value: (stall_total_time / transaction_duration_ms).into(),
1095                            unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
1096                        };
1097                        measurements.insert("stall_percentage".to_owned(), stall_percentage.into());
1098                    }
1099                }
1100            }
1101        }
1102    }
1103}
1104
1105/// Emit any breakdowns
1106fn normalize_breakdowns(event: &mut Event, breakdowns_config: Option<&BreakdownsConfig>) {
1107    match breakdowns_config {
1108        None => {}
1109        Some(config) => breakdowns::normalize_breakdowns(event, config),
1110    }
1111}
1112
1113fn normalize_default_attributes(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
1114    let event_type = infer_event_type(event);
1115    event.ty = Annotated::from(event_type);
1116    event.project = Annotated::from(config.project_id);
1117    event.key_id = Annotated::from(config.key_id.clone());
1118    event.version = Annotated::from(config.protocol_version.clone());
1119    event.grouping_config = config
1120        .grouping_config
1121        .clone()
1122        .map_or(Annotated::empty(), |x| {
1123            FromValue::from_value(Annotated::<Value>::from(x))
1124        });
1125
1126    let _ = relay_event_schema::processor::apply(&mut event.platform, |platform, _| {
1127        if is_valid_platform(platform) {
1128            Ok(())
1129        } else {
1130            Err(ProcessingAction::DeleteValueSoft)
1131        }
1132    });
1133
1134    // Default required attributes, even if they have errors
1135    event.errors.get_or_insert_with(Vec::new);
1136    event.id.get_or_insert_with(EventId::new);
1137    event.platform.get_or_insert_with(|| "other".to_owned());
1138    event.logger.get_or_insert_with(String::new);
1139    event.extra.get_or_insert_with(Object::new);
1140    event.level.get_or_insert_with(|| match event_type {
1141        EventType::Transaction => Level::Info,
1142        _ => Level::Error,
1143    });
1144    if event.client_sdk.value().is_none() {
1145        event.client_sdk.set_value(get_sdk_info(config));
1146    }
1147
1148    if event.platform.as_str() == Some("java") {
1149        if let Some(event_logger) = event.logger.value_mut().take() {
1150            let shortened = shorten_logger(event_logger, meta);
1151            event.logger.set_value(Some(shortened));
1152        }
1153    }
1154}
1155
1156/// Returns `true` if the given platform string is a known platform identifier.
1157///
1158/// See [`VALID_PLATFORMS`] for a list of all known platforms.
1159pub fn is_valid_platform(platform: &str) -> bool {
1160    VALID_PLATFORMS.contains(&platform)
1161}
1162
1163/// Infers the `EventType` from the event's interfaces.
1164fn infer_event_type(event: &Event) -> EventType {
1165    // The event type may be set explicitly when constructing the event items from specific
1166    // items. This is DEPRECATED, and each distinct event type may get its own base class. For
1167    // the time being, this is only implemented for transactions, so be specific:
1168    if event.ty.value() == Some(&EventType::Transaction) {
1169        return EventType::Transaction;
1170    }
1171    if event.ty.value() == Some(&EventType::UserReportV2) {
1172        return EventType::UserReportV2;
1173    }
1174
1175    // The SDKs do not describe event types, and we must infer them from available attributes.
1176    let has_exceptions = event
1177        .exceptions
1178        .value()
1179        .and_then(|exceptions| exceptions.values.value())
1180        .filter(|values| !values.is_empty())
1181        .is_some();
1182
1183    if has_exceptions {
1184        EventType::Error
1185    } else if event.csp.value().is_some() {
1186        EventType::Csp
1187    } else if event.hpkp.value().is_some() {
1188        EventType::Hpkp
1189    } else if event.expectct.value().is_some() {
1190        EventType::ExpectCt
1191    } else if event.expectstaple.value().is_some() {
1192        EventType::ExpectStaple
1193    } else if event.context::<NelContext>().is_some() {
1194        EventType::Nel
1195    } else {
1196        EventType::Default
1197    }
1198}
1199
1200/// Returns the SDK info from the config.
1201fn get_sdk_info(config: &NormalizationConfig) -> Option<ClientSdkInfo> {
1202    config.client.as_ref().and_then(|client| {
1203        client
1204            .splitn(2, '/')
1205            .collect_tuple()
1206            .or_else(|| client.splitn(2, ' ').collect_tuple())
1207            .map(|(name, version)| ClientSdkInfo {
1208                name: Annotated::new(name.to_owned()),
1209                version: Annotated::new(version.to_owned()),
1210                ..Default::default()
1211            })
1212    })
1213}
1214
1215/// If the logger is longer than [`MaxChars::Logger`], it returns a String with
1216/// a shortened version of the logger. If not, the same logger is returned as a
1217/// String. The resulting logger is always trimmed.
1218///
1219/// To shorten the logger, all extra chars that don't fit into the maximum limit
1220/// are removed, from the beginning of the logger.  Then, if the remaining
1221/// substring contains a `.` somewhere but in the end, all chars until `.`
1222/// (exclusive) are removed.
1223///
1224/// Additionally, the new logger is prefixed with `*`, to indicate it was
1225/// shortened.
1226fn shorten_logger(logger: String, meta: &mut Meta) -> String {
1227    let original_len = bytecount::num_chars(logger.as_bytes());
1228    let trimmed = logger.trim();
1229    let logger_len = bytecount::num_chars(trimmed.as_bytes());
1230    if logger_len <= MaxChars::Logger.limit() {
1231        if trimmed == logger {
1232            return logger;
1233        } else {
1234            if trimmed.is_empty() {
1235                meta.add_remark(Remark {
1236                    ty: RemarkType::Removed,
1237                    rule_id: "@logger:remove".to_owned(),
1238                    range: Some((0, original_len)),
1239                });
1240            } else {
1241                meta.add_remark(Remark {
1242                    ty: RemarkType::Substituted,
1243                    rule_id: "@logger:trim".to_owned(),
1244                    range: None,
1245                });
1246            }
1247            meta.set_original_length(Some(original_len));
1248            return trimmed.to_owned();
1249        };
1250    }
1251
1252    let mut tokens = trimmed.split("").collect_vec();
1253    // Remove empty str tokens from the beginning and end.
1254    tokens.pop();
1255    tokens.reverse(); // Prioritize chars from the end of the string.
1256    tokens.pop();
1257
1258    let word_cut = remove_logger_extra_chars(&mut tokens);
1259    if word_cut {
1260        remove_logger_word(&mut tokens);
1261    }
1262
1263    tokens.reverse();
1264    meta.add_remark(Remark {
1265        ty: RemarkType::Substituted,
1266        rule_id: "@logger:replace".to_owned(),
1267        range: Some((0, logger_len - tokens.len())),
1268    });
1269    meta.set_original_length(Some(original_len));
1270
1271    format!("*{}", tokens.join(""))
1272}
1273
1274/// Remove as many tokens as needed to match the maximum char limit defined in
1275/// [`MaxChars::Logger`], and an extra token for the logger prefix. Returns
1276/// whether a word has been cut.
1277///
1278/// A word is considered any non-empty substring that doesn't contain a `.`.
1279fn remove_logger_extra_chars(tokens: &mut Vec<&str>) -> bool {
1280    // Leave one slot of space for the prefix
1281    let mut remove_chars = tokens.len() - MaxChars::Logger.limit() + 1;
1282    let mut word_cut = false;
1283    while remove_chars > 0 {
1284        if let Some(c) = tokens.pop() {
1285            if !word_cut && c != "." {
1286                word_cut = true;
1287            } else if word_cut && c == "." {
1288                word_cut = false;
1289            }
1290        }
1291        remove_chars -= 1;
1292    }
1293    word_cut
1294}
1295
1296/// If the `.` token is present, removes all tokens from the end of the vector
1297/// until `.`. If it isn't present, nothing is removed.
1298fn remove_logger_word(tokens: &mut Vec<&str>) {
1299    let mut delimiter_found = false;
1300    for token in tokens.iter() {
1301        if *token == "." {
1302            delimiter_found = true;
1303            break;
1304        }
1305    }
1306    if !delimiter_found {
1307        return;
1308    }
1309    while let Some(i) = tokens.last() {
1310        if *i == "." {
1311            break;
1312        }
1313        tokens.pop();
1314    }
1315}
1316
1317/// Normalizes incoming contexts for the downstream metric extraction.
1318fn normalize_contexts(contexts: &mut Annotated<Contexts>) {
1319    let _ = processor::apply(contexts, |contexts, _meta| {
1320        // Reprocessing context sent from SDKs must not be accepted, it is a Sentry-internal
1321        // construct.
1322        // [`normalize`] does not run on renormalization anyway.
1323        contexts.0.remove("reprocessing");
1324
1325        for annotated in &mut contexts.0.values_mut() {
1326            if let Some(ContextInner(Context::Trace(context))) = annotated.value_mut() {
1327                context.status.get_or_insert_with(|| SpanStatus::Unknown);
1328            }
1329            if let Some(context_inner) = annotated.value_mut() {
1330                crate::normalize::contexts::normalize_context(&mut context_inner.0);
1331            }
1332        }
1333
1334        Ok(())
1335    });
1336}
1337
1338/// New SDKs do not send measurements when they exceed 180 seconds.
1339///
1340/// Drop those outlier measurements for older SDKs.
1341fn filter_mobile_outliers(measurements: &mut Measurements) {
1342    for key in [
1343        "app_start_cold",
1344        "app_start_warm",
1345        "time_to_initial_display",
1346        "time_to_full_display",
1347    ] {
1348        if let Some(value) = measurements.get_value(key) {
1349            if value > MAX_DURATION_MOBILE_MS {
1350                measurements.remove(key);
1351            }
1352        }
1353    }
1354}
1355
1356fn normalize_mobile_measurements(measurements: &mut Measurements) {
1357    normalize_app_start_measurements(measurements);
1358    filter_mobile_outliers(measurements);
1359}
1360
1361fn normalize_units(measurements: &mut Measurements) {
1362    for (name, measurement) in measurements.iter_mut() {
1363        let measurement = match measurement.value_mut() {
1364            Some(m) => m,
1365            None => continue,
1366        };
1367
1368        let stated_unit = measurement.unit.value().copied();
1369        let default_unit = get_metric_measurement_unit(name);
1370        measurement
1371            .unit
1372            .set_value(Some(stated_unit.or(default_unit).unwrap_or_default()))
1373    }
1374}
1375
1376/// Remove measurements that do not conform to the given config.
1377///
1378/// Built-in measurements are accepted if their unit is correct, dropped otherwise.
1379/// Custom measurements are accepted up to a limit.
1380///
1381/// Note that [`Measurements`] is a BTreeMap, which means its keys are sorted.
1382/// This ensures that for two events with the same measurement keys, the same set of custom
1383/// measurements is retained.
1384fn remove_invalid_measurements(
1385    measurements: &mut Measurements,
1386    meta: &mut Meta,
1387    measurements_config: CombinedMeasurementsConfig,
1388    max_name_and_unit_len: Option<usize>,
1389) {
1390    let max_custom_measurements = measurements_config.max_custom_measurements().unwrap_or(0);
1391
1392    let mut custom_measurements_count = 0;
1393    let mut removed_measurements = Object::new();
1394
1395    measurements.retain(|name, value| {
1396        let measurement = match value.value_mut() {
1397            Some(m) => m,
1398            None => return false,
1399        };
1400
1401        if !can_be_valid_metric_name(name) {
1402            meta.add_error(Error::invalid(format!(
1403                "Metric name contains invalid characters: \"{name}\""
1404            )));
1405            removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1406            return false;
1407        }
1408
1409        // TODO(jjbayer): Should we actually normalize the unit into the event?
1410        let unit = measurement.unit.value().unwrap_or(&MetricUnit::None);
1411
1412        if let Some(max_name_and_unit_len) = max_name_and_unit_len {
1413            let max_name_len = max_name_and_unit_len - unit.to_string().len();
1414
1415            if name.len() > max_name_len {
1416                meta.add_error(Error::invalid(format!(
1417                    "Metric name too long {}/{max_name_len}: \"{name}\"",
1418                    name.len(),
1419                )));
1420                removed_measurements
1421                    .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1422                return false;
1423            }
1424        }
1425
1426        // Check if this is a builtin measurement:
1427        for builtin_measurement in measurements_config.builtin_measurement_keys() {
1428            if builtin_measurement.name() == name {
1429                let value = measurement.value.value().unwrap_or(&FiniteF64::ZERO);
1430                // Drop negative values if the builtin measurement does not allow them.
1431                if !builtin_measurement.allow_negative() && *value < 0.0 {
1432                    meta.add_error(Error::invalid(format!(
1433                        "Negative value for measurement {name} not allowed: {value}",
1434                    )));
1435                    removed_measurements
1436                        .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1437                    return false;
1438                }
1439                // If the unit matches a built-in measurement, we allow it.
1440                // If the name matches but the unit is wrong, we do not even accept it as a custom measurement,
1441                // and just drop it instead.
1442                return builtin_measurement.unit() == unit;
1443            }
1444        }
1445
1446        // For custom measurements, check the budget:
1447        if custom_measurements_count < max_custom_measurements {
1448            custom_measurements_count += 1;
1449            return true;
1450        }
1451
1452        meta.add_error(Error::invalid(format!("Too many measurements: {name}")));
1453        removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1454
1455        false
1456    });
1457
1458    if !removed_measurements.is_empty() {
1459        meta.set_original_value(Some(removed_measurements));
1460    }
1461}
1462
1463/// Returns the unit of the provided metric.
1464///
1465/// For known measurements, this returns `Some(MetricUnit)`, which can also include
1466/// `Some(MetricUnit::None)`. For unknown measurement names, this returns `None`.
1467fn get_metric_measurement_unit(measurement_name: &str) -> Option<MetricUnit> {
1468    match measurement_name {
1469        // Web
1470        "fcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1471        "lcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1472        "fid" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1473        "fp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1474        "inp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1475        "ttfb" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1476        "ttfb.requesttime" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1477        "cls" => Some(MetricUnit::None),
1478
1479        // Mobile
1480        "app_start_cold" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1481        "app_start_warm" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1482        "frames_total" => Some(MetricUnit::None),
1483        "frames_slow" => Some(MetricUnit::None),
1484        "frames_slow_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1485        "frames_frozen" => Some(MetricUnit::None),
1486        "frames_frozen_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1487        "time_to_initial_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1488        "time_to_full_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1489
1490        // React-Native
1491        "stall_count" => Some(MetricUnit::None),
1492        "stall_total_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1493        "stall_longest_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1494        "stall_percentage" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1495
1496        // Default
1497        _ => None,
1498    }
1499}
1500
1501/// Replaces dot.case app start measurements keys with snake_case keys.
1502///
1503/// The dot.case app start measurements keys are treated as custom measurements.
1504/// The snake_case is the key expected by the Sentry UI to aggregate and display in graphs.
1505fn normalize_app_start_measurements(measurements: &mut Measurements) {
1506    if let Some(app_start_cold_value) = measurements.remove("app.start.cold") {
1507        measurements.insert("app_start_cold".to_owned(), app_start_cold_value);
1508    }
1509    if let Some(app_start_warm_value) = measurements.remove("app.start.warm") {
1510        measurements.insert("app_start_warm".to_owned(), app_start_warm_value);
1511    }
1512}
1513
1514#[cfg(test)]
1515mod tests {
1516
1517    use std::collections::BTreeMap;
1518    use std::collections::HashMap;
1519
1520    use insta::assert_debug_snapshot;
1521    use itertools::Itertools;
1522    use relay_event_schema::protocol::{Breadcrumb, Csp, DebugMeta, DeviceContext, Values};
1523    use relay_protocol::{SerializableAnnotated, get_value};
1524    use serde_json::json;
1525
1526    use super::*;
1527    use crate::{ClientHints, MeasurementsConfig, ModelCostV2};
1528
1529    const IOS_MOBILE_EVENT: &str = r#"
1530        {
1531            "sdk": {"name": "sentry.cocoa"},
1532            "contexts": {
1533                "trace": {
1534                    "op": "ui.load"
1535                }
1536            },
1537            "measurements": {
1538                "app_start_warm": {
1539                    "value": 8049.345970153808,
1540                    "unit": "millisecond"
1541                },
1542                "time_to_full_display": {
1543                    "value": 8240.571022033691,
1544                    "unit": "millisecond"
1545                },
1546                "time_to_initial_display": {
1547                    "value": 8049.345970153808,
1548                    "unit": "millisecond"
1549                }
1550            }
1551        }
1552        "#;
1553
1554    const ANDROID_MOBILE_EVENT: &str = r#"
1555        {
1556            "sdk": {"name": "sentry.java.android"},
1557            "contexts": {
1558                "trace": {
1559                    "op": "ui.load"
1560                }
1561            },
1562            "measurements": {
1563                "app_start_cold": {
1564                    "value": 22648,
1565                    "unit": "millisecond"
1566                },
1567                "time_to_full_display": {
1568                    "value": 22647,
1569                    "unit": "millisecond"
1570                },
1571                "time_to_initial_display": {
1572                    "value": 22647,
1573                    "unit": "millisecond"
1574                }
1575            }
1576        }
1577        "#;
1578
1579    #[test]
1580    fn test_normalize_dist_none() {
1581        let mut dist = Annotated::default();
1582        normalize_dist(&mut dist);
1583        assert_eq!(dist.value(), None);
1584    }
1585
1586    #[test]
1587    fn test_normalize_dist_empty() {
1588        let mut dist = Annotated::new("".to_owned());
1589        normalize_dist(&mut dist);
1590        assert_eq!(dist.value(), None);
1591    }
1592
1593    #[test]
1594    fn test_normalize_dist_trim() {
1595        let mut dist = Annotated::new(" foo  ".to_owned());
1596        normalize_dist(&mut dist);
1597        assert_eq!(dist.value(), Some(&"foo".to_owned()));
1598    }
1599
1600    #[test]
1601    fn test_normalize_dist_whitespace() {
1602        let mut dist = Annotated::new(" ".to_owned());
1603        normalize_dist(&mut dist);
1604        assert_eq!(dist.value(), None);
1605    }
1606
1607    #[test]
1608    fn test_normalize_platform_and_level_with_transaction_event() {
1609        let json = r#"
1610        {
1611            "type": "transaction"
1612        }
1613        "#;
1614
1615        let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1616        else {
1617            panic!("Invalid transaction json");
1618        };
1619
1620        normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1621
1622        assert_eq!(event.level.value().unwrap().to_string(), "info");
1623        assert_eq!(event.ty.value().unwrap().to_string(), "transaction");
1624        assert_eq!(event.platform.as_str().unwrap(), "other");
1625    }
1626
1627    #[test]
1628    fn test_normalize_platform_and_level_with_error_event() {
1629        let json = r#"
1630        {
1631            "type": "error",
1632            "exception": {
1633                "values": [{"type": "ValueError", "value": "Should not happen"}]
1634            }
1635        }
1636        "#;
1637
1638        let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1639        else {
1640            panic!("Invalid error json");
1641        };
1642
1643        normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1644
1645        assert_eq!(event.level.value().unwrap().to_string(), "error");
1646        assert_eq!(event.ty.value().unwrap().to_string(), "error");
1647        assert_eq!(event.platform.value().unwrap().to_owned(), "other");
1648    }
1649
1650    #[test]
1651    fn test_computed_measurements() {
1652        let json = r#"
1653        {
1654            "type": "transaction",
1655            "timestamp": "2021-04-26T08:00:05+0100",
1656            "start_timestamp": "2021-04-26T08:00:00+0100",
1657            "measurements": {
1658                "frames_slow": {"value": 1},
1659                "frames_frozen": {"value": 2},
1660                "frames_total": {"value": 4},
1661                "stall_total_time": {"value": 4000, "unit": "millisecond"}
1662            }
1663        }
1664        "#;
1665
1666        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1667
1668        normalize_event_measurements(&mut event, None, None);
1669
1670        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
1671        {
1672          "type": "transaction",
1673          "timestamp": 1619420405.0,
1674          "start_timestamp": 1619420400.0,
1675          "measurements": {
1676            "frames_frozen": {
1677              "value": 2.0,
1678              "unit": "none",
1679            },
1680            "frames_frozen_rate": {
1681              "value": 0.5,
1682              "unit": "ratio",
1683            },
1684            "frames_slow": {
1685              "value": 1.0,
1686              "unit": "none",
1687            },
1688            "frames_slow_rate": {
1689              "value": 0.25,
1690              "unit": "ratio",
1691            },
1692            "frames_total": {
1693              "value": 4.0,
1694              "unit": "none",
1695            },
1696            "stall_percentage": {
1697              "value": 0.8,
1698              "unit": "ratio",
1699            },
1700            "stall_total_time": {
1701              "value": 4000.0,
1702              "unit": "millisecond",
1703            },
1704          },
1705        }
1706        "#);
1707    }
1708
1709    #[test]
1710    fn test_filter_custom_measurements() {
1711        let json = r#"
1712        {
1713            "type": "transaction",
1714            "timestamp": "2021-04-26T08:00:05+0100",
1715            "start_timestamp": "2021-04-26T08:00:00+0100",
1716            "measurements": {
1717                "my_custom_measurement_1": {"value": 123},
1718                "frames_frozen": {"value": 666, "unit": "invalid_unit"},
1719                "frames_slow": {"value": 1},
1720                "my_custom_measurement_3": {"value": 456},
1721                "my_custom_measurement_2": {"value": 789}
1722            }
1723        }
1724        "#;
1725        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1726
1727        let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
1728            "builtinMeasurements": [
1729                {"name": "frames_frozen", "unit": "none"},
1730                {"name": "frames_slow", "unit": "none"}
1731            ],
1732            "maxCustomMeasurements": 2,
1733            "stray_key": "zzz"
1734        }))
1735        .unwrap();
1736
1737        let dynamic_measurement_config =
1738            CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
1739
1740        normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
1741
1742        // Only two custom measurements are retained, in alphabetic order (1 and 2)
1743        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
1744        {
1745          "type": "transaction",
1746          "timestamp": 1619420405.0,
1747          "start_timestamp": 1619420400.0,
1748          "measurements": {
1749            "frames_slow": {
1750              "value": 1.0,
1751              "unit": "none",
1752            },
1753            "my_custom_measurement_1": {
1754              "value": 123.0,
1755              "unit": "none",
1756            },
1757            "my_custom_measurement_2": {
1758              "value": 789.0,
1759              "unit": "none",
1760            },
1761          },
1762          "_meta": {
1763            "measurements": {
1764              "": Meta(Some(MetaInner(
1765                err: [
1766                  [
1767                    "invalid_data",
1768                    {
1769                      "reason": "Too many measurements: my_custom_measurement_3",
1770                    },
1771                  ],
1772                ],
1773                val: Some({
1774                  "my_custom_measurement_3": {
1775                    "unit": "none",
1776                    "value": 456.0,
1777                  },
1778                }),
1779              ))),
1780            },
1781          },
1782        }
1783        "#);
1784    }
1785
1786    #[test]
1787    fn test_normalize_units() {
1788        let mut measurements = Annotated::<Measurements>::from_json(
1789            r#"{
1790                "fcp": {"value": 1.1},
1791                "stall_count": {"value": 3.3},
1792                "foo": {"value": 8.8}
1793            }"#,
1794        )
1795        .unwrap()
1796        .into_value()
1797        .unwrap();
1798        insta::assert_debug_snapshot!(measurements, @r#"
1799        Measurements(
1800            {
1801                "fcp": Measurement {
1802                    value: 1.1,
1803                    unit: ~,
1804                },
1805                "foo": Measurement {
1806                    value: 8.8,
1807                    unit: ~,
1808                },
1809                "stall_count": Measurement {
1810                    value: 3.3,
1811                    unit: ~,
1812                },
1813            },
1814        )
1815        "#);
1816        normalize_units(&mut measurements);
1817        insta::assert_debug_snapshot!(measurements, @r#"
1818        Measurements(
1819            {
1820                "fcp": Measurement {
1821                    value: 1.1,
1822                    unit: Duration(
1823                        MilliSecond,
1824                    ),
1825                },
1826                "foo": Measurement {
1827                    value: 8.8,
1828                    unit: None,
1829                },
1830                "stall_count": Measurement {
1831                    value: 3.3,
1832                    unit: None,
1833                },
1834            },
1835        )
1836        "#);
1837    }
1838
1839    #[test]
1840    fn test_normalize_security_report() {
1841        let mut event = Event {
1842            csp: Annotated::from(Csp::default()),
1843            ..Default::default()
1844        };
1845        let ipaddr = IpAddr("213.164.1.114".to_owned());
1846
1847        let client_ip = Some(&ipaddr);
1848
1849        let user_agent = RawUserAgentInfo {
1850            user_agent: Some(
1851                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0",
1852            ),
1853            client_hints: ClientHints {
1854                sec_ch_ua_platform: Some("macOS"),
1855                sec_ch_ua_platform_version: Some("13.2.0"),
1856                sec_ch_ua: Some(
1857                    r#""Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110""#,
1858                ),
1859                sec_ch_ua_model: Some("some model"),
1860            },
1861        };
1862
1863        // This call should fill the event headers with info from the user_agent which is
1864        // tested below.
1865        normalize_security_report(&mut event, client_ip, &user_agent);
1866
1867        let headers = event
1868            .request
1869            .value_mut()
1870            .get_or_insert_with(Request::default)
1871            .headers
1872            .value_mut()
1873            .get_or_insert_with(Headers::default);
1874
1875        assert_eq!(
1876            event.user.value().unwrap().ip_address,
1877            Annotated::from(ipaddr)
1878        );
1879        assert_eq!(
1880            headers.get_header(RawUserAgentInfo::USER_AGENT),
1881            user_agent.user_agent
1882        );
1883        assert_eq!(
1884            headers.get_header(ClientHints::SEC_CH_UA),
1885            user_agent.client_hints.sec_ch_ua,
1886        );
1887        assert_eq!(
1888            headers.get_header(ClientHints::SEC_CH_UA_MODEL),
1889            user_agent.client_hints.sec_ch_ua_model,
1890        );
1891        assert_eq!(
1892            headers.get_header(ClientHints::SEC_CH_UA_PLATFORM),
1893            user_agent.client_hints.sec_ch_ua_platform,
1894        );
1895        assert_eq!(
1896            headers.get_header(ClientHints::SEC_CH_UA_PLATFORM_VERSION),
1897            user_agent.client_hints.sec_ch_ua_platform_version,
1898        );
1899
1900        assert!(
1901            std::mem::size_of_val(&ClientHints::<&str>::default()) == 64,
1902            "If you add new fields, update the test accordingly"
1903        );
1904    }
1905
1906    #[test]
1907    fn test_no_device_class() {
1908        let mut event = Event {
1909            ..Default::default()
1910        };
1911        normalize_device_class(&mut event);
1912        let tags = &event.tags.value_mut().get_or_insert_with(Tags::default).0;
1913        assert_eq!(None, tags.get("device_class"));
1914    }
1915
1916    #[test]
1917    fn test_apple_low_device_class() {
1918        let mut event = Event {
1919            contexts: {
1920                let mut contexts = Contexts::new();
1921                contexts.add(DeviceContext {
1922                    family: "iPhone".to_owned().into(),
1923                    model: "iPhone8,4".to_owned().into(),
1924                    ..Default::default()
1925                });
1926                Annotated::new(contexts)
1927            },
1928            ..Default::default()
1929        };
1930        normalize_device_class(&mut event);
1931        assert_debug_snapshot!(event.tags, @r#"
1932        Tags(
1933            PairList(
1934                [
1935                    TagEntry(
1936                        "device.class",
1937                        "1",
1938                    ),
1939                ],
1940            ),
1941        )
1942        "#);
1943    }
1944
1945    #[test]
1946    fn test_apple_medium_device_class() {
1947        let mut event = Event {
1948            contexts: {
1949                let mut contexts = Contexts::new();
1950                contexts.add(DeviceContext {
1951                    family: "iPhone".to_owned().into(),
1952                    model: "iPhone12,8".to_owned().into(),
1953                    ..Default::default()
1954                });
1955                Annotated::new(contexts)
1956            },
1957            ..Default::default()
1958        };
1959        normalize_device_class(&mut event);
1960        assert_debug_snapshot!(event.tags, @r#"
1961        Tags(
1962            PairList(
1963                [
1964                    TagEntry(
1965                        "device.class",
1966                        "2",
1967                    ),
1968                ],
1969            ),
1970        )
1971        "#);
1972    }
1973
1974    #[test]
1975    fn test_android_low_device_class() {
1976        let mut event = Event {
1977            contexts: {
1978                let mut contexts = Contexts::new();
1979                contexts.add(DeviceContext {
1980                    family: "android".to_owned().into(),
1981                    processor_frequency: 1000.into(),
1982                    processor_count: 6.into(),
1983                    memory_size: (2 * 1024 * 1024 * 1024).into(),
1984                    ..Default::default()
1985                });
1986                Annotated::new(contexts)
1987            },
1988            ..Default::default()
1989        };
1990        normalize_device_class(&mut event);
1991        assert_debug_snapshot!(event.tags, @r#"
1992        Tags(
1993            PairList(
1994                [
1995                    TagEntry(
1996                        "device.class",
1997                        "1",
1998                    ),
1999                ],
2000            ),
2001        )
2002        "#);
2003    }
2004
2005    #[test]
2006    fn test_android_medium_device_class() {
2007        let mut event = Event {
2008            contexts: {
2009                let mut contexts = Contexts::new();
2010                contexts.add(DeviceContext {
2011                    family: "android".to_owned().into(),
2012                    processor_frequency: 2000.into(),
2013                    processor_count: 8.into(),
2014                    memory_size: (6 * 1024 * 1024 * 1024).into(),
2015                    ..Default::default()
2016                });
2017                Annotated::new(contexts)
2018            },
2019            ..Default::default()
2020        };
2021        normalize_device_class(&mut event);
2022        assert_debug_snapshot!(event.tags, @r#"
2023        Tags(
2024            PairList(
2025                [
2026                    TagEntry(
2027                        "device.class",
2028                        "2",
2029                    ),
2030                ],
2031            ),
2032        )
2033        "#);
2034    }
2035
2036    #[test]
2037    fn test_android_high_device_class() {
2038        let mut event = Event {
2039            contexts: {
2040                let mut contexts = Contexts::new();
2041                contexts.add(DeviceContext {
2042                    family: "android".to_owned().into(),
2043                    processor_frequency: 2500.into(),
2044                    processor_count: 8.into(),
2045                    memory_size: (6 * 1024 * 1024 * 1024).into(),
2046                    ..Default::default()
2047                });
2048                Annotated::new(contexts)
2049            },
2050            ..Default::default()
2051        };
2052        normalize_device_class(&mut event);
2053        assert_debug_snapshot!(event.tags, @r#"
2054        Tags(
2055            PairList(
2056                [
2057                    TagEntry(
2058                        "device.class",
2059                        "3",
2060                    ),
2061                ],
2062            ),
2063        )
2064        "#);
2065    }
2066
2067    #[test]
2068    fn test_keeps_valid_measurement() {
2069        let name = "lcp";
2070        let measurement = Measurement {
2071            value: Annotated::new(420.69.try_into().unwrap()),
2072            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2073        };
2074
2075        assert!(!is_measurement_dropped(name, measurement));
2076    }
2077
2078    #[test]
2079    fn test_drops_too_long_measurement_names() {
2080        let name = "lcpppppppppppppppppppppppppppp";
2081        let measurement = Measurement {
2082            value: Annotated::new(420.69.try_into().unwrap()),
2083            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2084        };
2085
2086        assert!(is_measurement_dropped(name, measurement));
2087    }
2088
2089    #[test]
2090    fn test_drops_measurements_with_invalid_characters() {
2091        let name = "i æm frøm nørwåy";
2092        let measurement = Measurement {
2093            value: Annotated::new(420.69.try_into().unwrap()),
2094            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2095        };
2096
2097        assert!(is_measurement_dropped(name, measurement));
2098    }
2099
2100    fn is_measurement_dropped(name: &str, measurement: Measurement) -> bool {
2101        let max_name_and_unit_len = Some(30);
2102
2103        let mut measurements: BTreeMap<String, Annotated<Measurement>> = Object::new();
2104        measurements.insert(name.to_owned(), Annotated::new(measurement));
2105
2106        let mut measurements = Measurements(measurements);
2107        let mut meta = Meta::default();
2108        let measurements_config = MeasurementsConfig {
2109            max_custom_measurements: 1,
2110            ..Default::default()
2111        };
2112
2113        let dynamic_config = CombinedMeasurementsConfig::new(Some(&measurements_config), None);
2114
2115        // Just for clarity.
2116        // Checks that there is 1 measurement before processing.
2117        assert_eq!(measurements.len(), 1);
2118
2119        remove_invalid_measurements(
2120            &mut measurements,
2121            &mut meta,
2122            dynamic_config,
2123            max_name_and_unit_len,
2124        );
2125
2126        // Checks whether the measurement is dropped.
2127        measurements.is_empty()
2128    }
2129
2130    #[test]
2131    fn test_normalize_app_start_measurements_does_not_add_measurements() {
2132        let mut measurements = Annotated::<Measurements>::from_json(r###"{}"###)
2133            .unwrap()
2134            .into_value()
2135            .unwrap();
2136        insta::assert_debug_snapshot!(measurements, @r###"
2137        Measurements(
2138            {},
2139        )
2140        "###);
2141        normalize_app_start_measurements(&mut measurements);
2142        insta::assert_debug_snapshot!(measurements, @r###"
2143        Measurements(
2144            {},
2145        )
2146        "###);
2147    }
2148
2149    #[test]
2150    fn test_normalize_app_start_cold_measurements() {
2151        let mut measurements =
2152            Annotated::<Measurements>::from_json(r#"{"app.start.cold": {"value": 1.1}}"#)
2153                .unwrap()
2154                .into_value()
2155                .unwrap();
2156        insta::assert_debug_snapshot!(measurements, @r###"
2157        Measurements(
2158            {
2159                "app.start.cold": Measurement {
2160                    value: 1.1,
2161                    unit: ~,
2162                },
2163            },
2164        )
2165        "###);
2166        normalize_app_start_measurements(&mut measurements);
2167        insta::assert_debug_snapshot!(measurements, @r###"
2168        Measurements(
2169            {
2170                "app_start_cold": Measurement {
2171                    value: 1.1,
2172                    unit: ~,
2173                },
2174            },
2175        )
2176        "###);
2177    }
2178
2179    #[test]
2180    fn test_normalize_app_start_warm_measurements() {
2181        let mut measurements =
2182            Annotated::<Measurements>::from_json(r#"{"app.start.warm": {"value": 1.1}}"#)
2183                .unwrap()
2184                .into_value()
2185                .unwrap();
2186        insta::assert_debug_snapshot!(measurements, @r###"
2187        Measurements(
2188            {
2189                "app.start.warm": Measurement {
2190                    value: 1.1,
2191                    unit: ~,
2192                },
2193            },
2194        )
2195        "###);
2196        normalize_app_start_measurements(&mut measurements);
2197        insta::assert_debug_snapshot!(measurements, @r###"
2198        Measurements(
2199            {
2200                "app_start_warm": Measurement {
2201                    value: 1.1,
2202                    unit: ~,
2203                },
2204            },
2205        )
2206        "###);
2207    }
2208
2209    #[test]
2210    fn test_ai_legacy_measurements() {
2211        let json = r#"
2212            {
2213                "spans": [
2214                    {
2215                        "timestamp": 1702474613.0495,
2216                        "start_timestamp": 1702474613.0175,
2217                        "description": "OpenAI ",
2218                        "op": "ai.chat_completions.openai",
2219                        "span_id": "9c01bd820a083e63",
2220                        "parent_span_id": "a1e13f3f06239d69",
2221                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2222                        "measurements": {
2223                            "ai_prompt_tokens_used": {
2224                                "value": 1000
2225                            },
2226                            "ai_completion_tokens_used": {
2227                                "value": 2000
2228                            }
2229                        },
2230                        "data": {
2231                            "ai.pipeline.name": "Autofix Pipeline",
2232                            "ai.model_id": "claude-2.1"
2233                        }
2234                    },
2235                    {
2236                        "timestamp": 1702474613.0495,
2237                        "start_timestamp": 1702474613.0175,
2238                        "description": "OpenAI ",
2239                        "op": "ai.chat_completions.openai",
2240                        "span_id": "ac01bd820a083e63",
2241                        "parent_span_id": "a1e13f3f06239d69",
2242                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2243                        "measurements": {
2244                            "ai_prompt_tokens_used": {
2245                                "value": 1000
2246                            },
2247                            "ai_completion_tokens_used": {
2248                                "value": 2000
2249                            }
2250                        },
2251                        "data": {
2252                            "ai.pipeline.name": "Autofix Pipeline",
2253                            "ai.model_id": "gpt4-21-04"
2254                        }
2255                    }
2256                ]
2257            }
2258        "#;
2259
2260        let mut event = Annotated::<Event>::from_json(json).unwrap();
2261
2262        normalize_event(
2263            &mut event,
2264            &NormalizationConfig {
2265                ai_model_costs: Some(&ModelCosts {
2266                    version: 2,
2267                    costs: vec![],
2268                    models: HashMap::from([
2269                        (
2270                            "claude-2.1".to_owned(),
2271                            ModelCostV2 {
2272                                input_per_token: 0.01,
2273                                output_per_token: 0.02,
2274                                output_reasoning_per_token: 0.03,
2275                                input_cached_per_token: 0.0,
2276                            },
2277                        ),
2278                        (
2279                            "gpt4-21-04".to_owned(),
2280                            ModelCostV2 {
2281                                input_per_token: 0.02,
2282                                output_per_token: 0.03,
2283                                output_reasoning_per_token: 0.04,
2284                                input_cached_per_token: 0.0,
2285                            },
2286                        ),
2287                    ]),
2288                }),
2289                ..NormalizationConfig::default()
2290            },
2291        );
2292
2293        let spans = event.value().unwrap().spans.value().unwrap();
2294        assert_eq!(spans.len(), 2);
2295        assert_eq!(
2296            spans
2297                .first()
2298                .and_then(|span| span.value())
2299                .and_then(|span| span.data.value())
2300                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2301            Some(&Value::F64(50.0))
2302        );
2303        assert_eq!(
2304            spans
2305                .get(1)
2306                .and_then(|span| span.value())
2307                .and_then(|span| span.data.value())
2308                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2309            Some(&Value::F64(80.0))
2310        );
2311    }
2312
2313    #[test]
2314    fn test_ai_data() {
2315        let json = r#"
2316            {
2317                "spans": [
2318                    {
2319                        "timestamp": 1702474614.0175,
2320                        "start_timestamp": 1702474613.0175,
2321                        "description": "OpenAI ",
2322                        "op": "gen_ai.chat_completions.openai",
2323                        "span_id": "9c01bd820a083e63",
2324                        "parent_span_id": "a1e13f3f06239d69",
2325                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2326                        "data": {
2327                            "gen_ai.usage.input_tokens": 1000,
2328                            "gen_ai.usage.output_tokens": 2000,
2329                            "gen_ai.usage.output_tokens.reasoning": 3000,
2330                            "gen_ai.usage.input_tokens.cached": 4000,
2331                            "gen_ai.request.model": "claude-2.1"
2332                        }
2333                    },
2334                    {
2335                        "timestamp": 1702474614.0175,
2336                        "start_timestamp": 1702474613.0175,
2337                        "description": "OpenAI ",
2338                        "op": "gen_ai.chat_completions.openai",
2339                        "span_id": "ac01bd820a083e63",
2340                        "parent_span_id": "a1e13f3f06239d69",
2341                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2342                        "data": {
2343                            "gen_ai.usage.input_tokens": 1000,
2344                            "gen_ai.usage.output_tokens": 2000,
2345                            "gen_ai.request.model": "gpt4-21-04"
2346                        }
2347                    }
2348                ]
2349            }
2350        "#;
2351
2352        let mut event = Annotated::<Event>::from_json(json).unwrap();
2353
2354        normalize_event(
2355            &mut event,
2356            &NormalizationConfig {
2357                ai_model_costs: Some(&ModelCosts {
2358                    version: 2,
2359                    costs: vec![],
2360                    models: HashMap::from([
2361                        (
2362                            "claude-2.1".to_owned(),
2363                            ModelCostV2 {
2364                                input_per_token: 0.01,
2365                                output_per_token: 0.02,
2366                                output_reasoning_per_token: 0.03,
2367                                input_cached_per_token: 0.0,
2368                            },
2369                        ),
2370                        (
2371                            "gpt4-21-04".to_owned(),
2372                            ModelCostV2 {
2373                                input_per_token: 0.09,
2374                                output_per_token: 0.05,
2375                                output_reasoning_per_token: 0.06,
2376                                input_cached_per_token: 0.0,
2377                            },
2378                        ),
2379                    ]),
2380                }),
2381                ..NormalizationConfig::default()
2382            },
2383        );
2384
2385        let spans = event.value().unwrap().spans.value().unwrap();
2386        assert_eq!(spans.len(), 2);
2387        let first_span_data = spans
2388            .first()
2389            .and_then(|span| span.value())
2390            .and_then(|span| span.data.value());
2391        assert_eq!(
2392            first_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2393            Some(&Value::F64(140.0))
2394        );
2395        assert_eq!(
2396            first_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2397            Some(&Value::F64(2000.0))
2398        );
2399
2400        let second_span_data = spans
2401            .get(1)
2402            .and_then(|span| span.value())
2403            .and_then(|span| span.data.value());
2404        assert_eq!(
2405            second_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2406            Some(&Value::F64(190.0))
2407        );
2408        assert_eq!(
2409            second_span_data.and_then(|data| data.gen_ai_usage_total_tokens.value()),
2410            Some(&Value::F64(3000.0))
2411        );
2412        assert_eq!(
2413            second_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2414            Some(&Value::F64(2000.0))
2415        );
2416    }
2417
2418    #[test]
2419    fn test_ai_data_with_no_tokens() {
2420        let json = r#"
2421            {
2422                "spans": [
2423                    {
2424                        "timestamp": 1702474613.0495,
2425                        "start_timestamp": 1702474613.0175,
2426                        "description": "OpenAI ",
2427                        "op": "gen_ai.invoke_agent",
2428                        "span_id": "9c01bd820a083e63",
2429                        "parent_span_id": "a1e13f3f06239d69",
2430                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2431                        "data": {
2432                            "gen_ai.request.model": "claude-2.1"
2433                        }
2434                    }
2435                ]
2436            }
2437        "#;
2438
2439        let mut event = Annotated::<Event>::from_json(json).unwrap();
2440
2441        normalize_event(
2442            &mut event,
2443            &NormalizationConfig {
2444                ai_model_costs: Some(&ModelCosts {
2445                    version: 2,
2446                    costs: vec![],
2447                    models: HashMap::from([(
2448                        "claude-2.1".to_owned(),
2449                        ModelCostV2 {
2450                            input_per_token: 0.01,
2451                            output_per_token: 0.02,
2452                            output_reasoning_per_token: 0.03,
2453                            input_cached_per_token: 0.0,
2454                        },
2455                    )]),
2456                }),
2457                ..NormalizationConfig::default()
2458            },
2459        );
2460
2461        let spans = event.value().unwrap().spans.value().unwrap();
2462
2463        assert_eq!(spans.len(), 1);
2464        // total_cost shouldn't be set if no tokens are present on span data
2465        assert_eq!(
2466            spans
2467                .first()
2468                .and_then(|span| span.value())
2469                .and_then(|span| span.data.value())
2470                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2471            None
2472        );
2473        // total_tokens shouldn't be set if no tokens are present on span data
2474        assert_eq!(
2475            spans
2476                .first()
2477                .and_then(|span| span.value())
2478                .and_then(|span| span.data.value())
2479                .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2480            None
2481        );
2482    }
2483
2484    #[test]
2485    fn test_ai_data_with_ai_op_prefix() {
2486        let json = r#"
2487            {
2488                "spans": [
2489                    {
2490                        "timestamp": 1702474613.0495,
2491                        "start_timestamp": 1702474613.0175,
2492                        "description": "OpenAI ",
2493                        "op": "ai.chat_completions.openai",
2494                        "span_id": "9c01bd820a083e63",
2495                        "parent_span_id": "a1e13f3f06239d69",
2496                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2497                        "data": {
2498                            "gen_ai.usage.input_tokens": 1000,
2499                            "gen_ai.usage.output_tokens": 2000,
2500                            "gen_ai.usage.output_tokens.reasoning": 3000,
2501                            "gen_ai.usage.input_tokens.cached": 4000,
2502                            "gen_ai.request.model": "claude-2.1"
2503                        }
2504                    },
2505                    {
2506                        "timestamp": 1702474613.0495,
2507                        "start_timestamp": 1702474613.0175,
2508                        "description": "OpenAI ",
2509                        "op": "ai.chat_completions.openai",
2510                        "span_id": "ac01bd820a083e63",
2511                        "parent_span_id": "a1e13f3f06239d69",
2512                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2513                        "data": {
2514                            "gen_ai.usage.input_tokens": 1000,
2515                            "gen_ai.usage.output_tokens": 2000,
2516                            "gen_ai.request.model": "gpt4-21-04"
2517                        }
2518                    }
2519                ]
2520            }
2521        "#;
2522
2523        let mut event = Annotated::<Event>::from_json(json).unwrap();
2524
2525        normalize_event(
2526            &mut event,
2527            &NormalizationConfig {
2528                ai_model_costs: Some(&ModelCosts {
2529                    version: 2,
2530                    costs: vec![],
2531                    models: HashMap::from([
2532                        (
2533                            "claude-2.1".to_owned(),
2534                            ModelCostV2 {
2535                                input_per_token: 0.01,
2536                                output_per_token: 0.02,
2537                                output_reasoning_per_token: 0.03,
2538                                input_cached_per_token: 0.0,
2539                            },
2540                        ),
2541                        (
2542                            "gpt4-21-04".to_owned(),
2543                            ModelCostV2 {
2544                                input_per_token: 0.09,
2545                                output_per_token: 0.05,
2546                                output_reasoning_per_token: 0.06,
2547                                input_cached_per_token: 0.0,
2548                            },
2549                        ),
2550                    ]),
2551                }),
2552                ..NormalizationConfig::default()
2553            },
2554        );
2555
2556        let spans = event.value().unwrap().spans.value().unwrap();
2557        assert_eq!(spans.len(), 2);
2558        assert_eq!(
2559            spans
2560                .first()
2561                .and_then(|span| span.value())
2562                .and_then(|span| span.data.value())
2563                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2564            Some(&Value::F64(140.0))
2565        );
2566        assert_eq!(
2567            spans
2568                .get(1)
2569                .and_then(|span| span.value())
2570                .and_then(|span| span.data.value())
2571                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2572            Some(&Value::F64(190.0))
2573        );
2574        assert_eq!(
2575            spans
2576                .get(1)
2577                .and_then(|span| span.value())
2578                .and_then(|span| span.data.value())
2579                .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2580            Some(&Value::F64(3000.0))
2581        );
2582    }
2583
2584    #[test]
2585    fn test_ai_response_tokens_per_second_no_output_tokens() {
2586        let json = r#"
2587            {
2588                "spans": [
2589                    {
2590                        "timestamp": 1702474614.0175,
2591                        "start_timestamp": 1702474613.0175,
2592                        "op": "gen_ai.chat_completions",
2593                        "span_id": "9c01bd820a083e63",
2594                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2595                        "data": {
2596                            "gen_ai.usage.input_tokens": 500
2597                        }
2598                    }
2599                ]
2600            }
2601        "#;
2602
2603        let mut event = Annotated::<Event>::from_json(json).unwrap();
2604
2605        normalize_event(
2606            &mut event,
2607            &NormalizationConfig {
2608                ai_model_costs: Some(&ModelCosts {
2609                    version: 2,
2610                    costs: vec![],
2611                    models: HashMap::new(),
2612                }),
2613                ..NormalizationConfig::default()
2614            },
2615        );
2616
2617        let span_data = get_value!(event.spans[0].data!);
2618
2619        // Should not set response_tokens_per_second when there are no output tokens
2620        assert!(
2621            span_data
2622                .gen_ai_response_tokens_per_second
2623                .value()
2624                .is_none()
2625        );
2626    }
2627
2628    #[test]
2629    fn test_ai_response_tokens_per_second_zero_duration() {
2630        let json = r#"
2631            {
2632                "spans": [
2633                    {
2634                        "timestamp": 1702474613.0175,
2635                        "start_timestamp": 1702474613.0175,
2636                        "op": "gen_ai.chat_completions",
2637                        "span_id": "9c01bd820a083e63",
2638                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2639                        "data": {
2640                            "gen_ai.usage.output_tokens": 1000
2641                        }
2642                    }
2643                ]
2644            }
2645        "#;
2646
2647        let mut event = Annotated::<Event>::from_json(json).unwrap();
2648
2649        normalize_event(
2650            &mut event,
2651            &NormalizationConfig {
2652                ai_model_costs: Some(&ModelCosts {
2653                    version: 2,
2654                    costs: vec![],
2655                    models: HashMap::new(),
2656                }),
2657                ..NormalizationConfig::default()
2658            },
2659        );
2660
2661        let span_data = get_value!(event.spans[0].data!);
2662
2663        // Should not set response_tokens_per_second when duration is zero
2664        assert!(
2665            span_data
2666                .gen_ai_response_tokens_per_second
2667                .value()
2668                .is_none()
2669        );
2670    }
2671
2672    #[test]
2673    fn test_apple_high_device_class() {
2674        let mut event = Event {
2675            contexts: {
2676                let mut contexts = Contexts::new();
2677                contexts.add(DeviceContext {
2678                    family: "iPhone".to_owned().into(),
2679                    model: "iPhone15,3".to_owned().into(),
2680                    ..Default::default()
2681                });
2682                Annotated::new(contexts)
2683            },
2684            ..Default::default()
2685        };
2686        normalize_device_class(&mut event);
2687        assert_debug_snapshot!(event.tags, @r#"
2688        Tags(
2689            PairList(
2690                [
2691                    TagEntry(
2692                        "device.class",
2693                        "3",
2694                    ),
2695                ],
2696            ),
2697        )
2698        "#);
2699    }
2700
2701    #[test]
2702    fn test_filter_mobile_outliers() {
2703        let mut measurements =
2704            Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2705                .unwrap()
2706                .into_value()
2707                .unwrap();
2708        assert_eq!(measurements.len(), 1);
2709        filter_mobile_outliers(&mut measurements);
2710        assert_eq!(measurements.len(), 0);
2711    }
2712
2713    #[test]
2714    fn test_computed_performance_score() {
2715        let json = r#"
2716        {
2717            "type": "transaction",
2718            "timestamp": "2021-04-26T08:00:05+0100",
2719            "start_timestamp": "2021-04-26T08:00:00+0100",
2720            "measurements": {
2721                "fid": {"value": 213, "unit": "millisecond"},
2722                "fcp": {"value": 1237, "unit": "millisecond"},
2723                "lcp": {"value": 6596, "unit": "millisecond"},
2724                "cls": {"value": 0.11}
2725            },
2726            "contexts": {
2727                "browser": {
2728                    "name": "Chrome",
2729                    "version": "120.1.1",
2730                    "type": "browser"
2731                }
2732            }
2733        }
2734        "#;
2735
2736        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2737
2738        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2739            "profiles": [
2740                {
2741                    "name": "Desktop",
2742                    "scoreComponents": [
2743                        {
2744                            "measurement": "fcp",
2745                            "weight": 0.15,
2746                            "p10": 900,
2747                            "p50": 1600
2748                        },
2749                        {
2750                            "measurement": "lcp",
2751                            "weight": 0.30,
2752                            "p10": 1200,
2753                            "p50": 2400
2754                        },
2755                        {
2756                            "measurement": "fid",
2757                            "weight": 0.30,
2758                            "p10": 100,
2759                            "p50": 300
2760                        },
2761                        {
2762                            "measurement": "cls",
2763                            "weight": 0.25,
2764                            "p10": 0.1,
2765                            "p50": 0.25
2766                        },
2767                        {
2768                            "measurement": "ttfb",
2769                            "weight": 0.0,
2770                            "p10": 0.2,
2771                            "p50": 0.4
2772                        },
2773                    ],
2774                    "condition": {
2775                        "op":"eq",
2776                        "name": "event.contexts.browser.name",
2777                        "value": "Chrome"
2778                    }
2779                }
2780            ]
2781        }))
2782        .unwrap();
2783
2784        normalize_performance_score(&mut event, Some(&performance_score));
2785
2786        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2787        {
2788          "type": "transaction",
2789          "timestamp": 1619420405.0,
2790          "start_timestamp": 1619420400.0,
2791          "contexts": {
2792            "browser": {
2793              "name": "Chrome",
2794              "version": "120.1.1",
2795              "type": "browser",
2796            },
2797          },
2798          "measurements": {
2799            "cls": {
2800              "value": 0.11,
2801            },
2802            "fcp": {
2803              "value": 1237.0,
2804              "unit": "millisecond",
2805            },
2806            "fid": {
2807              "value": 213.0,
2808              "unit": "millisecond",
2809            },
2810            "lcp": {
2811              "value": 6596.0,
2812              "unit": "millisecond",
2813            },
2814            "score.cls": {
2815              "value": 0.21864170607444863,
2816              "unit": "ratio",
2817            },
2818            "score.fcp": {
2819              "value": 0.10750855443790831,
2820              "unit": "ratio",
2821            },
2822            "score.fid": {
2823              "value": 0.19657361348282545,
2824              "unit": "ratio",
2825            },
2826            "score.lcp": {
2827              "value": 0.009238896571386584,
2828              "unit": "ratio",
2829            },
2830            "score.ratio.cls": {
2831              "value": 0.8745668242977945,
2832              "unit": "ratio",
2833            },
2834            "score.ratio.fcp": {
2835              "value": 0.7167236962527221,
2836              "unit": "ratio",
2837            },
2838            "score.ratio.fid": {
2839              "value": 0.6552453782760849,
2840              "unit": "ratio",
2841            },
2842            "score.ratio.lcp": {
2843              "value": 0.03079632190462195,
2844              "unit": "ratio",
2845            },
2846            "score.total": {
2847              "value": 0.531962770566569,
2848              "unit": "ratio",
2849            },
2850            "score.weight.cls": {
2851              "value": 0.25,
2852              "unit": "ratio",
2853            },
2854            "score.weight.fcp": {
2855              "value": 0.15,
2856              "unit": "ratio",
2857            },
2858            "score.weight.fid": {
2859              "value": 0.3,
2860              "unit": "ratio",
2861            },
2862            "score.weight.lcp": {
2863              "value": 0.3,
2864              "unit": "ratio",
2865            },
2866            "score.weight.ttfb": {
2867              "value": 0.0,
2868              "unit": "ratio",
2869            },
2870          },
2871        }
2872        "###);
2873    }
2874
2875    // Test performance score is calculated correctly when the sum of weights is under 1.
2876    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
2877    #[test]
2878    fn test_computed_performance_score_with_under_normalized_weights() {
2879        let json = r#"
2880        {
2881            "type": "transaction",
2882            "timestamp": "2021-04-26T08:00:05+0100",
2883            "start_timestamp": "2021-04-26T08:00:00+0100",
2884            "measurements": {
2885                "fid": {"value": 213, "unit": "millisecond"},
2886                "fcp": {"value": 1237, "unit": "millisecond"},
2887                "lcp": {"value": 6596, "unit": "millisecond"},
2888                "cls": {"value": 0.11}
2889            },
2890            "contexts": {
2891                "browser": {
2892                    "name": "Chrome",
2893                    "version": "120.1.1",
2894                    "type": "browser"
2895                }
2896            }
2897        }
2898        "#;
2899
2900        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2901
2902        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2903            "profiles": [
2904                {
2905                    "name": "Desktop",
2906                    "scoreComponents": [
2907                        {
2908                            "measurement": "fcp",
2909                            "weight": 0.03,
2910                            "p10": 900,
2911                            "p50": 1600
2912                        },
2913                        {
2914                            "measurement": "lcp",
2915                            "weight": 0.06,
2916                            "p10": 1200,
2917                            "p50": 2400
2918                        },
2919                        {
2920                            "measurement": "fid",
2921                            "weight": 0.06,
2922                            "p10": 100,
2923                            "p50": 300
2924                        },
2925                        {
2926                            "measurement": "cls",
2927                            "weight": 0.05,
2928                            "p10": 0.1,
2929                            "p50": 0.25
2930                        },
2931                        {
2932                            "measurement": "ttfb",
2933                            "weight": 0.0,
2934                            "p10": 0.2,
2935                            "p50": 0.4
2936                        },
2937                    ],
2938                    "condition": {
2939                        "op":"eq",
2940                        "name": "event.contexts.browser.name",
2941                        "value": "Chrome"
2942                    }
2943                }
2944            ]
2945        }))
2946        .unwrap();
2947
2948        normalize_performance_score(&mut event, Some(&performance_score));
2949
2950        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2951        {
2952          "type": "transaction",
2953          "timestamp": 1619420405.0,
2954          "start_timestamp": 1619420400.0,
2955          "contexts": {
2956            "browser": {
2957              "name": "Chrome",
2958              "version": "120.1.1",
2959              "type": "browser",
2960            },
2961          },
2962          "measurements": {
2963            "cls": {
2964              "value": 0.11,
2965            },
2966            "fcp": {
2967              "value": 1237.0,
2968              "unit": "millisecond",
2969            },
2970            "fid": {
2971              "value": 213.0,
2972              "unit": "millisecond",
2973            },
2974            "lcp": {
2975              "value": 6596.0,
2976              "unit": "millisecond",
2977            },
2978            "score.cls": {
2979              "value": 0.21864170607444863,
2980              "unit": "ratio",
2981            },
2982            "score.fcp": {
2983              "value": 0.10750855443790831,
2984              "unit": "ratio",
2985            },
2986            "score.fid": {
2987              "value": 0.19657361348282545,
2988              "unit": "ratio",
2989            },
2990            "score.lcp": {
2991              "value": 0.009238896571386584,
2992              "unit": "ratio",
2993            },
2994            "score.ratio.cls": {
2995              "value": 0.8745668242977945,
2996              "unit": "ratio",
2997            },
2998            "score.ratio.fcp": {
2999              "value": 0.7167236962527221,
3000              "unit": "ratio",
3001            },
3002            "score.ratio.fid": {
3003              "value": 0.6552453782760849,
3004              "unit": "ratio",
3005            },
3006            "score.ratio.lcp": {
3007              "value": 0.03079632190462195,
3008              "unit": "ratio",
3009            },
3010            "score.total": {
3011              "value": 0.531962770566569,
3012              "unit": "ratio",
3013            },
3014            "score.weight.cls": {
3015              "value": 0.25,
3016              "unit": "ratio",
3017            },
3018            "score.weight.fcp": {
3019              "value": 0.15,
3020              "unit": "ratio",
3021            },
3022            "score.weight.fid": {
3023              "value": 0.3,
3024              "unit": "ratio",
3025            },
3026            "score.weight.lcp": {
3027              "value": 0.3,
3028              "unit": "ratio",
3029            },
3030            "score.weight.ttfb": {
3031              "value": 0.0,
3032              "unit": "ratio",
3033            },
3034          },
3035        }
3036        "###);
3037    }
3038
3039    // Test performance score is calculated correctly when the sum of weights is over 1.
3040    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
3041    #[test]
3042    fn test_computed_performance_score_with_over_normalized_weights() {
3043        let json = r#"
3044        {
3045            "type": "transaction",
3046            "timestamp": "2021-04-26T08:00:05+0100",
3047            "start_timestamp": "2021-04-26T08:00:00+0100",
3048            "measurements": {
3049                "fid": {"value": 213, "unit": "millisecond"},
3050                "fcp": {"value": 1237, "unit": "millisecond"},
3051                "lcp": {"value": 6596, "unit": "millisecond"},
3052                "cls": {"value": 0.11}
3053            },
3054            "contexts": {
3055                "browser": {
3056                    "name": "Chrome",
3057                    "version": "120.1.1",
3058                    "type": "browser"
3059                }
3060            }
3061        }
3062        "#;
3063
3064        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3065
3066        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3067            "profiles": [
3068                {
3069                    "name": "Desktop",
3070                    "scoreComponents": [
3071                        {
3072                            "measurement": "fcp",
3073                            "weight": 0.30,
3074                            "p10": 900,
3075                            "p50": 1600
3076                        },
3077                        {
3078                            "measurement": "lcp",
3079                            "weight": 0.60,
3080                            "p10": 1200,
3081                            "p50": 2400
3082                        },
3083                        {
3084                            "measurement": "fid",
3085                            "weight": 0.60,
3086                            "p10": 100,
3087                            "p50": 300
3088                        },
3089                        {
3090                            "measurement": "cls",
3091                            "weight": 0.50,
3092                            "p10": 0.1,
3093                            "p50": 0.25
3094                        },
3095                        {
3096                            "measurement": "ttfb",
3097                            "weight": 0.0,
3098                            "p10": 0.2,
3099                            "p50": 0.4
3100                        },
3101                    ],
3102                    "condition": {
3103                        "op":"eq",
3104                        "name": "event.contexts.browser.name",
3105                        "value": "Chrome"
3106                    }
3107                }
3108            ]
3109        }))
3110        .unwrap();
3111
3112        normalize_performance_score(&mut event, Some(&performance_score));
3113
3114        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3115        {
3116          "type": "transaction",
3117          "timestamp": 1619420405.0,
3118          "start_timestamp": 1619420400.0,
3119          "contexts": {
3120            "browser": {
3121              "name": "Chrome",
3122              "version": "120.1.1",
3123              "type": "browser",
3124            },
3125          },
3126          "measurements": {
3127            "cls": {
3128              "value": 0.11,
3129            },
3130            "fcp": {
3131              "value": 1237.0,
3132              "unit": "millisecond",
3133            },
3134            "fid": {
3135              "value": 213.0,
3136              "unit": "millisecond",
3137            },
3138            "lcp": {
3139              "value": 6596.0,
3140              "unit": "millisecond",
3141            },
3142            "score.cls": {
3143              "value": 0.21864170607444863,
3144              "unit": "ratio",
3145            },
3146            "score.fcp": {
3147              "value": 0.10750855443790831,
3148              "unit": "ratio",
3149            },
3150            "score.fid": {
3151              "value": 0.19657361348282545,
3152              "unit": "ratio",
3153            },
3154            "score.lcp": {
3155              "value": 0.009238896571386584,
3156              "unit": "ratio",
3157            },
3158            "score.ratio.cls": {
3159              "value": 0.8745668242977945,
3160              "unit": "ratio",
3161            },
3162            "score.ratio.fcp": {
3163              "value": 0.7167236962527221,
3164              "unit": "ratio",
3165            },
3166            "score.ratio.fid": {
3167              "value": 0.6552453782760849,
3168              "unit": "ratio",
3169            },
3170            "score.ratio.lcp": {
3171              "value": 0.03079632190462195,
3172              "unit": "ratio",
3173            },
3174            "score.total": {
3175              "value": 0.531962770566569,
3176              "unit": "ratio",
3177            },
3178            "score.weight.cls": {
3179              "value": 0.25,
3180              "unit": "ratio",
3181            },
3182            "score.weight.fcp": {
3183              "value": 0.15,
3184              "unit": "ratio",
3185            },
3186            "score.weight.fid": {
3187              "value": 0.3,
3188              "unit": "ratio",
3189            },
3190            "score.weight.lcp": {
3191              "value": 0.3,
3192              "unit": "ratio",
3193            },
3194            "score.weight.ttfb": {
3195              "value": 0.0,
3196              "unit": "ratio",
3197            },
3198          },
3199        }
3200        "###);
3201    }
3202
3203    #[test]
3204    fn test_computed_performance_score_missing_measurement() {
3205        let json = r#"
3206        {
3207            "type": "transaction",
3208            "timestamp": "2021-04-26T08:00:05+0100",
3209            "start_timestamp": "2021-04-26T08:00:00+0100",
3210            "measurements": {
3211                "a": {"value": 213, "unit": "millisecond"}
3212            },
3213            "contexts": {
3214                "browser": {
3215                    "name": "Chrome",
3216                    "version": "120.1.1",
3217                    "type": "browser"
3218                }
3219            }
3220        }
3221        "#;
3222
3223        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3224
3225        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3226            "profiles": [
3227                {
3228                    "name": "Desktop",
3229                    "scoreComponents": [
3230                        {
3231                            "measurement": "a",
3232                            "weight": 0.15,
3233                            "p10": 900,
3234                            "p50": 1600
3235                        },
3236                        {
3237                            "measurement": "b",
3238                            "weight": 0.30,
3239                            "p10": 1200,
3240                            "p50": 2400
3241                        },
3242                    ],
3243                    "condition": {
3244                        "op":"eq",
3245                        "name": "event.contexts.browser.name",
3246                        "value": "Chrome"
3247                    }
3248                }
3249            ]
3250        }))
3251        .unwrap();
3252
3253        normalize_performance_score(&mut event, Some(&performance_score));
3254
3255        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3256        {
3257          "type": "transaction",
3258          "timestamp": 1619420405.0,
3259          "start_timestamp": 1619420400.0,
3260          "contexts": {
3261            "browser": {
3262              "name": "Chrome",
3263              "version": "120.1.1",
3264              "type": "browser",
3265            },
3266          },
3267          "measurements": {
3268            "a": {
3269              "value": 213.0,
3270              "unit": "millisecond",
3271            },
3272          },
3273        }
3274        "###);
3275    }
3276
3277    #[test]
3278    fn test_computed_performance_score_optional_measurement() {
3279        let json = r#"
3280        {
3281            "type": "transaction",
3282            "timestamp": "2021-04-26T08:00:05+0100",
3283            "start_timestamp": "2021-04-26T08:00:00+0100",
3284            "measurements": {
3285                "a": {"value": 213, "unit": "millisecond"},
3286                "b": {"value": 213, "unit": "millisecond"}
3287            },
3288            "contexts": {
3289                "browser": {
3290                    "name": "Chrome",
3291                    "version": "120.1.1",
3292                    "type": "browser"
3293                }
3294            }
3295        }
3296        "#;
3297
3298        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3299
3300        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3301            "profiles": [
3302                {
3303                    "name": "Desktop",
3304                    "scoreComponents": [
3305                        {
3306                            "measurement": "a",
3307                            "weight": 0.15,
3308                            "p10": 900,
3309                            "p50": 1600,
3310                        },
3311                        {
3312                            "measurement": "b",
3313                            "weight": 0.30,
3314                            "p10": 1200,
3315                            "p50": 2400,
3316                            "optional": true
3317                        },
3318                        {
3319                            "measurement": "c",
3320                            "weight": 0.55,
3321                            "p10": 1200,
3322                            "p50": 2400,
3323                            "optional": true
3324                        },
3325                    ],
3326                    "condition": {
3327                        "op":"eq",
3328                        "name": "event.contexts.browser.name",
3329                        "value": "Chrome"
3330                    }
3331                }
3332            ]
3333        }))
3334        .unwrap();
3335
3336        normalize_performance_score(&mut event, Some(&performance_score));
3337
3338        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3339        {
3340          "type": "transaction",
3341          "timestamp": 1619420405.0,
3342          "start_timestamp": 1619420400.0,
3343          "contexts": {
3344            "browser": {
3345              "name": "Chrome",
3346              "version": "120.1.1",
3347              "type": "browser",
3348            },
3349          },
3350          "measurements": {
3351            "a": {
3352              "value": 213.0,
3353              "unit": "millisecond",
3354            },
3355            "b": {
3356              "value": 213.0,
3357              "unit": "millisecond",
3358            },
3359            "score.a": {
3360              "value": 0.33333215313291975,
3361              "unit": "ratio",
3362            },
3363            "score.b": {
3364              "value": 0.66666415149198,
3365              "unit": "ratio",
3366            },
3367            "score.ratio.a": {
3368              "value": 0.9999964593987591,
3369              "unit": "ratio",
3370            },
3371            "score.ratio.b": {
3372              "value": 0.9999962272379699,
3373              "unit": "ratio",
3374            },
3375            "score.total": {
3376              "value": 0.9999963046248997,
3377              "unit": "ratio",
3378            },
3379            "score.weight.a": {
3380              "value": 0.33333333333333337,
3381              "unit": "ratio",
3382            },
3383            "score.weight.b": {
3384              "value": 0.6666666666666667,
3385              "unit": "ratio",
3386            },
3387            "score.weight.c": {
3388              "value": 0.0,
3389              "unit": "ratio",
3390            },
3391          },
3392        }
3393        "###);
3394    }
3395
3396    #[test]
3397    fn test_computed_performance_score_weight_0() {
3398        let json = r#"
3399        {
3400            "type": "transaction",
3401            "timestamp": "2021-04-26T08:00:05+0100",
3402            "start_timestamp": "2021-04-26T08:00:00+0100",
3403            "measurements": {
3404                "cls": {"value": 0.11}
3405            }
3406        }
3407        "#;
3408
3409        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3410
3411        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3412            "profiles": [
3413                {
3414                    "name": "Desktop",
3415                    "scoreComponents": [
3416                        {
3417                            "measurement": "cls",
3418                            "weight": 0,
3419                            "p10": 0.1,
3420                            "p50": 0.25
3421                        },
3422                    ],
3423                    "condition": {
3424                        "op":"and",
3425                        "inner": []
3426                    }
3427                }
3428            ]
3429        }))
3430        .unwrap();
3431
3432        normalize_performance_score(&mut event, Some(&performance_score));
3433
3434        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3435        {
3436          "type": "transaction",
3437          "timestamp": 1619420405.0,
3438          "start_timestamp": 1619420400.0,
3439          "measurements": {
3440            "cls": {
3441              "value": 0.11,
3442            },
3443          },
3444        }
3445        "###);
3446    }
3447
3448    #[test]
3449    fn test_computed_performance_score_negative_value() {
3450        let json = r#"
3451        {
3452            "type": "transaction",
3453            "timestamp": "2021-04-26T08:00:05+0100",
3454            "start_timestamp": "2021-04-26T08:00:00+0100",
3455            "measurements": {
3456                "ttfb": {"value": -100, "unit": "millisecond"}
3457            }
3458        }
3459        "#;
3460
3461        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3462
3463        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3464            "profiles": [
3465                {
3466                    "name": "Desktop",
3467                    "scoreComponents": [
3468                        {
3469                            "measurement": "ttfb",
3470                            "weight": 1.0,
3471                            "p10": 100.0,
3472                            "p50": 250.0
3473                        },
3474                    ],
3475                    "condition": {
3476                        "op":"and",
3477                        "inner": []
3478                    }
3479                }
3480            ]
3481        }))
3482        .unwrap();
3483
3484        normalize_performance_score(&mut event, Some(&performance_score));
3485
3486        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3487        {
3488          "type": "transaction",
3489          "timestamp": 1619420405.0,
3490          "start_timestamp": 1619420400.0,
3491          "measurements": {
3492            "score.ratio.ttfb": {
3493              "value": 1.0,
3494              "unit": "ratio",
3495            },
3496            "score.total": {
3497              "value": 1.0,
3498              "unit": "ratio",
3499            },
3500            "score.ttfb": {
3501              "value": 1.0,
3502              "unit": "ratio",
3503            },
3504            "score.weight.ttfb": {
3505              "value": 1.0,
3506              "unit": "ratio",
3507            },
3508            "ttfb": {
3509              "value": -100.0,
3510              "unit": "millisecond",
3511            },
3512          },
3513        }
3514        "###);
3515    }
3516
3517    #[test]
3518    fn test_filter_negative_web_vital_measurements() {
3519        let json = r#"
3520        {
3521            "type": "transaction",
3522            "timestamp": "2021-04-26T08:00:05+0100",
3523            "start_timestamp": "2021-04-26T08:00:00+0100",
3524            "measurements": {
3525                "ttfb": {"value": -100, "unit": "millisecond"}
3526            }
3527        }
3528        "#;
3529        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3530
3531        // Allow ttfb as a builtinMeasurement with allow_negative defaulted to false.
3532        let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3533            "builtinMeasurements": [
3534                {"name": "ttfb", "unit": "millisecond"},
3535            ],
3536        }))
3537        .unwrap();
3538
3539        let dynamic_measurement_config =
3540            CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3541
3542        normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3543
3544        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3545        {
3546          "type": "transaction",
3547          "timestamp": 1619420405.0,
3548          "start_timestamp": 1619420400.0,
3549          "measurements": {},
3550          "_meta": {
3551            "measurements": {
3552              "": Meta(Some(MetaInner(
3553                err: [
3554                  [
3555                    "invalid_data",
3556                    {
3557                      "reason": "Negative value for measurement ttfb not allowed: -100",
3558                    },
3559                  ],
3560                ],
3561                val: Some({
3562                  "ttfb": {
3563                    "unit": "millisecond",
3564                    "value": -100.0,
3565                  },
3566                }),
3567              ))),
3568            },
3569          },
3570        }
3571        "###);
3572    }
3573
3574    #[test]
3575    fn test_computed_performance_score_multiple_profiles() {
3576        let json = r#"
3577        {
3578            "type": "transaction",
3579            "timestamp": "2021-04-26T08:00:05+0100",
3580            "start_timestamp": "2021-04-26T08:00:00+0100",
3581            "measurements": {
3582                "cls": {"value": 0.11},
3583                "inp": {"value": 120.0}
3584            }
3585        }
3586        "#;
3587
3588        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3589
3590        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3591            "profiles": [
3592                {
3593                    "name": "Desktop",
3594                    "scoreComponents": [
3595                        {
3596                            "measurement": "cls",
3597                            "weight": 0,
3598                            "p10": 0.1,
3599                            "p50": 0.25
3600                        },
3601                    ],
3602                    "condition": {
3603                        "op":"and",
3604                        "inner": []
3605                    }
3606                },
3607                {
3608                    "name": "Desktop",
3609                    "scoreComponents": [
3610                        {
3611                            "measurement": "inp",
3612                            "weight": 1.0,
3613                            "p10": 0.1,
3614                            "p50": 0.25
3615                        },
3616                    ],
3617                    "condition": {
3618                        "op":"and",
3619                        "inner": []
3620                    }
3621                }
3622            ]
3623        }))
3624        .unwrap();
3625
3626        normalize_performance_score(&mut event, Some(&performance_score));
3627
3628        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3629        {
3630          "type": "transaction",
3631          "timestamp": 1619420405.0,
3632          "start_timestamp": 1619420400.0,
3633          "measurements": {
3634            "cls": {
3635              "value": 0.11,
3636            },
3637            "inp": {
3638              "value": 120.0,
3639            },
3640            "score.inp": {
3641              "value": 0.0,
3642              "unit": "ratio",
3643            },
3644            "score.ratio.inp": {
3645              "value": 0.0,
3646              "unit": "ratio",
3647            },
3648            "score.total": {
3649              "value": 0.0,
3650              "unit": "ratio",
3651            },
3652            "score.weight.inp": {
3653              "value": 1.0,
3654              "unit": "ratio",
3655            },
3656          },
3657        }
3658        "###);
3659    }
3660
3661    #[test]
3662    fn test_compute_performance_score_for_mobile_ios_profile() {
3663        let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3664            .unwrap()
3665            .0
3666            .unwrap();
3667
3668        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3669            "profiles": [
3670                {
3671                    "name": "Mobile",
3672                    "scoreComponents": [
3673                        {
3674                            "measurement": "time_to_initial_display",
3675                            "weight": 0.25,
3676                            "p10": 1800.0,
3677                            "p50": 3000.0,
3678                            "optional": true
3679                        },
3680                        {
3681                            "measurement": "time_to_full_display",
3682                            "weight": 0.25,
3683                            "p10": 2500.0,
3684                            "p50": 4000.0,
3685                            "optional": true
3686                        },
3687                        {
3688                            "measurement": "app_start_warm",
3689                            "weight": 0.25,
3690                            "p10": 200.0,
3691                            "p50": 500.0,
3692                            "optional": true
3693                        },
3694                        {
3695                            "measurement": "app_start_cold",
3696                            "weight": 0.25,
3697                            "p10": 200.0,
3698                            "p50": 500.0,
3699                            "optional": true
3700                        }
3701                    ],
3702                    "condition": {
3703                        "op": "and",
3704                        "inner": [
3705                            {
3706                                "op": "or",
3707                                "inner": [
3708                                    {
3709                                        "op": "eq",
3710                                        "name": "event.sdk.name",
3711                                        "value": "sentry.cocoa"
3712                                    },
3713                                    {
3714                                        "op": "eq",
3715                                        "name": "event.sdk.name",
3716                                        "value": "sentry.java.android"
3717                                    }
3718                                ]
3719                            },
3720                            {
3721                                "op": "eq",
3722                                "name": "event.contexts.trace.op",
3723                                "value": "ui.load"
3724                            }
3725                        ]
3726                    }
3727                }
3728            ]
3729        }))
3730        .unwrap();
3731
3732        normalize_performance_score(&mut event, Some(&performance_score));
3733
3734        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3735    }
3736
3737    #[test]
3738    fn test_compute_performance_score_for_mobile_android_profile() {
3739        let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3740            .unwrap()
3741            .0
3742            .unwrap();
3743
3744        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3745            "profiles": [
3746                {
3747                    "name": "Mobile",
3748                    "scoreComponents": [
3749                        {
3750                            "measurement": "time_to_initial_display",
3751                            "weight": 0.25,
3752                            "p10": 1800.0,
3753                            "p50": 3000.0,
3754                            "optional": true
3755                        },
3756                        {
3757                            "measurement": "time_to_full_display",
3758                            "weight": 0.25,
3759                            "p10": 2500.0,
3760                            "p50": 4000.0,
3761                            "optional": true
3762                        },
3763                        {
3764                            "measurement": "app_start_warm",
3765                            "weight": 0.25,
3766                            "p10": 200.0,
3767                            "p50": 500.0,
3768                            "optional": true
3769                        },
3770                        {
3771                            "measurement": "app_start_cold",
3772                            "weight": 0.25,
3773                            "p10": 200.0,
3774                            "p50": 500.0,
3775                            "optional": true
3776                        }
3777                    ],
3778                    "condition": {
3779                        "op": "and",
3780                        "inner": [
3781                            {
3782                                "op": "or",
3783                                "inner": [
3784                                    {
3785                                        "op": "eq",
3786                                        "name": "event.sdk.name",
3787                                        "value": "sentry.cocoa"
3788                                    },
3789                                    {
3790                                        "op": "eq",
3791                                        "name": "event.sdk.name",
3792                                        "value": "sentry.java.android"
3793                                    }
3794                                ]
3795                            },
3796                            {
3797                                "op": "eq",
3798                                "name": "event.contexts.trace.op",
3799                                "value": "ui.load"
3800                            }
3801                        ]
3802                    }
3803                }
3804            ]
3805        }))
3806        .unwrap();
3807
3808        normalize_performance_score(&mut event, Some(&performance_score));
3809
3810        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3811    }
3812
3813    #[test]
3814    fn test_computes_performance_score_and_tags_with_profile_version() {
3815        let json = r#"
3816        {
3817            "type": "transaction",
3818            "timestamp": "2021-04-26T08:00:05+0100",
3819            "start_timestamp": "2021-04-26T08:00:00+0100",
3820            "measurements": {
3821                "inp": {"value": 120.0}
3822            }
3823        }
3824        "#;
3825
3826        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3827
3828        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3829            "profiles": [
3830                {
3831                    "name": "Desktop",
3832                    "scoreComponents": [
3833                        {
3834                            "measurement": "inp",
3835                            "weight": 1.0,
3836                            "p10": 0.1,
3837                            "p50": 0.25
3838                        },
3839                    ],
3840                    "condition": {
3841                        "op":"and",
3842                        "inner": []
3843                    },
3844                    "version": "beta"
3845                }
3846            ]
3847        }))
3848        .unwrap();
3849
3850        normalize(
3851            &mut event,
3852            &mut Meta::default(),
3853            &NormalizationConfig {
3854                performance_score: Some(&performance_score),
3855                ..Default::default()
3856            },
3857        );
3858
3859        insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3860        {
3861          "performance_score": {
3862            "score_profile_version": "beta",
3863            "type": "performancescore",
3864          },
3865        }
3866        "###);
3867        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3868        {
3869          "inp": {
3870            "value": 120.0,
3871            "unit": "millisecond",
3872          },
3873          "score.inp": {
3874            "value": 0.0,
3875            "unit": "ratio",
3876          },
3877          "score.ratio.inp": {
3878            "value": 0.0,
3879            "unit": "ratio",
3880          },
3881          "score.total": {
3882            "value": 0.0,
3883            "unit": "ratio",
3884          },
3885          "score.weight.inp": {
3886            "value": 1.0,
3887            "unit": "ratio",
3888          },
3889        }
3890        "###);
3891    }
3892
3893    #[test]
3894    fn test_computes_standalone_cls_performance_score() {
3895        let json = r#"
3896        {
3897            "type": "transaction",
3898            "timestamp": "2021-04-26T08:00:05+0100",
3899            "start_timestamp": "2021-04-26T08:00:00+0100",
3900            "measurements": {
3901                "cls": {"value": 0.5}
3902            }
3903        }
3904        "#;
3905
3906        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3907
3908        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3909            "profiles": [
3910            {
3911                "name": "Default",
3912                "scoreComponents": [
3913                    {
3914                        "measurement": "fcp",
3915                        "weight": 0.15,
3916                        "p10": 900.0,
3917                        "p50": 1600.0,
3918                        "optional": true,
3919                    },
3920                    {
3921                        "measurement": "lcp",
3922                        "weight": 0.30,
3923                        "p10": 1200.0,
3924                        "p50": 2400.0,
3925                        "optional": true,
3926                    },
3927                    {
3928                        "measurement": "cls",
3929                        "weight": 0.15,
3930                        "p10": 0.1,
3931                        "p50": 0.25,
3932                        "optional": true,
3933                    },
3934                    {
3935                        "measurement": "ttfb",
3936                        "weight": 0.10,
3937                        "p10": 200.0,
3938                        "p50": 400.0,
3939                        "optional": true,
3940                    },
3941                ],
3942                "condition": {
3943                    "op": "and",
3944                    "inner": [],
3945                },
3946            }
3947            ]
3948        }))
3949        .unwrap();
3950
3951        normalize(
3952            &mut event,
3953            &mut Meta::default(),
3954            &NormalizationConfig {
3955                performance_score: Some(&performance_score),
3956                ..Default::default()
3957            },
3958        );
3959
3960        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3961        {
3962          "cls": {
3963            "value": 0.5,
3964            "unit": "none",
3965          },
3966          "score.cls": {
3967            "value": 0.16615877613713903,
3968            "unit": "ratio",
3969          },
3970          "score.ratio.cls": {
3971            "value": 0.16615877613713903,
3972            "unit": "ratio",
3973          },
3974          "score.total": {
3975            "value": 0.16615877613713903,
3976            "unit": "ratio",
3977          },
3978          "score.weight.cls": {
3979            "value": 1.0,
3980            "unit": "ratio",
3981          },
3982          "score.weight.fcp": {
3983            "value": 0.0,
3984            "unit": "ratio",
3985          },
3986          "score.weight.lcp": {
3987            "value": 0.0,
3988            "unit": "ratio",
3989          },
3990          "score.weight.ttfb": {
3991            "value": 0.0,
3992            "unit": "ratio",
3993          },
3994        }
3995        "###);
3996    }
3997
3998    #[test]
3999    fn test_computes_standalone_lcp_performance_score() {
4000        let json = r#"
4001        {
4002            "type": "transaction",
4003            "timestamp": "2021-04-26T08:00:05+0100",
4004            "start_timestamp": "2021-04-26T08:00:00+0100",
4005            "measurements": {
4006                "lcp": {"value": 1200.0}
4007            }
4008        }
4009        "#;
4010
4011        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4012
4013        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4014            "profiles": [
4015            {
4016                "name": "Default",
4017                "scoreComponents": [
4018                    {
4019                        "measurement": "fcp",
4020                        "weight": 0.15,
4021                        "p10": 900.0,
4022                        "p50": 1600.0,
4023                        "optional": true,
4024                    },
4025                    {
4026                        "measurement": "lcp",
4027                        "weight": 0.30,
4028                        "p10": 1200.0,
4029                        "p50": 2400.0,
4030                        "optional": true,
4031                    },
4032                    {
4033                        "measurement": "cls",
4034                        "weight": 0.15,
4035                        "p10": 0.1,
4036                        "p50": 0.25,
4037                        "optional": true,
4038                    },
4039                    {
4040                        "measurement": "ttfb",
4041                        "weight": 0.10,
4042                        "p10": 200.0,
4043                        "p50": 400.0,
4044                        "optional": true,
4045                    },
4046                ],
4047                "condition": {
4048                    "op": "and",
4049                    "inner": [],
4050                },
4051            }
4052            ]
4053        }))
4054        .unwrap();
4055
4056        normalize(
4057            &mut event,
4058            &mut Meta::default(),
4059            &NormalizationConfig {
4060                performance_score: Some(&performance_score),
4061                ..Default::default()
4062            },
4063        );
4064
4065        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4066        {
4067          "lcp": {
4068            "value": 1200.0,
4069            "unit": "millisecond",
4070          },
4071          "score.lcp": {
4072            "value": 0.8999999314038525,
4073            "unit": "ratio",
4074          },
4075          "score.ratio.lcp": {
4076            "value": 0.8999999314038525,
4077            "unit": "ratio",
4078          },
4079          "score.total": {
4080            "value": 0.8999999314038525,
4081            "unit": "ratio",
4082          },
4083          "score.weight.cls": {
4084            "value": 0.0,
4085            "unit": "ratio",
4086          },
4087          "score.weight.fcp": {
4088            "value": 0.0,
4089            "unit": "ratio",
4090          },
4091          "score.weight.lcp": {
4092            "value": 1.0,
4093            "unit": "ratio",
4094          },
4095          "score.weight.ttfb": {
4096            "value": 0.0,
4097            "unit": "ratio",
4098          },
4099        }
4100        "###);
4101    }
4102
4103    #[test]
4104    fn test_computed_performance_score_uses_first_matching_profile() {
4105        let json = r#"
4106        {
4107            "type": "transaction",
4108            "timestamp": "2021-04-26T08:00:05+0100",
4109            "start_timestamp": "2021-04-26T08:00:00+0100",
4110            "measurements": {
4111                "a": {"value": 213, "unit": "millisecond"},
4112                "b": {"value": 213, "unit": "millisecond"}
4113            },
4114            "contexts": {
4115                "browser": {
4116                    "name": "Chrome",
4117                    "version": "120.1.1",
4118                    "type": "browser"
4119                }
4120            }
4121        }
4122        "#;
4123
4124        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4125
4126        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4127            "profiles": [
4128                {
4129                    "name": "Mobile",
4130                    "scoreComponents": [
4131                        {
4132                            "measurement": "a",
4133                            "weight": 0.15,
4134                            "p10": 100,
4135                            "p50": 200,
4136                        },
4137                        {
4138                            "measurement": "b",
4139                            "weight": 0.30,
4140                            "p10": 100,
4141                            "p50": 200,
4142                            "optional": true
4143                        },
4144                        {
4145                            "measurement": "c",
4146                            "weight": 0.55,
4147                            "p10": 100,
4148                            "p50": 200,
4149                            "optional": true
4150                        },
4151                    ],
4152                    "condition": {
4153                        "op":"eq",
4154                        "name": "event.contexts.browser.name",
4155                        "value": "Chrome Mobile"
4156                    }
4157                },
4158                {
4159                    "name": "Desktop",
4160                    "scoreComponents": [
4161                        {
4162                            "measurement": "a",
4163                            "weight": 0.15,
4164                            "p10": 900,
4165                            "p50": 1600,
4166                        },
4167                        {
4168                            "measurement": "b",
4169                            "weight": 0.30,
4170                            "p10": 1200,
4171                            "p50": 2400,
4172                            "optional": true
4173                        },
4174                        {
4175                            "measurement": "c",
4176                            "weight": 0.55,
4177                            "p10": 1200,
4178                            "p50": 2400,
4179                            "optional": true
4180                        },
4181                    ],
4182                    "condition": {
4183                        "op":"eq",
4184                        "name": "event.contexts.browser.name",
4185                        "value": "Chrome"
4186                    }
4187                },
4188                {
4189                    "name": "Default",
4190                    "scoreComponents": [
4191                        {
4192                            "measurement": "a",
4193                            "weight": 0.15,
4194                            "p10": 100,
4195                            "p50": 200,
4196                        },
4197                        {
4198                            "measurement": "b",
4199                            "weight": 0.30,
4200                            "p10": 100,
4201                            "p50": 200,
4202                            "optional": true
4203                        },
4204                        {
4205                            "measurement": "c",
4206                            "weight": 0.55,
4207                            "p10": 100,
4208                            "p50": 200,
4209                            "optional": true
4210                        },
4211                    ],
4212                    "condition": {
4213                        "op": "and",
4214                        "inner": [],
4215                    }
4216                }
4217            ]
4218        }))
4219        .unwrap();
4220
4221        normalize_performance_score(&mut event, Some(&performance_score));
4222
4223        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4224        {
4225          "type": "transaction",
4226          "timestamp": 1619420405.0,
4227          "start_timestamp": 1619420400.0,
4228          "contexts": {
4229            "browser": {
4230              "name": "Chrome",
4231              "version": "120.1.1",
4232              "type": "browser",
4233            },
4234          },
4235          "measurements": {
4236            "a": {
4237              "value": 213.0,
4238              "unit": "millisecond",
4239            },
4240            "b": {
4241              "value": 213.0,
4242              "unit": "millisecond",
4243            },
4244            "score.a": {
4245              "value": 0.33333215313291975,
4246              "unit": "ratio",
4247            },
4248            "score.b": {
4249              "value": 0.66666415149198,
4250              "unit": "ratio",
4251            },
4252            "score.ratio.a": {
4253              "value": 0.9999964593987591,
4254              "unit": "ratio",
4255            },
4256            "score.ratio.b": {
4257              "value": 0.9999962272379699,
4258              "unit": "ratio",
4259            },
4260            "score.total": {
4261              "value": 0.9999963046248997,
4262              "unit": "ratio",
4263            },
4264            "score.weight.a": {
4265              "value": 0.33333333333333337,
4266              "unit": "ratio",
4267            },
4268            "score.weight.b": {
4269              "value": 0.6666666666666667,
4270              "unit": "ratio",
4271            },
4272            "score.weight.c": {
4273              "value": 0.0,
4274              "unit": "ratio",
4275            },
4276          },
4277        }
4278        "###);
4279    }
4280
4281    #[test]
4282    fn test_computed_performance_score_falls_back_to_default_profile() {
4283        let json = r#"
4284        {
4285            "type": "transaction",
4286            "timestamp": "2021-04-26T08:00:05+0100",
4287            "start_timestamp": "2021-04-26T08:00:00+0100",
4288            "measurements": {
4289                "a": {"value": 213, "unit": "millisecond"},
4290                "b": {"value": 213, "unit": "millisecond"}
4291            },
4292            "contexts": {}
4293        }
4294        "#;
4295
4296        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4297
4298        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4299            "profiles": [
4300                {
4301                    "name": "Mobile",
4302                    "scoreComponents": [
4303                        {
4304                            "measurement": "a",
4305                            "weight": 0.15,
4306                            "p10": 900,
4307                            "p50": 1600,
4308                            "optional": true
4309                        },
4310                        {
4311                            "measurement": "b",
4312                            "weight": 0.30,
4313                            "p10": 1200,
4314                            "p50": 2400,
4315                            "optional": true
4316                        },
4317                        {
4318                            "measurement": "c",
4319                            "weight": 0.55,
4320                            "p10": 1200,
4321                            "p50": 2400,
4322                            "optional": true
4323                        },
4324                    ],
4325                    "condition": {
4326                        "op":"eq",
4327                        "name": "event.contexts.browser.name",
4328                        "value": "Chrome Mobile"
4329                    }
4330                },
4331                {
4332                    "name": "Desktop",
4333                    "scoreComponents": [
4334                        {
4335                            "measurement": "a",
4336                            "weight": 0.15,
4337                            "p10": 900,
4338                            "p50": 1600,
4339                            "optional": true
4340                        },
4341                        {
4342                            "measurement": "b",
4343                            "weight": 0.30,
4344                            "p10": 1200,
4345                            "p50": 2400,
4346                            "optional": true
4347                        },
4348                        {
4349                            "measurement": "c",
4350                            "weight": 0.55,
4351                            "p10": 1200,
4352                            "p50": 2400,
4353                            "optional": true
4354                        },
4355                    ],
4356                    "condition": {
4357                        "op":"eq",
4358                        "name": "event.contexts.browser.name",
4359                        "value": "Chrome"
4360                    }
4361                },
4362                {
4363                    "name": "Default",
4364                    "scoreComponents": [
4365                        {
4366                            "measurement": "a",
4367                            "weight": 0.15,
4368                            "p10": 100,
4369                            "p50": 200,
4370                            "optional": true
4371                        },
4372                        {
4373                            "measurement": "b",
4374                            "weight": 0.30,
4375                            "p10": 100,
4376                            "p50": 200,
4377                            "optional": true
4378                        },
4379                        {
4380                            "measurement": "c",
4381                            "weight": 0.55,
4382                            "p10": 100,
4383                            "p50": 200,
4384                            "optional": true
4385                        },
4386                    ],
4387                    "condition": {
4388                        "op": "and",
4389                        "inner": [],
4390                    }
4391                }
4392            ]
4393        }))
4394        .unwrap();
4395
4396        normalize_performance_score(&mut event, Some(&performance_score));
4397
4398        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4399        {
4400          "type": "transaction",
4401          "timestamp": 1619420405.0,
4402          "start_timestamp": 1619420400.0,
4403          "contexts": {},
4404          "measurements": {
4405            "a": {
4406              "value": 213.0,
4407              "unit": "millisecond",
4408            },
4409            "b": {
4410              "value": 213.0,
4411              "unit": "millisecond",
4412            },
4413            "score.a": {
4414              "value": 0.15121816827413334,
4415              "unit": "ratio",
4416            },
4417            "score.b": {
4418              "value": 0.3024363365482667,
4419              "unit": "ratio",
4420            },
4421            "score.ratio.a": {
4422              "value": 0.45365450482239994,
4423              "unit": "ratio",
4424            },
4425            "score.ratio.b": {
4426              "value": 0.45365450482239994,
4427              "unit": "ratio",
4428            },
4429            "score.total": {
4430              "value": 0.4536545048224,
4431              "unit": "ratio",
4432            },
4433            "score.weight.a": {
4434              "value": 0.33333333333333337,
4435              "unit": "ratio",
4436            },
4437            "score.weight.b": {
4438              "value": 0.6666666666666667,
4439              "unit": "ratio",
4440            },
4441            "score.weight.c": {
4442              "value": 0.0,
4443              "unit": "ratio",
4444            },
4445          },
4446        }
4447        "###);
4448    }
4449
4450    #[test]
4451    fn test_normalization_removes_reprocessing_context() {
4452        let json = r#"{
4453            "contexts": {
4454                "reprocessing": {}
4455            }
4456        }"#;
4457        let mut event = Annotated::<Event>::from_json(json).unwrap();
4458        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4459        normalize_event(&mut event, &NormalizationConfig::default());
4460        assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4461    }
4462
4463    #[test]
4464    fn test_renormalization_does_not_remove_reprocessing_context() {
4465        let json = r#"{
4466            "contexts": {
4467                "reprocessing": {}
4468            }
4469        }"#;
4470        let mut event = Annotated::<Event>::from_json(json).unwrap();
4471        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4472        normalize_event(
4473            &mut event,
4474            &NormalizationConfig {
4475                is_renormalize: true,
4476                ..Default::default()
4477            },
4478        );
4479        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4480    }
4481
4482    #[test]
4483    fn test_normalize_user() {
4484        let json = r#"{
4485            "user": {
4486                "id": "123456",
4487                "username": "john",
4488                "other": "value"
4489            }
4490        }"#;
4491        let mut event = Annotated::<Event>::from_json(json).unwrap();
4492        normalize_user(event.value_mut().as_mut().unwrap());
4493
4494        let user = event.value().unwrap().user.value().unwrap();
4495        assert_eq!(user.data, {
4496            let mut map = Object::new();
4497            map.insert(
4498                "other".to_owned(),
4499                Annotated::new(Value::String("value".to_owned())),
4500            );
4501            Annotated::new(map)
4502        });
4503        assert_eq!(user.other, Object::new());
4504        assert_eq!(user.username, Annotated::new("john".to_owned().into()));
4505        assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
4506    }
4507
4508    #[test]
4509    fn test_handle_types_in_spaced_exception_values() {
4510        let mut exception = Annotated::new(Exception {
4511            value: Annotated::new("ValueError: unauthorized".to_owned().into()),
4512            ..Exception::default()
4513        });
4514        normalize_exception(&mut exception);
4515
4516        let exception = exception.value().unwrap();
4517        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4518        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4519    }
4520
4521    #[test]
4522    fn test_handle_types_in_non_spaced_excepton_values() {
4523        let mut exception = Annotated::new(Exception {
4524            value: Annotated::new("ValueError:unauthorized".to_owned().into()),
4525            ..Exception::default()
4526        });
4527        normalize_exception(&mut exception);
4528
4529        let exception = exception.value().unwrap();
4530        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4531        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4532    }
4533
4534    #[test]
4535    fn test_rejects_empty_exception_fields() {
4536        let mut exception = Annotated::new(Exception {
4537            value: Annotated::new("".to_owned().into()),
4538            ty: Annotated::new("".to_owned()),
4539            ..Default::default()
4540        });
4541
4542        normalize_exception(&mut exception);
4543
4544        assert!(exception.value().is_none());
4545        assert!(exception.meta().has_errors());
4546    }
4547
4548    #[test]
4549    fn test_json_value() {
4550        let mut exception = Annotated::new(Exception {
4551            value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
4552            ..Exception::default()
4553        });
4554
4555        normalize_exception(&mut exception);
4556
4557        let exception = exception.value().unwrap();
4558
4559        // Don't split a json-serialized value on the colon
4560        assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4561        assert_eq!(exception.ty.value(), None);
4562    }
4563
4564    #[test]
4565    fn test_exception_invalid() {
4566        let mut exception = Annotated::new(Exception::default());
4567
4568        normalize_exception(&mut exception);
4569
4570        let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4571            error.insert("attribute", "type or value");
4572        });
4573        assert_eq!(
4574            exception.meta().iter_errors().collect_tuple(),
4575            Some((&expected,))
4576        );
4577    }
4578
4579    #[test]
4580    fn test_normalize_exception() {
4581        let mut event = Annotated::new(Event {
4582            exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4583                // Exception with missing type and value
4584                ty: Annotated::empty(),
4585                value: Annotated::empty(),
4586                ..Default::default()
4587            })])),
4588            ..Default::default()
4589        });
4590
4591        normalize_event(&mut event, &NormalizationConfig::default());
4592
4593        let exception = event
4594            .value()
4595            .unwrap()
4596            .exceptions
4597            .value()
4598            .unwrap()
4599            .values
4600            .value()
4601            .unwrap()
4602            .first()
4603            .unwrap();
4604
4605        assert_debug_snapshot!(exception.meta(), @r#"
4606        Meta {
4607            remarks: [],
4608            errors: [
4609                Error {
4610                    kind: MissingAttribute,
4611                    data: {
4612                        "attribute": String(
4613                            "type or value",
4614                        ),
4615                    },
4616                },
4617            ],
4618            original_length: None,
4619            original_value: Some(
4620                Object(
4621                    {
4622                        "mechanism": ~,
4623                        "module": ~,
4624                        "raw_stacktrace": ~,
4625                        "stacktrace": ~,
4626                        "thread_id": ~,
4627                        "type": ~,
4628                        "value": ~,
4629                    },
4630                ),
4631            ),
4632        }"#);
4633    }
4634
4635    #[test]
4636    fn test_normalize_breadcrumbs() {
4637        let mut event = Event {
4638            breadcrumbs: Annotated::new(Values {
4639                values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4640                ..Default::default()
4641            }),
4642            ..Default::default()
4643        };
4644        normalize_breadcrumbs(&mut event);
4645
4646        let breadcrumb = event
4647            .breadcrumbs
4648            .value()
4649            .unwrap()
4650            .values
4651            .value()
4652            .unwrap()
4653            .first()
4654            .unwrap()
4655            .value()
4656            .unwrap();
4657        assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4658        assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4659    }
4660
4661    #[test]
4662    fn test_other_debug_images_have_meta_errors() {
4663        let mut event = Event {
4664            debug_meta: Annotated::new(DebugMeta {
4665                images: Annotated::new(vec![Annotated::new(
4666                    DebugImage::Other(BTreeMap::default()),
4667                )]),
4668                ..Default::default()
4669            }),
4670            ..Default::default()
4671        };
4672        normalize_debug_meta(&mut event);
4673
4674        let debug_image_meta = event
4675            .debug_meta
4676            .value()
4677            .unwrap()
4678            .images
4679            .value()
4680            .unwrap()
4681            .first()
4682            .unwrap()
4683            .meta();
4684        assert_debug_snapshot!(debug_image_meta, @r#"
4685        Meta {
4686            remarks: [],
4687            errors: [
4688                Error {
4689                    kind: InvalidData,
4690                    data: {
4691                        "reason": String(
4692                            "unsupported debug image type",
4693                        ),
4694                    },
4695                },
4696            ],
4697            original_length: None,
4698            original_value: Some(
4699                Object(
4700                    {},
4701                ),
4702            ),
4703        }"#);
4704    }
4705
4706    #[test]
4707    fn test_skip_span_normalization_when_configured() {
4708        let json = r#"{
4709            "type": "transaction",
4710            "start_timestamp": 1,
4711            "timestamp": 2,
4712            "contexts": {
4713                "trace": {
4714                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4715                    "span_id": "aaaaaaaaaaaaaaaa"
4716                }
4717            },
4718            "spans": [
4719                {
4720                    "op": "db",
4721                    "description": "SELECT * FROM table;",
4722                    "start_timestamp": 1,
4723                    "timestamp": 2,
4724                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4725                    "span_id": "bbbbbbbbbbbbbbbb",
4726                    "parent_span_id": "aaaaaaaaaaaaaaaa"
4727                }
4728            ]
4729        }"#;
4730
4731        let mut event = Annotated::<Event>::from_json(json).unwrap();
4732        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4733        normalize_event(
4734            &mut event,
4735            &NormalizationConfig {
4736                is_renormalize: true,
4737                ..Default::default()
4738            },
4739        );
4740        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4741        normalize_event(
4742            &mut event,
4743            &NormalizationConfig {
4744                is_renormalize: false,
4745                ..Default::default()
4746            },
4747        );
4748        assert!(get_value!(event.spans[0].exclusive_time).is_some());
4749    }
4750
4751    #[test]
4752    fn test_normalize_trace_context_tags_extracts_lcp_info() {
4753        let json = r#"{
4754            "type": "transaction",
4755            "start_timestamp": 1,
4756            "timestamp": 2,
4757            "contexts": {
4758                "trace": {
4759                    "data": {
4760                        "lcp.element": "body > div#app > div > h1#header",
4761                        "lcp.size": 24827,
4762                        "lcp.id": "header",
4763                        "lcp.url": "http://example.com/image.jpg"
4764                    }
4765                }
4766            },
4767            "measurements": {
4768                "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4769            }
4770        }"#;
4771        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4772        normalize_trace_context_tags(&mut event);
4773        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
4774        {
4775          "type": "transaction",
4776          "timestamp": 2.0,
4777          "start_timestamp": 1.0,
4778          "contexts": {
4779            "trace": {
4780              "data": {
4781                "lcp.element": "body > div#app > div > h1#header",
4782                "lcp.size": 24827,
4783                "lcp.id": "header",
4784                "lcp.url": "http://example.com/image.jpg",
4785              },
4786              "type": "trace",
4787            },
4788          },
4789          "tags": [
4790            [
4791              "lcp.element",
4792              "body > div#app > div > h1#header",
4793            ],
4794            [
4795              "lcp.size",
4796              "24827",
4797            ],
4798            [
4799              "lcp.id",
4800              "header",
4801            ],
4802            [
4803              "lcp.url",
4804              "http://example.com/image.jpg",
4805            ],
4806          ],
4807          "measurements": {
4808            "lcp": {
4809              "value": 146.20000000298023,
4810              "unit": "millisecond",
4811            },
4812          },
4813        }
4814        "#);
4815    }
4816
4817    #[test]
4818    fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4819        let json = r#"{
4820          "type": "transaction",
4821          "start_timestamp": 1,
4822          "timestamp": 2,
4823          "contexts": {
4824              "trace": {
4825                  "data": {
4826                      "lcp.element": "body > div#app > div > h1#id",
4827                      "lcp.size": 33333,
4828                      "lcp.id": "id",
4829                      "lcp.url": "http://example.com/another-image.jpg"
4830                  }
4831              }
4832          },
4833          "tags": {
4834              "lcp.element": "body > div#app > div > h1#header",
4835              "lcp.size": 24827,
4836              "lcp.id": "header",
4837              "lcp.url": "http://example.com/image.jpg"
4838          },
4839          "measurements": {
4840              "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4841          }
4842        }"#;
4843        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4844        normalize_trace_context_tags(&mut event);
4845        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
4846        {
4847          "type": "transaction",
4848          "timestamp": 2.0,
4849          "start_timestamp": 1.0,
4850          "contexts": {
4851            "trace": {
4852              "data": {
4853                "lcp.element": "body > div#app > div > h1#id",
4854                "lcp.size": 33333,
4855                "lcp.id": "id",
4856                "lcp.url": "http://example.com/another-image.jpg",
4857              },
4858              "type": "trace",
4859            },
4860          },
4861          "tags": [
4862            [
4863              "lcp.element",
4864              "body > div#app > div > h1#header",
4865            ],
4866            [
4867              "lcp.id",
4868              "header",
4869            ],
4870            [
4871              "lcp.size",
4872              "24827",
4873            ],
4874            [
4875              "lcp.url",
4876              "http://example.com/image.jpg",
4877            ],
4878          ],
4879          "measurements": {
4880            "lcp": {
4881              "value": 146.20000000298023,
4882              "unit": "millisecond",
4883            },
4884          },
4885        }
4886        "#);
4887    }
4888}