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