Skip to main content

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