relay_event_normalization/
event.rs

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