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::normalize_ai_measurements;
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    normalize_ai_measurements(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_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                .unwrap()
2275                .value()
2276                .unwrap()
2277                .measurements
2278                .value()
2279                .unwrap()
2280                .get_value("ai_total_cost"),
2281            Some(1.23)
2282        );
2283        assert_eq!(
2284            spans
2285                .get(1)
2286                .unwrap()
2287                .value()
2288                .unwrap()
2289                .measurements
2290                .value()
2291                .unwrap()
2292                .get_value("ai_total_cost"),
2293            Some(20.0 * 2.0 + 2.0)
2294        );
2295    }
2296
2297    #[test]
2298    fn test_apple_high_device_class() {
2299        let mut event = Event {
2300            contexts: {
2301                let mut contexts = Contexts::new();
2302                contexts.add(DeviceContext {
2303                    family: "iPhone".to_string().into(),
2304                    model: "iPhone15,3".to_string().into(),
2305                    ..Default::default()
2306                });
2307                Annotated::new(contexts)
2308            },
2309            ..Default::default()
2310        };
2311        normalize_device_class(&mut event);
2312        assert_debug_snapshot!(event.tags, @r#"
2313        Tags(
2314            PairList(
2315                [
2316                    TagEntry(
2317                        "device.class",
2318                        "3",
2319                    ),
2320                ],
2321            ),
2322        )
2323        "#);
2324    }
2325
2326    #[test]
2327    fn test_filter_mobile_outliers() {
2328        let mut measurements =
2329            Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2330                .unwrap()
2331                .into_value()
2332                .unwrap();
2333        assert_eq!(measurements.len(), 1);
2334        filter_mobile_outliers(&mut measurements);
2335        assert_eq!(measurements.len(), 0);
2336    }
2337
2338    #[test]
2339    fn test_computed_performance_score() {
2340        let json = r#"
2341        {
2342            "type": "transaction",
2343            "timestamp": "2021-04-26T08:00:05+0100",
2344            "start_timestamp": "2021-04-26T08:00:00+0100",
2345            "measurements": {
2346                "fid": {"value": 213, "unit": "millisecond"},
2347                "fcp": {"value": 1237, "unit": "millisecond"},
2348                "lcp": {"value": 6596, "unit": "millisecond"},
2349                "cls": {"value": 0.11}
2350            },
2351            "contexts": {
2352                "browser": {
2353                    "name": "Chrome",
2354                    "version": "120.1.1",
2355                    "type": "browser"
2356                }
2357            }
2358        }
2359        "#;
2360
2361        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2362
2363        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2364            "profiles": [
2365                {
2366                    "name": "Desktop",
2367                    "scoreComponents": [
2368                        {
2369                            "measurement": "fcp",
2370                            "weight": 0.15,
2371                            "p10": 900,
2372                            "p50": 1600
2373                        },
2374                        {
2375                            "measurement": "lcp",
2376                            "weight": 0.30,
2377                            "p10": 1200,
2378                            "p50": 2400
2379                        },
2380                        {
2381                            "measurement": "fid",
2382                            "weight": 0.30,
2383                            "p10": 100,
2384                            "p50": 300
2385                        },
2386                        {
2387                            "measurement": "cls",
2388                            "weight": 0.25,
2389                            "p10": 0.1,
2390                            "p50": 0.25
2391                        },
2392                        {
2393                            "measurement": "ttfb",
2394                            "weight": 0.0,
2395                            "p10": 0.2,
2396                            "p50": 0.4
2397                        },
2398                    ],
2399                    "condition": {
2400                        "op":"eq",
2401                        "name": "event.contexts.browser.name",
2402                        "value": "Chrome"
2403                    }
2404                }
2405            ]
2406        }))
2407        .unwrap();
2408
2409        normalize_performance_score(&mut event, Some(&performance_score));
2410
2411        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2412        {
2413          "type": "transaction",
2414          "timestamp": 1619420405.0,
2415          "start_timestamp": 1619420400.0,
2416          "contexts": {
2417            "browser": {
2418              "name": "Chrome",
2419              "version": "120.1.1",
2420              "type": "browser",
2421            },
2422          },
2423          "measurements": {
2424            "cls": {
2425              "value": 0.11,
2426            },
2427            "fcp": {
2428              "value": 1237.0,
2429              "unit": "millisecond",
2430            },
2431            "fid": {
2432              "value": 213.0,
2433              "unit": "millisecond",
2434            },
2435            "lcp": {
2436              "value": 6596.0,
2437              "unit": "millisecond",
2438            },
2439            "score.cls": {
2440              "value": 0.21864170607444863,
2441              "unit": "ratio",
2442            },
2443            "score.fcp": {
2444              "value": 0.10750855443790831,
2445              "unit": "ratio",
2446            },
2447            "score.fid": {
2448              "value": 0.19657361348282545,
2449              "unit": "ratio",
2450            },
2451            "score.lcp": {
2452              "value": 0.009238896571386584,
2453              "unit": "ratio",
2454            },
2455            "score.ratio.cls": {
2456              "value": 0.8745668242977945,
2457              "unit": "ratio",
2458            },
2459            "score.ratio.fcp": {
2460              "value": 0.7167236962527221,
2461              "unit": "ratio",
2462            },
2463            "score.ratio.fid": {
2464              "value": 0.6552453782760849,
2465              "unit": "ratio",
2466            },
2467            "score.ratio.lcp": {
2468              "value": 0.03079632190462195,
2469              "unit": "ratio",
2470            },
2471            "score.total": {
2472              "value": 0.531962770566569,
2473              "unit": "ratio",
2474            },
2475            "score.weight.cls": {
2476              "value": 0.25,
2477              "unit": "ratio",
2478            },
2479            "score.weight.fcp": {
2480              "value": 0.15,
2481              "unit": "ratio",
2482            },
2483            "score.weight.fid": {
2484              "value": 0.3,
2485              "unit": "ratio",
2486            },
2487            "score.weight.lcp": {
2488              "value": 0.3,
2489              "unit": "ratio",
2490            },
2491            "score.weight.ttfb": {
2492              "value": 0.0,
2493              "unit": "ratio",
2494            },
2495          },
2496        }
2497        "###);
2498    }
2499
2500    // Test performance score is calculated correctly when the sum of weights is under 1.
2501    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
2502    #[test]
2503    fn test_computed_performance_score_with_under_normalized_weights() {
2504        let json = r#"
2505        {
2506            "type": "transaction",
2507            "timestamp": "2021-04-26T08:00:05+0100",
2508            "start_timestamp": "2021-04-26T08:00:00+0100",
2509            "measurements": {
2510                "fid": {"value": 213, "unit": "millisecond"},
2511                "fcp": {"value": 1237, "unit": "millisecond"},
2512                "lcp": {"value": 6596, "unit": "millisecond"},
2513                "cls": {"value": 0.11}
2514            },
2515            "contexts": {
2516                "browser": {
2517                    "name": "Chrome",
2518                    "version": "120.1.1",
2519                    "type": "browser"
2520                }
2521            }
2522        }
2523        "#;
2524
2525        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2526
2527        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2528            "profiles": [
2529                {
2530                    "name": "Desktop",
2531                    "scoreComponents": [
2532                        {
2533                            "measurement": "fcp",
2534                            "weight": 0.03,
2535                            "p10": 900,
2536                            "p50": 1600
2537                        },
2538                        {
2539                            "measurement": "lcp",
2540                            "weight": 0.06,
2541                            "p10": 1200,
2542                            "p50": 2400
2543                        },
2544                        {
2545                            "measurement": "fid",
2546                            "weight": 0.06,
2547                            "p10": 100,
2548                            "p50": 300
2549                        },
2550                        {
2551                            "measurement": "cls",
2552                            "weight": 0.05,
2553                            "p10": 0.1,
2554                            "p50": 0.25
2555                        },
2556                        {
2557                            "measurement": "ttfb",
2558                            "weight": 0.0,
2559                            "p10": 0.2,
2560                            "p50": 0.4
2561                        },
2562                    ],
2563                    "condition": {
2564                        "op":"eq",
2565                        "name": "event.contexts.browser.name",
2566                        "value": "Chrome"
2567                    }
2568                }
2569            ]
2570        }))
2571        .unwrap();
2572
2573        normalize_performance_score(&mut event, Some(&performance_score));
2574
2575        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2576        {
2577          "type": "transaction",
2578          "timestamp": 1619420405.0,
2579          "start_timestamp": 1619420400.0,
2580          "contexts": {
2581            "browser": {
2582              "name": "Chrome",
2583              "version": "120.1.1",
2584              "type": "browser",
2585            },
2586          },
2587          "measurements": {
2588            "cls": {
2589              "value": 0.11,
2590            },
2591            "fcp": {
2592              "value": 1237.0,
2593              "unit": "millisecond",
2594            },
2595            "fid": {
2596              "value": 213.0,
2597              "unit": "millisecond",
2598            },
2599            "lcp": {
2600              "value": 6596.0,
2601              "unit": "millisecond",
2602            },
2603            "score.cls": {
2604              "value": 0.21864170607444863,
2605              "unit": "ratio",
2606            },
2607            "score.fcp": {
2608              "value": 0.10750855443790831,
2609              "unit": "ratio",
2610            },
2611            "score.fid": {
2612              "value": 0.19657361348282545,
2613              "unit": "ratio",
2614            },
2615            "score.lcp": {
2616              "value": 0.009238896571386584,
2617              "unit": "ratio",
2618            },
2619            "score.ratio.cls": {
2620              "value": 0.8745668242977945,
2621              "unit": "ratio",
2622            },
2623            "score.ratio.fcp": {
2624              "value": 0.7167236962527221,
2625              "unit": "ratio",
2626            },
2627            "score.ratio.fid": {
2628              "value": 0.6552453782760849,
2629              "unit": "ratio",
2630            },
2631            "score.ratio.lcp": {
2632              "value": 0.03079632190462195,
2633              "unit": "ratio",
2634            },
2635            "score.total": {
2636              "value": 0.531962770566569,
2637              "unit": "ratio",
2638            },
2639            "score.weight.cls": {
2640              "value": 0.25,
2641              "unit": "ratio",
2642            },
2643            "score.weight.fcp": {
2644              "value": 0.15,
2645              "unit": "ratio",
2646            },
2647            "score.weight.fid": {
2648              "value": 0.3,
2649              "unit": "ratio",
2650            },
2651            "score.weight.lcp": {
2652              "value": 0.3,
2653              "unit": "ratio",
2654            },
2655            "score.weight.ttfb": {
2656              "value": 0.0,
2657              "unit": "ratio",
2658            },
2659          },
2660        }
2661        "###);
2662    }
2663
2664    // Test performance score is calculated correctly when the sum of weights is over 1.
2665    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
2666    #[test]
2667    fn test_computed_performance_score_with_over_normalized_weights() {
2668        let json = r#"
2669        {
2670            "type": "transaction",
2671            "timestamp": "2021-04-26T08:00:05+0100",
2672            "start_timestamp": "2021-04-26T08:00:00+0100",
2673            "measurements": {
2674                "fid": {"value": 213, "unit": "millisecond"},
2675                "fcp": {"value": 1237, "unit": "millisecond"},
2676                "lcp": {"value": 6596, "unit": "millisecond"},
2677                "cls": {"value": 0.11}
2678            },
2679            "contexts": {
2680                "browser": {
2681                    "name": "Chrome",
2682                    "version": "120.1.1",
2683                    "type": "browser"
2684                }
2685            }
2686        }
2687        "#;
2688
2689        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2690
2691        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2692            "profiles": [
2693                {
2694                    "name": "Desktop",
2695                    "scoreComponents": [
2696                        {
2697                            "measurement": "fcp",
2698                            "weight": 0.30,
2699                            "p10": 900,
2700                            "p50": 1600
2701                        },
2702                        {
2703                            "measurement": "lcp",
2704                            "weight": 0.60,
2705                            "p10": 1200,
2706                            "p50": 2400
2707                        },
2708                        {
2709                            "measurement": "fid",
2710                            "weight": 0.60,
2711                            "p10": 100,
2712                            "p50": 300
2713                        },
2714                        {
2715                            "measurement": "cls",
2716                            "weight": 0.50,
2717                            "p10": 0.1,
2718                            "p50": 0.25
2719                        },
2720                        {
2721                            "measurement": "ttfb",
2722                            "weight": 0.0,
2723                            "p10": 0.2,
2724                            "p50": 0.4
2725                        },
2726                    ],
2727                    "condition": {
2728                        "op":"eq",
2729                        "name": "event.contexts.browser.name",
2730                        "value": "Chrome"
2731                    }
2732                }
2733            ]
2734        }))
2735        .unwrap();
2736
2737        normalize_performance_score(&mut event, Some(&performance_score));
2738
2739        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2740        {
2741          "type": "transaction",
2742          "timestamp": 1619420405.0,
2743          "start_timestamp": 1619420400.0,
2744          "contexts": {
2745            "browser": {
2746              "name": "Chrome",
2747              "version": "120.1.1",
2748              "type": "browser",
2749            },
2750          },
2751          "measurements": {
2752            "cls": {
2753              "value": 0.11,
2754            },
2755            "fcp": {
2756              "value": 1237.0,
2757              "unit": "millisecond",
2758            },
2759            "fid": {
2760              "value": 213.0,
2761              "unit": "millisecond",
2762            },
2763            "lcp": {
2764              "value": 6596.0,
2765              "unit": "millisecond",
2766            },
2767            "score.cls": {
2768              "value": 0.21864170607444863,
2769              "unit": "ratio",
2770            },
2771            "score.fcp": {
2772              "value": 0.10750855443790831,
2773              "unit": "ratio",
2774            },
2775            "score.fid": {
2776              "value": 0.19657361348282545,
2777              "unit": "ratio",
2778            },
2779            "score.lcp": {
2780              "value": 0.009238896571386584,
2781              "unit": "ratio",
2782            },
2783            "score.ratio.cls": {
2784              "value": 0.8745668242977945,
2785              "unit": "ratio",
2786            },
2787            "score.ratio.fcp": {
2788              "value": 0.7167236962527221,
2789              "unit": "ratio",
2790            },
2791            "score.ratio.fid": {
2792              "value": 0.6552453782760849,
2793              "unit": "ratio",
2794            },
2795            "score.ratio.lcp": {
2796              "value": 0.03079632190462195,
2797              "unit": "ratio",
2798            },
2799            "score.total": {
2800              "value": 0.531962770566569,
2801              "unit": "ratio",
2802            },
2803            "score.weight.cls": {
2804              "value": 0.25,
2805              "unit": "ratio",
2806            },
2807            "score.weight.fcp": {
2808              "value": 0.15,
2809              "unit": "ratio",
2810            },
2811            "score.weight.fid": {
2812              "value": 0.3,
2813              "unit": "ratio",
2814            },
2815            "score.weight.lcp": {
2816              "value": 0.3,
2817              "unit": "ratio",
2818            },
2819            "score.weight.ttfb": {
2820              "value": 0.0,
2821              "unit": "ratio",
2822            },
2823          },
2824        }
2825        "###);
2826    }
2827
2828    #[test]
2829    fn test_computed_performance_score_missing_measurement() {
2830        let json = r#"
2831        {
2832            "type": "transaction",
2833            "timestamp": "2021-04-26T08:00:05+0100",
2834            "start_timestamp": "2021-04-26T08:00:00+0100",
2835            "measurements": {
2836                "a": {"value": 213, "unit": "millisecond"}
2837            },
2838            "contexts": {
2839                "browser": {
2840                    "name": "Chrome",
2841                    "version": "120.1.1",
2842                    "type": "browser"
2843                }
2844            }
2845        }
2846        "#;
2847
2848        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2849
2850        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2851            "profiles": [
2852                {
2853                    "name": "Desktop",
2854                    "scoreComponents": [
2855                        {
2856                            "measurement": "a",
2857                            "weight": 0.15,
2858                            "p10": 900,
2859                            "p50": 1600
2860                        },
2861                        {
2862                            "measurement": "b",
2863                            "weight": 0.30,
2864                            "p10": 1200,
2865                            "p50": 2400
2866                        },
2867                    ],
2868                    "condition": {
2869                        "op":"eq",
2870                        "name": "event.contexts.browser.name",
2871                        "value": "Chrome"
2872                    }
2873                }
2874            ]
2875        }))
2876        .unwrap();
2877
2878        normalize_performance_score(&mut event, Some(&performance_score));
2879
2880        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2881        {
2882          "type": "transaction",
2883          "timestamp": 1619420405.0,
2884          "start_timestamp": 1619420400.0,
2885          "contexts": {
2886            "browser": {
2887              "name": "Chrome",
2888              "version": "120.1.1",
2889              "type": "browser",
2890            },
2891          },
2892          "measurements": {
2893            "a": {
2894              "value": 213.0,
2895              "unit": "millisecond",
2896            },
2897          },
2898        }
2899        "###);
2900    }
2901
2902    #[test]
2903    fn test_computed_performance_score_optional_measurement() {
2904        let json = r#"
2905        {
2906            "type": "transaction",
2907            "timestamp": "2021-04-26T08:00:05+0100",
2908            "start_timestamp": "2021-04-26T08:00:00+0100",
2909            "measurements": {
2910                "a": {"value": 213, "unit": "millisecond"},
2911                "b": {"value": 213, "unit": "millisecond"}
2912            },
2913            "contexts": {
2914                "browser": {
2915                    "name": "Chrome",
2916                    "version": "120.1.1",
2917                    "type": "browser"
2918                }
2919            }
2920        }
2921        "#;
2922
2923        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2924
2925        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2926            "profiles": [
2927                {
2928                    "name": "Desktop",
2929                    "scoreComponents": [
2930                        {
2931                            "measurement": "a",
2932                            "weight": 0.15,
2933                            "p10": 900,
2934                            "p50": 1600,
2935                        },
2936                        {
2937                            "measurement": "b",
2938                            "weight": 0.30,
2939                            "p10": 1200,
2940                            "p50": 2400,
2941                            "optional": true
2942                        },
2943                        {
2944                            "measurement": "c",
2945                            "weight": 0.55,
2946                            "p10": 1200,
2947                            "p50": 2400,
2948                            "optional": true
2949                        },
2950                    ],
2951                    "condition": {
2952                        "op":"eq",
2953                        "name": "event.contexts.browser.name",
2954                        "value": "Chrome"
2955                    }
2956                }
2957            ]
2958        }))
2959        .unwrap();
2960
2961        normalize_performance_score(&mut event, Some(&performance_score));
2962
2963        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2964        {
2965          "type": "transaction",
2966          "timestamp": 1619420405.0,
2967          "start_timestamp": 1619420400.0,
2968          "contexts": {
2969            "browser": {
2970              "name": "Chrome",
2971              "version": "120.1.1",
2972              "type": "browser",
2973            },
2974          },
2975          "measurements": {
2976            "a": {
2977              "value": 213.0,
2978              "unit": "millisecond",
2979            },
2980            "b": {
2981              "value": 213.0,
2982              "unit": "millisecond",
2983            },
2984            "score.a": {
2985              "value": 0.33333215313291975,
2986              "unit": "ratio",
2987            },
2988            "score.b": {
2989              "value": 0.66666415149198,
2990              "unit": "ratio",
2991            },
2992            "score.ratio.a": {
2993              "value": 0.9999964593987591,
2994              "unit": "ratio",
2995            },
2996            "score.ratio.b": {
2997              "value": 0.9999962272379699,
2998              "unit": "ratio",
2999            },
3000            "score.total": {
3001              "value": 0.9999963046248997,
3002              "unit": "ratio",
3003            },
3004            "score.weight.a": {
3005              "value": 0.33333333333333337,
3006              "unit": "ratio",
3007            },
3008            "score.weight.b": {
3009              "value": 0.6666666666666667,
3010              "unit": "ratio",
3011            },
3012            "score.weight.c": {
3013              "value": 0.0,
3014              "unit": "ratio",
3015            },
3016          },
3017        }
3018        "###);
3019    }
3020
3021    #[test]
3022    fn test_computed_performance_score_weight_0() {
3023        let json = r#"
3024        {
3025            "type": "transaction",
3026            "timestamp": "2021-04-26T08:00:05+0100",
3027            "start_timestamp": "2021-04-26T08:00:00+0100",
3028            "measurements": {
3029                "cls": {"value": 0.11}
3030            }
3031        }
3032        "#;
3033
3034        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3035
3036        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3037            "profiles": [
3038                {
3039                    "name": "Desktop",
3040                    "scoreComponents": [
3041                        {
3042                            "measurement": "cls",
3043                            "weight": 0,
3044                            "p10": 0.1,
3045                            "p50": 0.25
3046                        },
3047                    ],
3048                    "condition": {
3049                        "op":"and",
3050                        "inner": []
3051                    }
3052                }
3053            ]
3054        }))
3055        .unwrap();
3056
3057        normalize_performance_score(&mut event, Some(&performance_score));
3058
3059        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3060        {
3061          "type": "transaction",
3062          "timestamp": 1619420405.0,
3063          "start_timestamp": 1619420400.0,
3064          "measurements": {
3065            "cls": {
3066              "value": 0.11,
3067            },
3068          },
3069        }
3070        "###);
3071    }
3072
3073    #[test]
3074    fn test_computed_performance_score_negative_value() {
3075        let json = r#"
3076        {
3077            "type": "transaction",
3078            "timestamp": "2021-04-26T08:00:05+0100",
3079            "start_timestamp": "2021-04-26T08:00:00+0100",
3080            "measurements": {
3081                "ttfb": {"value": -100, "unit": "millisecond"}
3082            }
3083        }
3084        "#;
3085
3086        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3087
3088        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3089            "profiles": [
3090                {
3091                    "name": "Desktop",
3092                    "scoreComponents": [
3093                        {
3094                            "measurement": "ttfb",
3095                            "weight": 1.0,
3096                            "p10": 100.0,
3097                            "p50": 250.0
3098                        },
3099                    ],
3100                    "condition": {
3101                        "op":"and",
3102                        "inner": []
3103                    }
3104                }
3105            ]
3106        }))
3107        .unwrap();
3108
3109        normalize_performance_score(&mut event, Some(&performance_score));
3110
3111        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3112        {
3113          "type": "transaction",
3114          "timestamp": 1619420405.0,
3115          "start_timestamp": 1619420400.0,
3116          "measurements": {
3117            "score.ratio.ttfb": {
3118              "value": 1.0,
3119              "unit": "ratio",
3120            },
3121            "score.total": {
3122              "value": 1.0,
3123              "unit": "ratio",
3124            },
3125            "score.ttfb": {
3126              "value": 1.0,
3127              "unit": "ratio",
3128            },
3129            "score.weight.ttfb": {
3130              "value": 1.0,
3131              "unit": "ratio",
3132            },
3133            "ttfb": {
3134              "value": -100.0,
3135              "unit": "millisecond",
3136            },
3137          },
3138        }
3139        "###);
3140    }
3141
3142    #[test]
3143    fn test_filter_negative_web_vital_measurements() {
3144        let json = r#"
3145        {
3146            "type": "transaction",
3147            "timestamp": "2021-04-26T08:00:05+0100",
3148            "start_timestamp": "2021-04-26T08:00:00+0100",
3149            "measurements": {
3150                "ttfb": {"value": -100, "unit": "millisecond"}
3151            }
3152        }
3153        "#;
3154        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3155
3156        // Allow ttfb as a builtinMeasurement with allow_negative defaulted to false.
3157        let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3158            "builtinMeasurements": [
3159                {"name": "ttfb", "unit": "millisecond"},
3160            ],
3161        }))
3162        .unwrap();
3163
3164        let dynamic_measurement_config =
3165            CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3166
3167        normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3168
3169        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3170        {
3171          "type": "transaction",
3172          "timestamp": 1619420405.0,
3173          "start_timestamp": 1619420400.0,
3174          "measurements": {},
3175          "_meta": {
3176            "measurements": {
3177              "": Meta(Some(MetaInner(
3178                err: [
3179                  [
3180                    "invalid_data",
3181                    {
3182                      "reason": "Negative value for measurement ttfb not allowed: -100",
3183                    },
3184                  ],
3185                ],
3186                val: Some({
3187                  "ttfb": {
3188                    "unit": "millisecond",
3189                    "value": -100.0,
3190                  },
3191                }),
3192              ))),
3193            },
3194          },
3195        }
3196        "###);
3197    }
3198
3199    #[test]
3200    fn test_computed_performance_score_multiple_profiles() {
3201        let json = r#"
3202        {
3203            "type": "transaction",
3204            "timestamp": "2021-04-26T08:00:05+0100",
3205            "start_timestamp": "2021-04-26T08:00:00+0100",
3206            "measurements": {
3207                "cls": {"value": 0.11},
3208                "inp": {"value": 120.0}
3209            }
3210        }
3211        "#;
3212
3213        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3214
3215        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3216            "profiles": [
3217                {
3218                    "name": "Desktop",
3219                    "scoreComponents": [
3220                        {
3221                            "measurement": "cls",
3222                            "weight": 0,
3223                            "p10": 0.1,
3224                            "p50": 0.25
3225                        },
3226                    ],
3227                    "condition": {
3228                        "op":"and",
3229                        "inner": []
3230                    }
3231                },
3232                {
3233                    "name": "Desktop",
3234                    "scoreComponents": [
3235                        {
3236                            "measurement": "inp",
3237                            "weight": 1.0,
3238                            "p10": 0.1,
3239                            "p50": 0.25
3240                        },
3241                    ],
3242                    "condition": {
3243                        "op":"and",
3244                        "inner": []
3245                    }
3246                }
3247            ]
3248        }))
3249        .unwrap();
3250
3251        normalize_performance_score(&mut event, Some(&performance_score));
3252
3253        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3254        {
3255          "type": "transaction",
3256          "timestamp": 1619420405.0,
3257          "start_timestamp": 1619420400.0,
3258          "measurements": {
3259            "cls": {
3260              "value": 0.11,
3261            },
3262            "inp": {
3263              "value": 120.0,
3264            },
3265            "score.inp": {
3266              "value": 0.0,
3267              "unit": "ratio",
3268            },
3269            "score.ratio.inp": {
3270              "value": 0.0,
3271              "unit": "ratio",
3272            },
3273            "score.total": {
3274              "value": 0.0,
3275              "unit": "ratio",
3276            },
3277            "score.weight.inp": {
3278              "value": 1.0,
3279              "unit": "ratio",
3280            },
3281          },
3282        }
3283        "###);
3284    }
3285
3286    #[test]
3287    fn test_compute_performance_score_for_mobile_ios_profile() {
3288        let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3289            .unwrap()
3290            .0
3291            .unwrap();
3292
3293        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3294            "profiles": [
3295                {
3296                    "name": "Mobile",
3297                    "scoreComponents": [
3298                        {
3299                            "measurement": "time_to_initial_display",
3300                            "weight": 0.25,
3301                            "p10": 1800.0,
3302                            "p50": 3000.0,
3303                            "optional": true
3304                        },
3305                        {
3306                            "measurement": "time_to_full_display",
3307                            "weight": 0.25,
3308                            "p10": 2500.0,
3309                            "p50": 4000.0,
3310                            "optional": true
3311                        },
3312                        {
3313                            "measurement": "app_start_warm",
3314                            "weight": 0.25,
3315                            "p10": 200.0,
3316                            "p50": 500.0,
3317                            "optional": true
3318                        },
3319                        {
3320                            "measurement": "app_start_cold",
3321                            "weight": 0.25,
3322                            "p10": 200.0,
3323                            "p50": 500.0,
3324                            "optional": true
3325                        }
3326                    ],
3327                    "condition": {
3328                        "op": "and",
3329                        "inner": [
3330                            {
3331                                "op": "or",
3332                                "inner": [
3333                                    {
3334                                        "op": "eq",
3335                                        "name": "event.sdk.name",
3336                                        "value": "sentry.cocoa"
3337                                    },
3338                                    {
3339                                        "op": "eq",
3340                                        "name": "event.sdk.name",
3341                                        "value": "sentry.java.android"
3342                                    }
3343                                ]
3344                            },
3345                            {
3346                                "op": "eq",
3347                                "name": "event.contexts.trace.op",
3348                                "value": "ui.load"
3349                            }
3350                        ]
3351                    }
3352                }
3353            ]
3354        }))
3355        .unwrap();
3356
3357        normalize_performance_score(&mut event, Some(&performance_score));
3358
3359        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3360    }
3361
3362    #[test]
3363    fn test_compute_performance_score_for_mobile_android_profile() {
3364        let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3365            .unwrap()
3366            .0
3367            .unwrap();
3368
3369        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3370            "profiles": [
3371                {
3372                    "name": "Mobile",
3373                    "scoreComponents": [
3374                        {
3375                            "measurement": "time_to_initial_display",
3376                            "weight": 0.25,
3377                            "p10": 1800.0,
3378                            "p50": 3000.0,
3379                            "optional": true
3380                        },
3381                        {
3382                            "measurement": "time_to_full_display",
3383                            "weight": 0.25,
3384                            "p10": 2500.0,
3385                            "p50": 4000.0,
3386                            "optional": true
3387                        },
3388                        {
3389                            "measurement": "app_start_warm",
3390                            "weight": 0.25,
3391                            "p10": 200.0,
3392                            "p50": 500.0,
3393                            "optional": true
3394                        },
3395                        {
3396                            "measurement": "app_start_cold",
3397                            "weight": 0.25,
3398                            "p10": 200.0,
3399                            "p50": 500.0,
3400                            "optional": true
3401                        }
3402                    ],
3403                    "condition": {
3404                        "op": "and",
3405                        "inner": [
3406                            {
3407                                "op": "or",
3408                                "inner": [
3409                                    {
3410                                        "op": "eq",
3411                                        "name": "event.sdk.name",
3412                                        "value": "sentry.cocoa"
3413                                    },
3414                                    {
3415                                        "op": "eq",
3416                                        "name": "event.sdk.name",
3417                                        "value": "sentry.java.android"
3418                                    }
3419                                ]
3420                            },
3421                            {
3422                                "op": "eq",
3423                                "name": "event.contexts.trace.op",
3424                                "value": "ui.load"
3425                            }
3426                        ]
3427                    }
3428                }
3429            ]
3430        }))
3431        .unwrap();
3432
3433        normalize_performance_score(&mut event, Some(&performance_score));
3434
3435        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3436    }
3437
3438    #[test]
3439    fn test_computes_performance_score_and_tags_with_profile_version() {
3440        let json = r#"
3441        {
3442            "type": "transaction",
3443            "timestamp": "2021-04-26T08:00:05+0100",
3444            "start_timestamp": "2021-04-26T08:00:00+0100",
3445            "measurements": {
3446                "inp": {"value": 120.0}
3447            }
3448        }
3449        "#;
3450
3451        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3452
3453        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3454            "profiles": [
3455                {
3456                    "name": "Desktop",
3457                    "scoreComponents": [
3458                        {
3459                            "measurement": "inp",
3460                            "weight": 1.0,
3461                            "p10": 0.1,
3462                            "p50": 0.25
3463                        },
3464                    ],
3465                    "condition": {
3466                        "op":"and",
3467                        "inner": []
3468                    },
3469                    "version": "beta"
3470                }
3471            ]
3472        }))
3473        .unwrap();
3474
3475        normalize(
3476            &mut event,
3477            &mut Meta::default(),
3478            &NormalizationConfig {
3479                performance_score: Some(&performance_score),
3480                ..Default::default()
3481            },
3482        );
3483
3484        insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3485        {
3486          "performance_score": {
3487            "score_profile_version": "beta",
3488            "type": "performancescore",
3489          },
3490        }
3491        "###);
3492        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3493        {
3494          "inp": {
3495            "value": 120.0,
3496            "unit": "millisecond",
3497          },
3498          "score.inp": {
3499            "value": 0.0,
3500            "unit": "ratio",
3501          },
3502          "score.ratio.inp": {
3503            "value": 0.0,
3504            "unit": "ratio",
3505          },
3506          "score.total": {
3507            "value": 0.0,
3508            "unit": "ratio",
3509          },
3510          "score.weight.inp": {
3511            "value": 1.0,
3512            "unit": "ratio",
3513          },
3514        }
3515        "###);
3516    }
3517
3518    #[test]
3519    fn test_computes_standalone_cls_performance_score() {
3520        let json = r#"
3521        {
3522            "type": "transaction",
3523            "timestamp": "2021-04-26T08:00:05+0100",
3524            "start_timestamp": "2021-04-26T08:00:00+0100",
3525            "measurements": {
3526                "cls": {"value": 0.5}
3527            }
3528        }
3529        "#;
3530
3531        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3532
3533        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3534            "profiles": [
3535            {
3536                "name": "Default",
3537                "scoreComponents": [
3538                    {
3539                        "measurement": "fcp",
3540                        "weight": 0.15,
3541                        "p10": 900.0,
3542                        "p50": 1600.0,
3543                        "optional": true,
3544                    },
3545                    {
3546                        "measurement": "lcp",
3547                        "weight": 0.30,
3548                        "p10": 1200.0,
3549                        "p50": 2400.0,
3550                        "optional": true,
3551                    },
3552                    {
3553                        "measurement": "cls",
3554                        "weight": 0.15,
3555                        "p10": 0.1,
3556                        "p50": 0.25,
3557                        "optional": true,
3558                    },
3559                    {
3560                        "measurement": "ttfb",
3561                        "weight": 0.10,
3562                        "p10": 200.0,
3563                        "p50": 400.0,
3564                        "optional": true,
3565                    },
3566                ],
3567                "condition": {
3568                    "op": "and",
3569                    "inner": [],
3570                },
3571            }
3572            ]
3573        }))
3574        .unwrap();
3575
3576        normalize(
3577            &mut event,
3578            &mut Meta::default(),
3579            &NormalizationConfig {
3580                performance_score: Some(&performance_score),
3581                ..Default::default()
3582            },
3583        );
3584
3585        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3586        {
3587          "cls": {
3588            "value": 0.5,
3589            "unit": "none",
3590          },
3591          "score.cls": {
3592            "value": 0.16615877613713903,
3593            "unit": "ratio",
3594          },
3595          "score.ratio.cls": {
3596            "value": 0.16615877613713903,
3597            "unit": "ratio",
3598          },
3599          "score.total": {
3600            "value": 0.16615877613713903,
3601            "unit": "ratio",
3602          },
3603          "score.weight.cls": {
3604            "value": 1.0,
3605            "unit": "ratio",
3606          },
3607          "score.weight.fcp": {
3608            "value": 0.0,
3609            "unit": "ratio",
3610          },
3611          "score.weight.lcp": {
3612            "value": 0.0,
3613            "unit": "ratio",
3614          },
3615          "score.weight.ttfb": {
3616            "value": 0.0,
3617            "unit": "ratio",
3618          },
3619        }
3620        "###);
3621    }
3622
3623    #[test]
3624    fn test_computes_standalone_lcp_performance_score() {
3625        let json = r#"
3626        {
3627            "type": "transaction",
3628            "timestamp": "2021-04-26T08:00:05+0100",
3629            "start_timestamp": "2021-04-26T08:00:00+0100",
3630            "measurements": {
3631                "lcp": {"value": 1200.0}
3632            }
3633        }
3634        "#;
3635
3636        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3637
3638        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3639            "profiles": [
3640            {
3641                "name": "Default",
3642                "scoreComponents": [
3643                    {
3644                        "measurement": "fcp",
3645                        "weight": 0.15,
3646                        "p10": 900.0,
3647                        "p50": 1600.0,
3648                        "optional": true,
3649                    },
3650                    {
3651                        "measurement": "lcp",
3652                        "weight": 0.30,
3653                        "p10": 1200.0,
3654                        "p50": 2400.0,
3655                        "optional": true,
3656                    },
3657                    {
3658                        "measurement": "cls",
3659                        "weight": 0.15,
3660                        "p10": 0.1,
3661                        "p50": 0.25,
3662                        "optional": true,
3663                    },
3664                    {
3665                        "measurement": "ttfb",
3666                        "weight": 0.10,
3667                        "p10": 200.0,
3668                        "p50": 400.0,
3669                        "optional": true,
3670                    },
3671                ],
3672                "condition": {
3673                    "op": "and",
3674                    "inner": [],
3675                },
3676            }
3677            ]
3678        }))
3679        .unwrap();
3680
3681        normalize(
3682            &mut event,
3683            &mut Meta::default(),
3684            &NormalizationConfig {
3685                performance_score: Some(&performance_score),
3686                ..Default::default()
3687            },
3688        );
3689
3690        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3691        {
3692          "lcp": {
3693            "value": 1200.0,
3694            "unit": "millisecond",
3695          },
3696          "score.lcp": {
3697            "value": 0.8999999314038525,
3698            "unit": "ratio",
3699          },
3700          "score.ratio.lcp": {
3701            "value": 0.8999999314038525,
3702            "unit": "ratio",
3703          },
3704          "score.total": {
3705            "value": 0.8999999314038525,
3706            "unit": "ratio",
3707          },
3708          "score.weight.cls": {
3709            "value": 0.0,
3710            "unit": "ratio",
3711          },
3712          "score.weight.fcp": {
3713            "value": 0.0,
3714            "unit": "ratio",
3715          },
3716          "score.weight.lcp": {
3717            "value": 1.0,
3718            "unit": "ratio",
3719          },
3720          "score.weight.ttfb": {
3721            "value": 0.0,
3722            "unit": "ratio",
3723          },
3724        }
3725        "###);
3726    }
3727
3728    #[test]
3729    fn test_computed_performance_score_uses_first_matching_profile() {
3730        let json = r#"
3731        {
3732            "type": "transaction",
3733            "timestamp": "2021-04-26T08:00:05+0100",
3734            "start_timestamp": "2021-04-26T08:00:00+0100",
3735            "measurements": {
3736                "a": {"value": 213, "unit": "millisecond"},
3737                "b": {"value": 213, "unit": "millisecond"}
3738            },
3739            "contexts": {
3740                "browser": {
3741                    "name": "Chrome",
3742                    "version": "120.1.1",
3743                    "type": "browser"
3744                }
3745            }
3746        }
3747        "#;
3748
3749        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3750
3751        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3752            "profiles": [
3753                {
3754                    "name": "Mobile",
3755                    "scoreComponents": [
3756                        {
3757                            "measurement": "a",
3758                            "weight": 0.15,
3759                            "p10": 100,
3760                            "p50": 200,
3761                        },
3762                        {
3763                            "measurement": "b",
3764                            "weight": 0.30,
3765                            "p10": 100,
3766                            "p50": 200,
3767                            "optional": true
3768                        },
3769                        {
3770                            "measurement": "c",
3771                            "weight": 0.55,
3772                            "p10": 100,
3773                            "p50": 200,
3774                            "optional": true
3775                        },
3776                    ],
3777                    "condition": {
3778                        "op":"eq",
3779                        "name": "event.contexts.browser.name",
3780                        "value": "Chrome Mobile"
3781                    }
3782                },
3783                {
3784                    "name": "Desktop",
3785                    "scoreComponents": [
3786                        {
3787                            "measurement": "a",
3788                            "weight": 0.15,
3789                            "p10": 900,
3790                            "p50": 1600,
3791                        },
3792                        {
3793                            "measurement": "b",
3794                            "weight": 0.30,
3795                            "p10": 1200,
3796                            "p50": 2400,
3797                            "optional": true
3798                        },
3799                        {
3800                            "measurement": "c",
3801                            "weight": 0.55,
3802                            "p10": 1200,
3803                            "p50": 2400,
3804                            "optional": true
3805                        },
3806                    ],
3807                    "condition": {
3808                        "op":"eq",
3809                        "name": "event.contexts.browser.name",
3810                        "value": "Chrome"
3811                    }
3812                },
3813                {
3814                    "name": "Default",
3815                    "scoreComponents": [
3816                        {
3817                            "measurement": "a",
3818                            "weight": 0.15,
3819                            "p10": 100,
3820                            "p50": 200,
3821                        },
3822                        {
3823                            "measurement": "b",
3824                            "weight": 0.30,
3825                            "p10": 100,
3826                            "p50": 200,
3827                            "optional": true
3828                        },
3829                        {
3830                            "measurement": "c",
3831                            "weight": 0.55,
3832                            "p10": 100,
3833                            "p50": 200,
3834                            "optional": true
3835                        },
3836                    ],
3837                    "condition": {
3838                        "op": "and",
3839                        "inner": [],
3840                    }
3841                }
3842            ]
3843        }))
3844        .unwrap();
3845
3846        normalize_performance_score(&mut event, Some(&performance_score));
3847
3848        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3849        {
3850          "type": "transaction",
3851          "timestamp": 1619420405.0,
3852          "start_timestamp": 1619420400.0,
3853          "contexts": {
3854            "browser": {
3855              "name": "Chrome",
3856              "version": "120.1.1",
3857              "type": "browser",
3858            },
3859          },
3860          "measurements": {
3861            "a": {
3862              "value": 213.0,
3863              "unit": "millisecond",
3864            },
3865            "b": {
3866              "value": 213.0,
3867              "unit": "millisecond",
3868            },
3869            "score.a": {
3870              "value": 0.33333215313291975,
3871              "unit": "ratio",
3872            },
3873            "score.b": {
3874              "value": 0.66666415149198,
3875              "unit": "ratio",
3876            },
3877            "score.ratio.a": {
3878              "value": 0.9999964593987591,
3879              "unit": "ratio",
3880            },
3881            "score.ratio.b": {
3882              "value": 0.9999962272379699,
3883              "unit": "ratio",
3884            },
3885            "score.total": {
3886              "value": 0.9999963046248997,
3887              "unit": "ratio",
3888            },
3889            "score.weight.a": {
3890              "value": 0.33333333333333337,
3891              "unit": "ratio",
3892            },
3893            "score.weight.b": {
3894              "value": 0.6666666666666667,
3895              "unit": "ratio",
3896            },
3897            "score.weight.c": {
3898              "value": 0.0,
3899              "unit": "ratio",
3900            },
3901          },
3902        }
3903        "###);
3904    }
3905
3906    #[test]
3907    fn test_computed_performance_score_falls_back_to_default_profile() {
3908        let json = r#"
3909        {
3910            "type": "transaction",
3911            "timestamp": "2021-04-26T08:00:05+0100",
3912            "start_timestamp": "2021-04-26T08:00:00+0100",
3913            "measurements": {
3914                "a": {"value": 213, "unit": "millisecond"},
3915                "b": {"value": 213, "unit": "millisecond"}
3916            },
3917            "contexts": {}
3918        }
3919        "#;
3920
3921        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3922
3923        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3924            "profiles": [
3925                {
3926                    "name": "Mobile",
3927                    "scoreComponents": [
3928                        {
3929                            "measurement": "a",
3930                            "weight": 0.15,
3931                            "p10": 900,
3932                            "p50": 1600,
3933                            "optional": true
3934                        },
3935                        {
3936                            "measurement": "b",
3937                            "weight": 0.30,
3938                            "p10": 1200,
3939                            "p50": 2400,
3940                            "optional": true
3941                        },
3942                        {
3943                            "measurement": "c",
3944                            "weight": 0.55,
3945                            "p10": 1200,
3946                            "p50": 2400,
3947                            "optional": true
3948                        },
3949                    ],
3950                    "condition": {
3951                        "op":"eq",
3952                        "name": "event.contexts.browser.name",
3953                        "value": "Chrome Mobile"
3954                    }
3955                },
3956                {
3957                    "name": "Desktop",
3958                    "scoreComponents": [
3959                        {
3960                            "measurement": "a",
3961                            "weight": 0.15,
3962                            "p10": 900,
3963                            "p50": 1600,
3964                            "optional": true
3965                        },
3966                        {
3967                            "measurement": "b",
3968                            "weight": 0.30,
3969                            "p10": 1200,
3970                            "p50": 2400,
3971                            "optional": true
3972                        },
3973                        {
3974                            "measurement": "c",
3975                            "weight": 0.55,
3976                            "p10": 1200,
3977                            "p50": 2400,
3978                            "optional": true
3979                        },
3980                    ],
3981                    "condition": {
3982                        "op":"eq",
3983                        "name": "event.contexts.browser.name",
3984                        "value": "Chrome"
3985                    }
3986                },
3987                {
3988                    "name": "Default",
3989                    "scoreComponents": [
3990                        {
3991                            "measurement": "a",
3992                            "weight": 0.15,
3993                            "p10": 100,
3994                            "p50": 200,
3995                            "optional": true
3996                        },
3997                        {
3998                            "measurement": "b",
3999                            "weight": 0.30,
4000                            "p10": 100,
4001                            "p50": 200,
4002                            "optional": true
4003                        },
4004                        {
4005                            "measurement": "c",
4006                            "weight": 0.55,
4007                            "p10": 100,
4008                            "p50": 200,
4009                            "optional": true
4010                        },
4011                    ],
4012                    "condition": {
4013                        "op": "and",
4014                        "inner": [],
4015                    }
4016                }
4017            ]
4018        }))
4019        .unwrap();
4020
4021        normalize_performance_score(&mut event, Some(&performance_score));
4022
4023        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4024        {
4025          "type": "transaction",
4026          "timestamp": 1619420405.0,
4027          "start_timestamp": 1619420400.0,
4028          "contexts": {},
4029          "measurements": {
4030            "a": {
4031              "value": 213.0,
4032              "unit": "millisecond",
4033            },
4034            "b": {
4035              "value": 213.0,
4036              "unit": "millisecond",
4037            },
4038            "score.a": {
4039              "value": 0.15121816827413334,
4040              "unit": "ratio",
4041            },
4042            "score.b": {
4043              "value": 0.3024363365482667,
4044              "unit": "ratio",
4045            },
4046            "score.ratio.a": {
4047              "value": 0.45365450482239994,
4048              "unit": "ratio",
4049            },
4050            "score.ratio.b": {
4051              "value": 0.45365450482239994,
4052              "unit": "ratio",
4053            },
4054            "score.total": {
4055              "value": 0.4536545048224,
4056              "unit": "ratio",
4057            },
4058            "score.weight.a": {
4059              "value": 0.33333333333333337,
4060              "unit": "ratio",
4061            },
4062            "score.weight.b": {
4063              "value": 0.6666666666666667,
4064              "unit": "ratio",
4065            },
4066            "score.weight.c": {
4067              "value": 0.0,
4068              "unit": "ratio",
4069            },
4070          },
4071        }
4072        "###);
4073    }
4074
4075    #[test]
4076    fn test_normalization_removes_reprocessing_context() {
4077        let json = r#"{
4078            "contexts": {
4079                "reprocessing": {}
4080            }
4081        }"#;
4082        let mut event = Annotated::<Event>::from_json(json).unwrap();
4083        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4084        normalize_event(&mut event, &NormalizationConfig::default());
4085        assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4086    }
4087
4088    #[test]
4089    fn test_renormalization_does_not_remove_reprocessing_context() {
4090        let json = r#"{
4091            "contexts": {
4092                "reprocessing": {}
4093            }
4094        }"#;
4095        let mut event = Annotated::<Event>::from_json(json).unwrap();
4096        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4097        normalize_event(
4098            &mut event,
4099            &NormalizationConfig {
4100                is_renormalize: true,
4101                ..Default::default()
4102            },
4103        );
4104        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4105    }
4106
4107    #[test]
4108    fn test_normalize_user() {
4109        let json = r#"{
4110            "user": {
4111                "id": "123456",
4112                "username": "john",
4113                "other": "value"
4114            }
4115        }"#;
4116        let mut event = Annotated::<Event>::from_json(json).unwrap();
4117        normalize_user(event.value_mut().as_mut().unwrap());
4118
4119        let user = event.value().unwrap().user.value().unwrap();
4120        assert_eq!(user.data, {
4121            let mut map = Object::new();
4122            map.insert(
4123                "other".to_string(),
4124                Annotated::new(Value::String("value".to_owned())),
4125            );
4126            Annotated::new(map)
4127        });
4128        assert_eq!(user.other, Object::new());
4129        assert_eq!(user.username, Annotated::new("john".to_string().into()));
4130        assert_eq!(user.sentry_user, Annotated::new("id:123456".to_string()));
4131    }
4132
4133    #[test]
4134    fn test_handle_types_in_spaced_exception_values() {
4135        let mut exception = Annotated::new(Exception {
4136            value: Annotated::new("ValueError: unauthorized".to_string().into()),
4137            ..Exception::default()
4138        });
4139        normalize_exception(&mut exception);
4140
4141        let exception = exception.value().unwrap();
4142        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4143        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4144    }
4145
4146    #[test]
4147    fn test_handle_types_in_non_spaced_excepton_values() {
4148        let mut exception = Annotated::new(Exception {
4149            value: Annotated::new("ValueError:unauthorized".to_string().into()),
4150            ..Exception::default()
4151        });
4152        normalize_exception(&mut exception);
4153
4154        let exception = exception.value().unwrap();
4155        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4156        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4157    }
4158
4159    #[test]
4160    fn test_rejects_empty_exception_fields() {
4161        let mut exception = Annotated::new(Exception {
4162            value: Annotated::new("".to_string().into()),
4163            ty: Annotated::new("".to_string()),
4164            ..Default::default()
4165        });
4166
4167        normalize_exception(&mut exception);
4168
4169        assert!(exception.value().is_none());
4170        assert!(exception.meta().has_errors());
4171    }
4172
4173    #[test]
4174    fn test_json_value() {
4175        let mut exception = Annotated::new(Exception {
4176            value: Annotated::new(r#"{"unauthorized":true}"#.to_string().into()),
4177            ..Exception::default()
4178        });
4179
4180        normalize_exception(&mut exception);
4181
4182        let exception = exception.value().unwrap();
4183
4184        // Don't split a json-serialized value on the colon
4185        assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4186        assert_eq!(exception.ty.value(), None);
4187    }
4188
4189    #[test]
4190    fn test_exception_invalid() {
4191        let mut exception = Annotated::new(Exception::default());
4192
4193        normalize_exception(&mut exception);
4194
4195        let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4196            error.insert("attribute", "type or value");
4197        });
4198        assert_eq!(
4199            exception.meta().iter_errors().collect_tuple(),
4200            Some((&expected,))
4201        );
4202    }
4203
4204    #[test]
4205    fn test_normalize_exception() {
4206        let mut event = Annotated::new(Event {
4207            exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4208                // Exception with missing type and value
4209                ty: Annotated::empty(),
4210                value: Annotated::empty(),
4211                ..Default::default()
4212            })])),
4213            ..Default::default()
4214        });
4215
4216        normalize_event(&mut event, &NormalizationConfig::default());
4217
4218        let exception = event
4219            .value()
4220            .unwrap()
4221            .exceptions
4222            .value()
4223            .unwrap()
4224            .values
4225            .value()
4226            .unwrap()
4227            .first()
4228            .unwrap();
4229
4230        assert_debug_snapshot!(exception.meta(), @r#"
4231        Meta {
4232            remarks: [],
4233            errors: [
4234                Error {
4235                    kind: MissingAttribute,
4236                    data: {
4237                        "attribute": String(
4238                            "type or value",
4239                        ),
4240                    },
4241                },
4242            ],
4243            original_length: None,
4244            original_value: Some(
4245                Object(
4246                    {
4247                        "mechanism": ~,
4248                        "module": ~,
4249                        "raw_stacktrace": ~,
4250                        "stacktrace": ~,
4251                        "thread_id": ~,
4252                        "type": ~,
4253                        "value": ~,
4254                    },
4255                ),
4256            ),
4257        }"#);
4258    }
4259
4260    #[test]
4261    fn test_normalize_breadcrumbs() {
4262        let mut event = Event {
4263            breadcrumbs: Annotated::new(Values {
4264                values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4265                ..Default::default()
4266            }),
4267            ..Default::default()
4268        };
4269        normalize_breadcrumbs(&mut event);
4270
4271        let breadcrumb = event
4272            .breadcrumbs
4273            .value()
4274            .unwrap()
4275            .values
4276            .value()
4277            .unwrap()
4278            .first()
4279            .unwrap()
4280            .value()
4281            .unwrap();
4282        assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4283        assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4284    }
4285
4286    #[test]
4287    fn test_other_debug_images_have_meta_errors() {
4288        let mut event = Event {
4289            debug_meta: Annotated::new(DebugMeta {
4290                images: Annotated::new(vec![Annotated::new(
4291                    DebugImage::Other(BTreeMap::default()),
4292                )]),
4293                ..Default::default()
4294            }),
4295            ..Default::default()
4296        };
4297        normalize_debug_meta(&mut event);
4298
4299        let debug_image_meta = event
4300            .debug_meta
4301            .value()
4302            .unwrap()
4303            .images
4304            .value()
4305            .unwrap()
4306            .first()
4307            .unwrap()
4308            .meta();
4309        assert_debug_snapshot!(debug_image_meta, @r#"
4310        Meta {
4311            remarks: [],
4312            errors: [
4313                Error {
4314                    kind: InvalidData,
4315                    data: {
4316                        "reason": String(
4317                            "unsupported debug image type",
4318                        ),
4319                    },
4320                },
4321            ],
4322            original_length: None,
4323            original_value: Some(
4324                Object(
4325                    {},
4326                ),
4327            ),
4328        }"#);
4329    }
4330
4331    #[test]
4332    fn test_skip_span_normalization_when_configured() {
4333        let json = r#"{
4334            "type": "transaction",
4335            "start_timestamp": 1,
4336            "timestamp": 2,
4337            "contexts": {
4338                "trace": {
4339                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4340                    "span_id": "aaaaaaaaaaaaaaaa"
4341                }
4342            },
4343            "spans": [
4344                {
4345                    "op": "db",
4346                    "description": "SELECT * FROM table;",
4347                    "start_timestamp": 1,
4348                    "timestamp": 2,
4349                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4350                    "span_id": "bbbbbbbbbbbbbbbb",
4351                    "parent_span_id": "aaaaaaaaaaaaaaaa"
4352                }
4353            ]
4354        }"#;
4355
4356        let mut event = Annotated::<Event>::from_json(json).unwrap();
4357        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4358        normalize_event(
4359            &mut event,
4360            &NormalizationConfig {
4361                is_renormalize: true,
4362                ..Default::default()
4363            },
4364        );
4365        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4366        normalize_event(
4367            &mut event,
4368            &NormalizationConfig {
4369                is_renormalize: false,
4370                ..Default::default()
4371            },
4372        );
4373        assert!(get_value!(event.spans[0].exclusive_time).is_some());
4374    }
4375
4376    #[test]
4377    fn test_normalize_trace_context_tags_extracts_lcp_info() {
4378        let json = r#"{
4379            "type": "transaction",
4380            "start_timestamp": 1,
4381            "timestamp": 2,
4382            "contexts": {
4383                "trace": {
4384                    "data": {
4385                        "lcp.element": "body > div#app > div > h1#header",
4386                        "lcp.size": 24827,
4387                        "lcp.id": "header",
4388                        "lcp.url": "http://example.com/image.jpg"
4389                    }
4390                }
4391            },
4392            "measurements": {
4393                "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4394            }
4395        }"#;
4396        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4397        normalize_trace_context_tags(&mut event);
4398        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
4399        {
4400          "type": "transaction",
4401          "timestamp": 2.0,
4402          "start_timestamp": 1.0,
4403          "contexts": {
4404            "trace": {
4405              "data": {
4406                "lcp.element": "body > div#app > div > h1#header",
4407                "lcp.size": 24827,
4408                "lcp.id": "header",
4409                "lcp.url": "http://example.com/image.jpg",
4410              },
4411              "type": "trace",
4412            },
4413          },
4414          "tags": [
4415            [
4416              "lcp.element",
4417              "body > div#app > div > h1#header",
4418            ],
4419            [
4420              "lcp.size",
4421              "24827",
4422            ],
4423            [
4424              "lcp.id",
4425              "header",
4426            ],
4427            [
4428              "lcp.url",
4429              "http://example.com/image.jpg",
4430            ],
4431          ],
4432          "measurements": {
4433            "lcp": {
4434              "value": 146.20000000298023,
4435              "unit": "millisecond",
4436            },
4437          },
4438        }
4439        "#);
4440    }
4441
4442    #[test]
4443    fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4444        let json = r#"{
4445          "type": "transaction",
4446          "start_timestamp": 1,
4447          "timestamp": 2,
4448          "contexts": {
4449              "trace": {
4450                  "data": {
4451                      "lcp.element": "body > div#app > div > h1#id",
4452                      "lcp.size": 33333,
4453                      "lcp.id": "id",
4454                      "lcp.url": "http://example.com/another-image.jpg"
4455                  }
4456              }
4457          },
4458          "tags": {
4459              "lcp.element": "body > div#app > div > h1#header",
4460              "lcp.size": 24827,
4461              "lcp.id": "header",
4462              "lcp.url": "http://example.com/image.jpg"
4463          },
4464          "measurements": {
4465              "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4466          }
4467        }"#;
4468        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4469        normalize_trace_context_tags(&mut event);
4470        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r#"
4471        {
4472          "type": "transaction",
4473          "timestamp": 2.0,
4474          "start_timestamp": 1.0,
4475          "contexts": {
4476            "trace": {
4477              "data": {
4478                "lcp.element": "body > div#app > div > h1#id",
4479                "lcp.size": 33333,
4480                "lcp.id": "id",
4481                "lcp.url": "http://example.com/another-image.jpg",
4482              },
4483              "type": "trace",
4484            },
4485          },
4486          "tags": [
4487            [
4488              "lcp.element",
4489              "body > div#app > div > h1#header",
4490            ],
4491            [
4492              "lcp.id",
4493              "header",
4494            ],
4495            [
4496              "lcp.size",
4497              "24827",
4498            ],
4499            [
4500              "lcp.url",
4501              "http://example.com/image.jpg",
4502            ],
4503          ],
4504          "measurements": {
4505            "lcp": {
4506              "value": 146.20000000298023,
4507              "unit": "millisecond",
4508            },
4509          },
4510        }
4511        "#);
4512    }
4513}