relay_event_normalization/
event.rs

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