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