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    // If there is no project or global config allow all the custom measurements through.
1395    let max_custom_measurements = measurements_config
1396        .max_custom_measurements()
1397        .unwrap_or(usize::MAX);
1398
1399    let mut custom_measurements_count = 0;
1400    let mut removed_measurements = Object::new();
1401
1402    measurements.retain(|name, value| {
1403        let measurement = match value.value_mut() {
1404            Some(m) => m,
1405            None => return false,
1406        };
1407
1408        if !can_be_valid_metric_name(name) {
1409            meta.add_error(Error::invalid(format!(
1410                "Metric name contains invalid characters: \"{name}\""
1411            )));
1412            removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1413            return false;
1414        }
1415
1416        // TODO(jjbayer): Should we actually normalize the unit into the event?
1417        let unit = measurement.unit.value().unwrap_or(&MetricUnit::None);
1418
1419        if let Some(max_name_and_unit_len) = max_name_and_unit_len {
1420            let max_name_len = max_name_and_unit_len - unit.to_string().len();
1421
1422            if name.len() > max_name_len {
1423                meta.add_error(Error::invalid(format!(
1424                    "Metric name too long {}/{max_name_len}: \"{name}\"",
1425                    name.len(),
1426                )));
1427                removed_measurements
1428                    .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1429                return false;
1430            }
1431        }
1432
1433        // Check if this is a builtin measurement:
1434        if let Some(builtin_measurement) = measurements_config
1435            .builtin_measurement_keys()
1436            .find(|builtin| builtin.name() == name)
1437        {
1438            let value = measurement.value.value().unwrap_or(&FiniteF64::ZERO);
1439            // Drop negative values if the builtin measurement does not allow them.
1440            if !builtin_measurement.allow_negative() && *value < 0.0 {
1441                meta.add_error(Error::invalid(format!(
1442                    "Negative value for measurement {name} not allowed: {value}",
1443                )));
1444                removed_measurements
1445                    .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1446                return false;
1447            }
1448            // If the unit matches a built-in measurement, we allow it.
1449            // If the name matches but the unit is wrong, we do not even accept it as a custom measurement,
1450            // and just drop it instead.
1451            return builtin_measurement.unit() == unit;
1452        }
1453
1454        // For custom measurements, check the budget:
1455        if custom_measurements_count < max_custom_measurements {
1456            custom_measurements_count += 1;
1457            return true;
1458        }
1459
1460        meta.add_error(Error::invalid(format!("Too many measurements: {name}")));
1461        removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1462
1463        false
1464    });
1465
1466    if !removed_measurements.is_empty() {
1467        meta.set_original_value(Some(removed_measurements));
1468    }
1469}
1470
1471/// Returns the unit of the provided metric.
1472///
1473/// For known measurements, this returns `Some(MetricUnit)`, which can also include
1474/// `Some(MetricUnit::None)`. For unknown measurement names, this returns `None`.
1475fn get_metric_measurement_unit(measurement_name: &str) -> Option<MetricUnit> {
1476    match measurement_name {
1477        // Web
1478        "fcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1479        "lcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1480        "fid" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1481        "fp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1482        "inp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1483        "ttfb" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1484        "ttfb.requesttime" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1485        "cls" => Some(MetricUnit::None),
1486
1487        // Mobile
1488        "app_start_cold" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1489        "app_start_warm" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1490        "frames_total" => Some(MetricUnit::None),
1491        "frames_slow" => Some(MetricUnit::None),
1492        "frames_slow_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1493        "frames_frozen" => Some(MetricUnit::None),
1494        "frames_frozen_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1495        "time_to_initial_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1496        "time_to_full_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1497
1498        // React-Native
1499        "stall_count" => Some(MetricUnit::None),
1500        "stall_total_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1501        "stall_longest_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1502        "stall_percentage" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1503
1504        // Default
1505        _ => None,
1506    }
1507}
1508
1509/// Replaces dot.case app start measurements keys with snake_case keys.
1510///
1511/// The dot.case app start measurements keys are treated as custom measurements.
1512/// The snake_case is the key expected by the Sentry UI to aggregate and display in graphs.
1513fn normalize_app_start_measurements(measurements: &mut Measurements) {
1514    if let Some(app_start_cold_value) = measurements.remove("app.start.cold") {
1515        measurements.insert("app_start_cold".to_owned(), app_start_cold_value);
1516    }
1517    if let Some(app_start_warm_value) = measurements.remove("app.start.warm") {
1518        measurements.insert("app_start_warm".to_owned(), app_start_warm_value);
1519    }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524
1525    use std::collections::BTreeMap;
1526    use std::collections::HashMap;
1527
1528    use insta::assert_debug_snapshot;
1529    use itertools::Itertools;
1530    use relay_event_schema::protocol::{Breadcrumb, Csp, DebugMeta, DeviceContext, Values};
1531    use relay_protocol::{SerializableAnnotated, get_value};
1532    use serde_json::json;
1533
1534    use super::*;
1535    use crate::{ClientHints, MeasurementsConfig, ModelCostV2};
1536
1537    const IOS_MOBILE_EVENT: &str = r#"
1538        {
1539            "sdk": {"name": "sentry.cocoa"},
1540            "contexts": {
1541                "trace": {
1542                    "op": "ui.load"
1543                }
1544            },
1545            "measurements": {
1546                "app_start_warm": {
1547                    "value": 8049.345970153808,
1548                    "unit": "millisecond"
1549                },
1550                "time_to_full_display": {
1551                    "value": 8240.571022033691,
1552                    "unit": "millisecond"
1553                },
1554                "time_to_initial_display": {
1555                    "value": 8049.345970153808,
1556                    "unit": "millisecond"
1557                }
1558            }
1559        }
1560        "#;
1561
1562    const ANDROID_MOBILE_EVENT: &str = r#"
1563        {
1564            "sdk": {"name": "sentry.java.android"},
1565            "contexts": {
1566                "trace": {
1567                    "op": "ui.load"
1568                }
1569            },
1570            "measurements": {
1571                "app_start_cold": {
1572                    "value": 22648,
1573                    "unit": "millisecond"
1574                },
1575                "time_to_full_display": {
1576                    "value": 22647,
1577                    "unit": "millisecond"
1578                },
1579                "time_to_initial_display": {
1580                    "value": 22647,
1581                    "unit": "millisecond"
1582                }
1583            }
1584        }
1585        "#;
1586
1587    #[test]
1588    fn test_normalize_dist_none() {
1589        let mut dist = Annotated::default();
1590        normalize_dist(&mut dist);
1591        assert_eq!(dist.value(), None);
1592    }
1593
1594    #[test]
1595    fn test_normalize_dist_empty() {
1596        let mut dist = Annotated::new("".to_owned());
1597        normalize_dist(&mut dist);
1598        assert_eq!(dist.value(), None);
1599    }
1600
1601    #[test]
1602    fn test_normalize_dist_trim() {
1603        let mut dist = Annotated::new(" foo  ".to_owned());
1604        normalize_dist(&mut dist);
1605        assert_eq!(dist.value(), Some(&"foo".to_owned()));
1606    }
1607
1608    #[test]
1609    fn test_normalize_dist_whitespace() {
1610        let mut dist = Annotated::new(" ".to_owned());
1611        normalize_dist(&mut dist);
1612        assert_eq!(dist.value(), None);
1613    }
1614
1615    #[test]
1616    fn test_normalize_platform_and_level_with_transaction_event() {
1617        let json = r#"
1618        {
1619            "type": "transaction"
1620        }
1621        "#;
1622
1623        let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1624        else {
1625            panic!("Invalid transaction json");
1626        };
1627
1628        normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1629
1630        assert_eq!(event.level.value().unwrap().to_string(), "info");
1631        assert_eq!(event.ty.value().unwrap().to_string(), "transaction");
1632        assert_eq!(event.platform.as_str().unwrap(), "other");
1633    }
1634
1635    #[test]
1636    fn test_normalize_platform_and_level_with_error_event() {
1637        let json = r#"
1638        {
1639            "type": "error",
1640            "exception": {
1641                "values": [{"type": "ValueError", "value": "Should not happen"}]
1642            }
1643        }
1644        "#;
1645
1646        let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1647        else {
1648            panic!("Invalid error json");
1649        };
1650
1651        normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1652
1653        assert_eq!(event.level.value().unwrap().to_string(), "error");
1654        assert_eq!(event.ty.value().unwrap().to_string(), "error");
1655        assert_eq!(event.platform.value().unwrap().to_owned(), "other");
1656    }
1657
1658    #[test]
1659    fn test_computed_measurements() {
1660        let json = r#"
1661        {
1662            "type": "transaction",
1663            "timestamp": "2021-04-26T08:00:05+0100",
1664            "start_timestamp": "2021-04-26T08:00:00+0100",
1665            "measurements": {
1666                "frames_slow": {"value": 1},
1667                "frames_frozen": {"value": 2},
1668                "frames_total": {"value": 4},
1669                "stall_total_time": {"value": 4000, "unit": "millisecond"}
1670            }
1671        }
1672        "#;
1673
1674        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1675
1676        normalize_event_measurements(&mut event, None, None);
1677
1678        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1679        {
1680          "type": "transaction",
1681          "timestamp": 1619420405.0,
1682          "start_timestamp": 1619420400.0,
1683          "measurements": {
1684            "frames_frozen": {
1685              "value": 2.0,
1686              "unit": "none",
1687            },
1688            "frames_frozen_rate": {
1689              "value": 0.5,
1690              "unit": "ratio",
1691            },
1692            "frames_slow": {
1693              "value": 1.0,
1694              "unit": "none",
1695            },
1696            "frames_slow_rate": {
1697              "value": 0.25,
1698              "unit": "ratio",
1699            },
1700            "frames_total": {
1701              "value": 4.0,
1702              "unit": "none",
1703            },
1704            "stall_percentage": {
1705              "value": 0.8,
1706              "unit": "ratio",
1707            },
1708            "stall_total_time": {
1709              "value": 4000.0,
1710              "unit": "millisecond",
1711            },
1712          },
1713        }
1714        "###);
1715    }
1716
1717    #[test]
1718    fn test_filter_custom_measurements() {
1719        let json = r#"
1720        {
1721            "type": "transaction",
1722            "timestamp": "2021-04-26T08:00:05+0100",
1723            "start_timestamp": "2021-04-26T08:00:00+0100",
1724            "measurements": {
1725                "my_custom_measurement_1": {"value": 123},
1726                "frames_frozen": {"value": 666, "unit": "invalid_unit"},
1727                "frames_slow": {"value": 1},
1728                "my_custom_measurement_3": {"value": 456},
1729                "my_custom_measurement_2": {"value": 789}
1730            }
1731        }
1732        "#;
1733        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1734
1735        let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
1736            "builtinMeasurements": [
1737                {"name": "frames_frozen", "unit": "none"},
1738                {"name": "frames_slow", "unit": "none"}
1739            ],
1740            "maxCustomMeasurements": 2,
1741            "stray_key": "zzz"
1742        }))
1743        .unwrap();
1744
1745        let dynamic_measurement_config =
1746            CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
1747
1748        normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
1749
1750        // Only two custom measurements are retained, in alphabetic order (1 and 2)
1751        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1752        {
1753          "type": "transaction",
1754          "timestamp": 1619420405.0,
1755          "start_timestamp": 1619420400.0,
1756          "measurements": {
1757            "frames_slow": {
1758              "value": 1.0,
1759              "unit": "none",
1760            },
1761            "my_custom_measurement_1": {
1762              "value": 123.0,
1763              "unit": "none",
1764            },
1765            "my_custom_measurement_2": {
1766              "value": 789.0,
1767              "unit": "none",
1768            },
1769          },
1770          "_meta": {
1771            "measurements": {
1772              "": Meta(Some(MetaInner(
1773                err: [
1774                  [
1775                    "invalid_data",
1776                    {
1777                      "reason": "Too many measurements: my_custom_measurement_3",
1778                    },
1779                  ],
1780                ],
1781                val: Some({
1782                  "my_custom_measurement_3": {
1783                    "unit": "none",
1784                    "value": 456.0,
1785                  },
1786                }),
1787              ))),
1788            },
1789          },
1790        }
1791        "###);
1792    }
1793
1794    #[test]
1795    fn test_normalize_units() {
1796        let mut measurements = Annotated::<Measurements>::from_json(
1797            r#"{
1798                "fcp": {"value": 1.1},
1799                "stall_count": {"value": 3.3},
1800                "foo": {"value": 8.8}
1801            }"#,
1802        )
1803        .unwrap()
1804        .into_value()
1805        .unwrap();
1806        insta::assert_debug_snapshot!(measurements, @r###"
1807        Measurements(
1808            {
1809                "fcp": Measurement {
1810                    value: 1.1,
1811                    unit: ~,
1812                },
1813                "foo": Measurement {
1814                    value: 8.8,
1815                    unit: ~,
1816                },
1817                "stall_count": Measurement {
1818                    value: 3.3,
1819                    unit: ~,
1820                },
1821            },
1822        )
1823        "###);
1824        normalize_units(&mut measurements);
1825        insta::assert_debug_snapshot!(measurements, @r###"
1826        Measurements(
1827            {
1828                "fcp": Measurement {
1829                    value: 1.1,
1830                    unit: Duration(
1831                        MilliSecond,
1832                    ),
1833                },
1834                "foo": Measurement {
1835                    value: 8.8,
1836                    unit: None,
1837                },
1838                "stall_count": Measurement {
1839                    value: 3.3,
1840                    unit: None,
1841                },
1842            },
1843        )
1844        "###);
1845    }
1846
1847    #[test]
1848    fn test_normalize_security_report() {
1849        let mut event = Event {
1850            csp: Annotated::from(Csp::default()),
1851            ..Default::default()
1852        };
1853        let ipaddr = IpAddr("213.164.1.114".to_owned());
1854
1855        let client_ip = Some(&ipaddr);
1856
1857        let user_agent = RawUserAgentInfo {
1858            user_agent: Some(
1859                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0",
1860            ),
1861            client_hints: ClientHints {
1862                sec_ch_ua_platform: Some("macOS"),
1863                sec_ch_ua_platform_version: Some("13.2.0"),
1864                sec_ch_ua: Some(
1865                    r#""Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110""#,
1866                ),
1867                sec_ch_ua_model: Some("some model"),
1868            },
1869        };
1870
1871        // This call should fill the event headers with info from the user_agent which is
1872        // tested below.
1873        normalize_security_report(&mut event, client_ip, &user_agent);
1874
1875        let headers = event
1876            .request
1877            .value_mut()
1878            .get_or_insert_with(Request::default)
1879            .headers
1880            .value_mut()
1881            .get_or_insert_with(Headers::default);
1882
1883        assert_eq!(
1884            event.user.value().unwrap().ip_address,
1885            Annotated::from(ipaddr)
1886        );
1887        assert_eq!(
1888            headers.get_header(RawUserAgentInfo::USER_AGENT),
1889            user_agent.user_agent
1890        );
1891        assert_eq!(
1892            headers.get_header(ClientHints::SEC_CH_UA),
1893            user_agent.client_hints.sec_ch_ua,
1894        );
1895        assert_eq!(
1896            headers.get_header(ClientHints::SEC_CH_UA_MODEL),
1897            user_agent.client_hints.sec_ch_ua_model,
1898        );
1899        assert_eq!(
1900            headers.get_header(ClientHints::SEC_CH_UA_PLATFORM),
1901            user_agent.client_hints.sec_ch_ua_platform,
1902        );
1903        assert_eq!(
1904            headers.get_header(ClientHints::SEC_CH_UA_PLATFORM_VERSION),
1905            user_agent.client_hints.sec_ch_ua_platform_version,
1906        );
1907
1908        assert!(
1909            std::mem::size_of_val(&ClientHints::<&str>::default()) == 64,
1910            "If you add new fields, update the test accordingly"
1911        );
1912    }
1913
1914    #[test]
1915    fn test_no_device_class() {
1916        let mut event = Event {
1917            ..Default::default()
1918        };
1919        normalize_device_class(&mut event);
1920        let tags = &event.tags.value_mut().get_or_insert_with(Tags::default).0;
1921        assert_eq!(None, tags.get("device_class"));
1922    }
1923
1924    #[test]
1925    fn test_apple_low_device_class() {
1926        let mut event = Event {
1927            contexts: {
1928                let mut contexts = Contexts::new();
1929                contexts.add(DeviceContext {
1930                    family: "iPhone".to_owned().into(),
1931                    model: "iPhone8,4".to_owned().into(),
1932                    ..Default::default()
1933                });
1934                Annotated::new(contexts)
1935            },
1936            ..Default::default()
1937        };
1938        normalize_device_class(&mut event);
1939        assert_debug_snapshot!(event.tags, @r###"
1940        Tags(
1941            PairList(
1942                [
1943                    TagEntry(
1944                        "device.class",
1945                        "1",
1946                    ),
1947                ],
1948            ),
1949        )
1950        "###);
1951    }
1952
1953    #[test]
1954    fn test_apple_medium_device_class() {
1955        let mut event = Event {
1956            contexts: {
1957                let mut contexts = Contexts::new();
1958                contexts.add(DeviceContext {
1959                    family: "iPhone".to_owned().into(),
1960                    model: "iPhone12,8".to_owned().into(),
1961                    ..Default::default()
1962                });
1963                Annotated::new(contexts)
1964            },
1965            ..Default::default()
1966        };
1967        normalize_device_class(&mut event);
1968        assert_debug_snapshot!(event.tags, @r###"
1969        Tags(
1970            PairList(
1971                [
1972                    TagEntry(
1973                        "device.class",
1974                        "2",
1975                    ),
1976                ],
1977            ),
1978        )
1979        "###);
1980    }
1981
1982    #[test]
1983    fn test_android_low_device_class() {
1984        let mut event = Event {
1985            contexts: {
1986                let mut contexts = Contexts::new();
1987                contexts.add(DeviceContext {
1988                    family: "android".to_owned().into(),
1989                    processor_frequency: 1000.into(),
1990                    processor_count: 6.into(),
1991                    memory_size: (2 * 1024 * 1024 * 1024).into(),
1992                    ..Default::default()
1993                });
1994                Annotated::new(contexts)
1995            },
1996            ..Default::default()
1997        };
1998        normalize_device_class(&mut event);
1999        assert_debug_snapshot!(event.tags, @r###"
2000        Tags(
2001            PairList(
2002                [
2003                    TagEntry(
2004                        "device.class",
2005                        "1",
2006                    ),
2007                ],
2008            ),
2009        )
2010        "###);
2011    }
2012
2013    #[test]
2014    fn test_android_medium_device_class() {
2015        let mut event = Event {
2016            contexts: {
2017                let mut contexts = Contexts::new();
2018                contexts.add(DeviceContext {
2019                    family: "android".to_owned().into(),
2020                    processor_frequency: 2000.into(),
2021                    processor_count: 8.into(),
2022                    memory_size: (6 * 1024 * 1024 * 1024).into(),
2023                    ..Default::default()
2024                });
2025                Annotated::new(contexts)
2026            },
2027            ..Default::default()
2028        };
2029        normalize_device_class(&mut event);
2030        assert_debug_snapshot!(event.tags, @r###"
2031        Tags(
2032            PairList(
2033                [
2034                    TagEntry(
2035                        "device.class",
2036                        "2",
2037                    ),
2038                ],
2039            ),
2040        )
2041        "###);
2042    }
2043
2044    #[test]
2045    fn test_android_high_device_class() {
2046        let mut event = Event {
2047            contexts: {
2048                let mut contexts = Contexts::new();
2049                contexts.add(DeviceContext {
2050                    family: "android".to_owned().into(),
2051                    processor_frequency: 2500.into(),
2052                    processor_count: 8.into(),
2053                    memory_size: (6 * 1024 * 1024 * 1024).into(),
2054                    ..Default::default()
2055                });
2056                Annotated::new(contexts)
2057            },
2058            ..Default::default()
2059        };
2060        normalize_device_class(&mut event);
2061        assert_debug_snapshot!(event.tags, @r###"
2062        Tags(
2063            PairList(
2064                [
2065                    TagEntry(
2066                        "device.class",
2067                        "3",
2068                    ),
2069                ],
2070            ),
2071        )
2072        "###);
2073    }
2074
2075    #[test]
2076    fn test_keeps_valid_measurement() {
2077        let name = "lcp";
2078        let measurement = Measurement {
2079            value: Annotated::new(420.69.try_into().unwrap()),
2080            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2081        };
2082
2083        assert!(!is_measurement_dropped(name, measurement));
2084    }
2085
2086    #[test]
2087    fn test_drops_too_long_measurement_names() {
2088        let name = "lcpppppppppppppppppppppppppppp";
2089        let measurement = Measurement {
2090            value: Annotated::new(420.69.try_into().unwrap()),
2091            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2092        };
2093
2094        assert!(is_measurement_dropped(name, measurement));
2095    }
2096
2097    #[test]
2098    fn test_drops_measurements_with_invalid_characters() {
2099        let name = "i æm frøm nørwåy";
2100        let measurement = Measurement {
2101            value: Annotated::new(420.69.try_into().unwrap()),
2102            unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2103        };
2104
2105        assert!(is_measurement_dropped(name, measurement));
2106    }
2107
2108    fn is_measurement_dropped(name: &str, measurement: Measurement) -> bool {
2109        let max_name_and_unit_len = Some(30);
2110
2111        let mut measurements: BTreeMap<String, Annotated<Measurement>> = Object::new();
2112        measurements.insert(name.to_owned(), Annotated::new(measurement));
2113
2114        let mut measurements = Measurements(measurements);
2115        let mut meta = Meta::default();
2116        let measurements_config = MeasurementsConfig {
2117            max_custom_measurements: 1,
2118            ..Default::default()
2119        };
2120
2121        let dynamic_config = CombinedMeasurementsConfig::new(Some(&measurements_config), None);
2122
2123        // Just for clarity.
2124        // Checks that there is 1 measurement before processing.
2125        assert_eq!(measurements.len(), 1);
2126
2127        remove_invalid_measurements(
2128            &mut measurements,
2129            &mut meta,
2130            dynamic_config,
2131            max_name_and_unit_len,
2132        );
2133
2134        // Checks whether the measurement is dropped.
2135        measurements.is_empty()
2136    }
2137
2138    #[test]
2139    fn test_custom_measurements_not_dropped() {
2140        let mut measurements = Measurements(BTreeMap::from([(
2141            "custom_measurement".to_owned(),
2142            Annotated::new(Measurement {
2143                value: Annotated::new(42.0.try_into().unwrap()),
2144                unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2145            }),
2146        )]));
2147
2148        let original = measurements.clone();
2149        remove_invalid_measurements(
2150            &mut measurements,
2151            &mut Meta::default(),
2152            CombinedMeasurementsConfig::new(None, None),
2153            Some(30),
2154        );
2155
2156        assert_eq!(original, measurements);
2157    }
2158
2159    #[test]
2160    fn test_normalize_app_start_measurements_does_not_add_measurements() {
2161        let mut measurements = Annotated::<Measurements>::from_json(r###"{}"###)
2162            .unwrap()
2163            .into_value()
2164            .unwrap();
2165        insta::assert_debug_snapshot!(measurements, @r###"
2166        Measurements(
2167            {},
2168        )
2169        "###);
2170        normalize_app_start_measurements(&mut measurements);
2171        insta::assert_debug_snapshot!(measurements, @r###"
2172        Measurements(
2173            {},
2174        )
2175        "###);
2176    }
2177
2178    #[test]
2179    fn test_normalize_app_start_cold_measurements() {
2180        let mut measurements =
2181            Annotated::<Measurements>::from_json(r#"{"app.start.cold": {"value": 1.1}}"#)
2182                .unwrap()
2183                .into_value()
2184                .unwrap();
2185        insta::assert_debug_snapshot!(measurements, @r###"
2186        Measurements(
2187            {
2188                "app.start.cold": Measurement {
2189                    value: 1.1,
2190                    unit: ~,
2191                },
2192            },
2193        )
2194        "###);
2195        normalize_app_start_measurements(&mut measurements);
2196        insta::assert_debug_snapshot!(measurements, @r###"
2197        Measurements(
2198            {
2199                "app_start_cold": Measurement {
2200                    value: 1.1,
2201                    unit: ~,
2202                },
2203            },
2204        )
2205        "###);
2206    }
2207
2208    #[test]
2209    fn test_normalize_app_start_warm_measurements() {
2210        let mut measurements =
2211            Annotated::<Measurements>::from_json(r#"{"app.start.warm": {"value": 1.1}}"#)
2212                .unwrap()
2213                .into_value()
2214                .unwrap();
2215        insta::assert_debug_snapshot!(measurements, @r###"
2216        Measurements(
2217            {
2218                "app.start.warm": Measurement {
2219                    value: 1.1,
2220                    unit: ~,
2221                },
2222            },
2223        )
2224        "###);
2225        normalize_app_start_measurements(&mut measurements);
2226        insta::assert_debug_snapshot!(measurements, @r###"
2227        Measurements(
2228            {
2229                "app_start_warm": Measurement {
2230                    value: 1.1,
2231                    unit: ~,
2232                },
2233            },
2234        )
2235        "###);
2236    }
2237
2238    #[test]
2239    fn test_ai_legacy_measurements() {
2240        let json = r#"
2241            {
2242                "spans": [
2243                    {
2244                        "timestamp": 1702474613.0495,
2245                        "start_timestamp": 1702474613.0175,
2246                        "description": "OpenAI ",
2247                        "op": "ai.chat_completions.openai",
2248                        "span_id": "9c01bd820a083e63",
2249                        "parent_span_id": "a1e13f3f06239d69",
2250                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2251                        "measurements": {
2252                            "ai_prompt_tokens_used": {
2253                                "value": 1000
2254                            },
2255                            "ai_completion_tokens_used": {
2256                                "value": 2000
2257                            }
2258                        },
2259                        "data": {
2260                            "ai.model_id": "claude-2.1"
2261                        }
2262                    },
2263                    {
2264                        "timestamp": 1702474613.0495,
2265                        "start_timestamp": 1702474613.0175,
2266                        "description": "OpenAI ",
2267                        "op": "ai.chat_completions.openai",
2268                        "span_id": "ac01bd820a083e63",
2269                        "parent_span_id": "a1e13f3f06239d69",
2270                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2271                        "measurements": {
2272                            "ai_prompt_tokens_used": {
2273                                "value": 1000
2274                            },
2275                            "ai_completion_tokens_used": {
2276                                "value": 2000
2277                            }
2278                        },
2279                        "data": {
2280                            "ai.model_id": "gpt4-21-04"
2281                        }
2282                    }
2283                ]
2284            }
2285        "#;
2286
2287        let mut event = Annotated::<Event>::from_json(json).unwrap();
2288
2289        normalize_event(
2290            &mut event,
2291            &NormalizationConfig {
2292                ai_model_costs: Some(&ModelCosts {
2293                    version: 2,
2294                    models: HashMap::from([
2295                        (
2296                            "claude-2.1".to_owned(),
2297                            ModelCostV2 {
2298                                input_per_token: 0.01,
2299                                output_per_token: 0.02,
2300                                output_reasoning_per_token: 0.03,
2301                                input_cached_per_token: 0.0,
2302                            },
2303                        ),
2304                        (
2305                            "gpt4-21-04".to_owned(),
2306                            ModelCostV2 {
2307                                input_per_token: 0.02,
2308                                output_per_token: 0.03,
2309                                output_reasoning_per_token: 0.04,
2310                                input_cached_per_token: 0.0,
2311                            },
2312                        ),
2313                    ]),
2314                }),
2315                ..NormalizationConfig::default()
2316            },
2317        );
2318
2319        let spans = event.value().unwrap().spans.value().unwrap();
2320        assert_eq!(spans.len(), 2);
2321        assert_eq!(
2322            spans
2323                .first()
2324                .and_then(|span| span.value())
2325                .and_then(|span| span.data.value())
2326                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2327            Some(&Value::F64(50.0))
2328        );
2329        assert_eq!(
2330            spans
2331                .get(1)
2332                .and_then(|span| span.value())
2333                .and_then(|span| span.data.value())
2334                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2335            Some(&Value::F64(80.0))
2336        );
2337    }
2338
2339    #[test]
2340    fn test_ai_data() {
2341        let json = r#"
2342            {
2343                "spans": [
2344                    {
2345                        "timestamp": 1702474614.0175,
2346                        "start_timestamp": 1702474613.0175,
2347                        "description": "OpenAI ",
2348                        "op": "gen_ai.chat_completions.openai",
2349                        "span_id": "9c01bd820a083e63",
2350                        "parent_span_id": "a1e13f3f06239d69",
2351                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2352                        "data": {
2353                            "gen_ai.usage.input_tokens": 1000,
2354                            "gen_ai.usage.output_tokens": 2000,
2355                            "gen_ai.usage.output_tokens.reasoning": 1000,
2356                            "gen_ai.usage.input_tokens.cached": 500,
2357                            "gen_ai.request.model": "claude-2.1"
2358                        }
2359                    },
2360                    {
2361                        "timestamp": 1702474614.0175,
2362                        "start_timestamp": 1702474613.0175,
2363                        "description": "OpenAI ",
2364                        "op": "gen_ai.chat_completions.openai",
2365                        "span_id": "ac01bd820a083e63",
2366                        "parent_span_id": "a1e13f3f06239d69",
2367                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2368                        "data": {
2369                            "gen_ai.usage.input_tokens": 1000,
2370                            "gen_ai.usage.output_tokens": 2000,
2371                            "gen_ai.request.model": "gpt4-21-04"
2372                        }
2373                    },
2374                    {
2375                        "timestamp": 1702474614.0175,
2376                        "start_timestamp": 1702474613.0175,
2377                        "description": "OpenAI ",
2378                        "op": "gen_ai.chat_completions.openai",
2379                        "span_id": "ac01bd820a083e63",
2380                        "parent_span_id": "a1e13f3f06239d69",
2381                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2382                        "data": {
2383                            "gen_ai.usage.input_tokens": 1000,
2384                            "gen_ai.usage.output_tokens": 2000,
2385                            "gen_ai.response.model": "gpt4-21-04"
2386                        }
2387                    }
2388                ]
2389            }
2390        "#;
2391
2392        let mut event = Annotated::<Event>::from_json(json).unwrap();
2393
2394        normalize_event(
2395            &mut event,
2396            &NormalizationConfig {
2397                ai_model_costs: Some(&ModelCosts {
2398                    version: 2,
2399                    models: HashMap::from([
2400                        (
2401                            "claude-2.1".to_owned(),
2402                            ModelCostV2 {
2403                                input_per_token: 0.01,
2404                                output_per_token: 0.02,
2405                                output_reasoning_per_token: 0.03,
2406                                input_cached_per_token: 0.04,
2407                            },
2408                        ),
2409                        (
2410                            "gpt4-21-04".to_owned(),
2411                            ModelCostV2 {
2412                                input_per_token: 0.09,
2413                                output_per_token: 0.05,
2414                                output_reasoning_per_token: 0.0,
2415                                input_cached_per_token: 0.0,
2416                            },
2417                        ),
2418                    ]),
2419                }),
2420                ..NormalizationConfig::default()
2421            },
2422        );
2423
2424        let spans = event.value().unwrap().spans.value().unwrap();
2425        assert_eq!(spans.len(), 3);
2426        let first_span_data = spans
2427            .first()
2428            .and_then(|span| span.value())
2429            .and_then(|span| span.data.value());
2430        assert_eq!(
2431            first_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2432            Some(&Value::F64(75.0))
2433        );
2434        assert_eq!(
2435            first_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2436            Some(&Value::F64(2000.0))
2437        );
2438
2439        let second_span_data = spans
2440            .get(1)
2441            .and_then(|span| span.value())
2442            .and_then(|span| span.data.value());
2443        assert_eq!(
2444            second_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2445            Some(&Value::F64(190.0))
2446        );
2447        assert_eq!(
2448            second_span_data.and_then(|data| data.gen_ai_usage_total_tokens.value()),
2449            Some(&Value::F64(3000.0))
2450        );
2451        assert_eq!(
2452            second_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2453            Some(&Value::F64(2000.0))
2454        );
2455
2456        // Cost calculation when there is only gen_ai.response.model present
2457        let third_span_data = spans
2458            .get(2)
2459            .and_then(|span| span.value())
2460            .and_then(|span| span.data.value());
2461        assert_eq!(
2462            third_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2463            Some(&Value::F64(190.0))
2464        );
2465    }
2466
2467    #[test]
2468    fn test_ai_data_with_no_tokens() {
2469        let json = r#"
2470            {
2471                "spans": [
2472                    {
2473                        "timestamp": 1702474613.0495,
2474                        "start_timestamp": 1702474613.0175,
2475                        "description": "OpenAI ",
2476                        "op": "gen_ai.invoke_agent",
2477                        "span_id": "9c01bd820a083e63",
2478                        "parent_span_id": "a1e13f3f06239d69",
2479                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2480                        "data": {
2481                            "gen_ai.request.model": "claude-2.1"
2482                        }
2483                    }
2484                ]
2485            }
2486        "#;
2487
2488        let mut event = Annotated::<Event>::from_json(json).unwrap();
2489
2490        normalize_event(
2491            &mut event,
2492            &NormalizationConfig {
2493                ai_model_costs: Some(&ModelCosts {
2494                    version: 2,
2495                    models: HashMap::from([(
2496                        "claude-2.1".to_owned(),
2497                        ModelCostV2 {
2498                            input_per_token: 0.01,
2499                            output_per_token: 0.02,
2500                            output_reasoning_per_token: 0.03,
2501                            input_cached_per_token: 0.0,
2502                        },
2503                    )]),
2504                }),
2505                ..NormalizationConfig::default()
2506            },
2507        );
2508
2509        let spans = event.value().unwrap().spans.value().unwrap();
2510
2511        assert_eq!(spans.len(), 1);
2512        // total_cost shouldn't be set if no tokens are present on span data
2513        assert_eq!(
2514            spans
2515                .first()
2516                .and_then(|span| span.value())
2517                .and_then(|span| span.data.value())
2518                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2519            None
2520        );
2521        // total_tokens shouldn't be set if no tokens are present on span data
2522        assert_eq!(
2523            spans
2524                .first()
2525                .and_then(|span| span.value())
2526                .and_then(|span| span.data.value())
2527                .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2528            None
2529        );
2530    }
2531
2532    #[test]
2533    fn test_ai_data_with_ai_op_prefix() {
2534        let json = r#"
2535            {
2536                "spans": [
2537                    {
2538                        "timestamp": 1702474613.0495,
2539                        "start_timestamp": 1702474613.0175,
2540                        "description": "OpenAI ",
2541                        "op": "ai.chat_completions.openai",
2542                        "span_id": "9c01bd820a083e63",
2543                        "parent_span_id": "a1e13f3f06239d69",
2544                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2545                        "data": {
2546                            "gen_ai.usage.input_tokens": 1000,
2547                            "gen_ai.usage.output_tokens": 2000,
2548                            "gen_ai.usage.output_tokens.reasoning": 1000,
2549                            "gen_ai.usage.input_tokens.cached": 500,
2550                            "gen_ai.request.model": "claude-2.1"
2551                        }
2552                    },
2553                    {
2554                        "timestamp": 1702474613.0495,
2555                        "start_timestamp": 1702474613.0175,
2556                        "description": "OpenAI ",
2557                        "op": "ai.chat_completions.openai",
2558                        "span_id": "ac01bd820a083e63",
2559                        "parent_span_id": "a1e13f3f06239d69",
2560                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2561                        "data": {
2562                            "gen_ai.usage.input_tokens": 1000,
2563                            "gen_ai.usage.output_tokens": 2000,
2564                            "gen_ai.request.model": "gpt4-21-04"
2565                        }
2566                    }
2567                ]
2568            }
2569        "#;
2570
2571        let mut event = Annotated::<Event>::from_json(json).unwrap();
2572
2573        normalize_event(
2574            &mut event,
2575            &NormalizationConfig {
2576                ai_model_costs: Some(&ModelCosts {
2577                    version: 2,
2578                    models: HashMap::from([
2579                        (
2580                            "claude-2.1".to_owned(),
2581                            ModelCostV2 {
2582                                input_per_token: 0.01,
2583                                output_per_token: 0.02,
2584                                output_reasoning_per_token: 0.0,
2585                                input_cached_per_token: 0.04,
2586                            },
2587                        ),
2588                        (
2589                            "gpt4-21-04".to_owned(),
2590                            ModelCostV2 {
2591                                input_per_token: 0.09,
2592                                output_per_token: 0.05,
2593                                output_reasoning_per_token: 0.06,
2594                                input_cached_per_token: 0.0,
2595                            },
2596                        ),
2597                    ]),
2598                }),
2599                ..NormalizationConfig::default()
2600            },
2601        );
2602
2603        let spans = event.value().unwrap().spans.value().unwrap();
2604        assert_eq!(spans.len(), 2);
2605        assert_eq!(
2606            spans
2607                .first()
2608                .and_then(|span| span.value())
2609                .and_then(|span| span.data.value())
2610                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2611            Some(&Value::F64(65.0))
2612        );
2613        assert_eq!(
2614            spans
2615                .get(1)
2616                .and_then(|span| span.value())
2617                .and_then(|span| span.data.value())
2618                .and_then(|data| data.gen_ai_usage_total_cost.value()),
2619            Some(&Value::F64(190.0))
2620        );
2621        assert_eq!(
2622            spans
2623                .get(1)
2624                .and_then(|span| span.value())
2625                .and_then(|span| span.data.value())
2626                .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2627            Some(&Value::F64(3000.0))
2628        );
2629    }
2630
2631    #[test]
2632    fn test_ai_response_tokens_per_second_no_output_tokens() {
2633        let json = r#"
2634            {
2635                "spans": [
2636                    {
2637                        "timestamp": 1702474614.0175,
2638                        "start_timestamp": 1702474613.0175,
2639                        "op": "gen_ai.chat_completions",
2640                        "span_id": "9c01bd820a083e63",
2641                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2642                        "data": {
2643                            "gen_ai.usage.input_tokens": 500
2644                        }
2645                    }
2646                ]
2647            }
2648        "#;
2649
2650        let mut event = Annotated::<Event>::from_json(json).unwrap();
2651
2652        normalize_event(
2653            &mut event,
2654            &NormalizationConfig {
2655                ai_model_costs: Some(&ModelCosts {
2656                    version: 2,
2657                    models: HashMap::new(),
2658                }),
2659                ..NormalizationConfig::default()
2660            },
2661        );
2662
2663        let span_data = get_value!(event.spans[0].data!);
2664
2665        // Should not set response_tokens_per_second when there are no output tokens
2666        assert!(
2667            span_data
2668                .gen_ai_response_tokens_per_second
2669                .value()
2670                .is_none()
2671        );
2672    }
2673
2674    #[test]
2675    fn test_ai_response_tokens_per_second_zero_duration() {
2676        let json = r#"
2677            {
2678                "spans": [
2679                    {
2680                        "timestamp": 1702474613.0175,
2681                        "start_timestamp": 1702474613.0175,
2682                        "op": "gen_ai.chat_completions",
2683                        "span_id": "9c01bd820a083e63",
2684                        "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2685                        "data": {
2686                            "gen_ai.usage.output_tokens": 1000
2687                        }
2688                    }
2689                ]
2690            }
2691        "#;
2692
2693        let mut event = Annotated::<Event>::from_json(json).unwrap();
2694
2695        normalize_event(
2696            &mut event,
2697            &NormalizationConfig {
2698                ai_model_costs: Some(&ModelCosts {
2699                    version: 2,
2700                    models: HashMap::new(),
2701                }),
2702                ..NormalizationConfig::default()
2703            },
2704        );
2705
2706        let span_data = get_value!(event.spans[0].data!);
2707
2708        // Should not set response_tokens_per_second when duration is zero
2709        assert!(
2710            span_data
2711                .gen_ai_response_tokens_per_second
2712                .value()
2713                .is_none()
2714        );
2715    }
2716
2717    #[test]
2718    fn test_apple_high_device_class() {
2719        let mut event = Event {
2720            contexts: {
2721                let mut contexts = Contexts::new();
2722                contexts.add(DeviceContext {
2723                    family: "iPhone".to_owned().into(),
2724                    model: "iPhone15,3".to_owned().into(),
2725                    ..Default::default()
2726                });
2727                Annotated::new(contexts)
2728            },
2729            ..Default::default()
2730        };
2731        normalize_device_class(&mut event);
2732        assert_debug_snapshot!(event.tags, @r###"
2733        Tags(
2734            PairList(
2735                [
2736                    TagEntry(
2737                        "device.class",
2738                        "3",
2739                    ),
2740                ],
2741            ),
2742        )
2743        "###);
2744    }
2745
2746    #[test]
2747    fn test_filter_mobile_outliers() {
2748        let mut measurements =
2749            Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2750                .unwrap()
2751                .into_value()
2752                .unwrap();
2753        assert_eq!(measurements.len(), 1);
2754        filter_mobile_outliers(&mut measurements);
2755        assert_eq!(measurements.len(), 0);
2756    }
2757
2758    #[test]
2759    fn test_computed_performance_score() {
2760        let json = r#"
2761        {
2762            "type": "transaction",
2763            "timestamp": "2021-04-26T08:00:05+0100",
2764            "start_timestamp": "2021-04-26T08:00:00+0100",
2765            "measurements": {
2766                "fid": {"value": 213, "unit": "millisecond"},
2767                "fcp": {"value": 1237, "unit": "millisecond"},
2768                "lcp": {"value": 6596, "unit": "millisecond"},
2769                "cls": {"value": 0.11}
2770            },
2771            "contexts": {
2772                "browser": {
2773                    "name": "Chrome",
2774                    "version": "120.1.1",
2775                    "type": "browser"
2776                }
2777            }
2778        }
2779        "#;
2780
2781        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2782
2783        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2784            "profiles": [
2785                {
2786                    "name": "Desktop",
2787                    "scoreComponents": [
2788                        {
2789                            "measurement": "fcp",
2790                            "weight": 0.15,
2791                            "p10": 900,
2792                            "p50": 1600
2793                        },
2794                        {
2795                            "measurement": "lcp",
2796                            "weight": 0.30,
2797                            "p10": 1200,
2798                            "p50": 2400
2799                        },
2800                        {
2801                            "measurement": "fid",
2802                            "weight": 0.30,
2803                            "p10": 100,
2804                            "p50": 300
2805                        },
2806                        {
2807                            "measurement": "cls",
2808                            "weight": 0.25,
2809                            "p10": 0.1,
2810                            "p50": 0.25
2811                        },
2812                        {
2813                            "measurement": "ttfb",
2814                            "weight": 0.0,
2815                            "p10": 0.2,
2816                            "p50": 0.4
2817                        },
2818                    ],
2819                    "condition": {
2820                        "op":"eq",
2821                        "name": "event.contexts.browser.name",
2822                        "value": "Chrome"
2823                    }
2824                }
2825            ]
2826        }))
2827        .unwrap();
2828
2829        normalize_performance_score(&mut event, Some(&performance_score));
2830
2831        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2832        {
2833          "type": "transaction",
2834          "timestamp": 1619420405.0,
2835          "start_timestamp": 1619420400.0,
2836          "contexts": {
2837            "browser": {
2838              "name": "Chrome",
2839              "version": "120.1.1",
2840              "type": "browser",
2841            },
2842          },
2843          "measurements": {
2844            "cls": {
2845              "value": 0.11,
2846            },
2847            "fcp": {
2848              "value": 1237.0,
2849              "unit": "millisecond",
2850            },
2851            "fid": {
2852              "value": 213.0,
2853              "unit": "millisecond",
2854            },
2855            "lcp": {
2856              "value": 6596.0,
2857              "unit": "millisecond",
2858            },
2859            "score.cls": {
2860              "value": 0.21864170607444863,
2861              "unit": "ratio",
2862            },
2863            "score.fcp": {
2864              "value": 0.10750855443790831,
2865              "unit": "ratio",
2866            },
2867            "score.fid": {
2868              "value": 0.19657361348282545,
2869              "unit": "ratio",
2870            },
2871            "score.lcp": {
2872              "value": 0.009238896571386584,
2873              "unit": "ratio",
2874            },
2875            "score.ratio.cls": {
2876              "value": 0.8745668242977945,
2877              "unit": "ratio",
2878            },
2879            "score.ratio.fcp": {
2880              "value": 0.7167236962527221,
2881              "unit": "ratio",
2882            },
2883            "score.ratio.fid": {
2884              "value": 0.6552453782760849,
2885              "unit": "ratio",
2886            },
2887            "score.ratio.lcp": {
2888              "value": 0.03079632190462195,
2889              "unit": "ratio",
2890            },
2891            "score.total": {
2892              "value": 0.531962770566569,
2893              "unit": "ratio",
2894            },
2895            "score.weight.cls": {
2896              "value": 0.25,
2897              "unit": "ratio",
2898            },
2899            "score.weight.fcp": {
2900              "value": 0.15,
2901              "unit": "ratio",
2902            },
2903            "score.weight.fid": {
2904              "value": 0.3,
2905              "unit": "ratio",
2906            },
2907            "score.weight.lcp": {
2908              "value": 0.3,
2909              "unit": "ratio",
2910            },
2911            "score.weight.ttfb": {
2912              "value": 0.0,
2913              "unit": "ratio",
2914            },
2915          },
2916        }
2917        "###);
2918    }
2919
2920    // Test performance score is calculated correctly when the sum of weights is under 1.
2921    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
2922    #[test]
2923    fn test_computed_performance_score_with_under_normalized_weights() {
2924        let json = r#"
2925        {
2926            "type": "transaction",
2927            "timestamp": "2021-04-26T08:00:05+0100",
2928            "start_timestamp": "2021-04-26T08:00:00+0100",
2929            "measurements": {
2930                "fid": {"value": 213, "unit": "millisecond"},
2931                "fcp": {"value": 1237, "unit": "millisecond"},
2932                "lcp": {"value": 6596, "unit": "millisecond"},
2933                "cls": {"value": 0.11}
2934            },
2935            "contexts": {
2936                "browser": {
2937                    "name": "Chrome",
2938                    "version": "120.1.1",
2939                    "type": "browser"
2940                }
2941            }
2942        }
2943        "#;
2944
2945        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2946
2947        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2948            "profiles": [
2949                {
2950                    "name": "Desktop",
2951                    "scoreComponents": [
2952                        {
2953                            "measurement": "fcp",
2954                            "weight": 0.03,
2955                            "p10": 900,
2956                            "p50": 1600
2957                        },
2958                        {
2959                            "measurement": "lcp",
2960                            "weight": 0.06,
2961                            "p10": 1200,
2962                            "p50": 2400
2963                        },
2964                        {
2965                            "measurement": "fid",
2966                            "weight": 0.06,
2967                            "p10": 100,
2968                            "p50": 300
2969                        },
2970                        {
2971                            "measurement": "cls",
2972                            "weight": 0.05,
2973                            "p10": 0.1,
2974                            "p50": 0.25
2975                        },
2976                        {
2977                            "measurement": "ttfb",
2978                            "weight": 0.0,
2979                            "p10": 0.2,
2980                            "p50": 0.4
2981                        },
2982                    ],
2983                    "condition": {
2984                        "op":"eq",
2985                        "name": "event.contexts.browser.name",
2986                        "value": "Chrome"
2987                    }
2988                }
2989            ]
2990        }))
2991        .unwrap();
2992
2993        normalize_performance_score(&mut event, Some(&performance_score));
2994
2995        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2996        {
2997          "type": "transaction",
2998          "timestamp": 1619420405.0,
2999          "start_timestamp": 1619420400.0,
3000          "contexts": {
3001            "browser": {
3002              "name": "Chrome",
3003              "version": "120.1.1",
3004              "type": "browser",
3005            },
3006          },
3007          "measurements": {
3008            "cls": {
3009              "value": 0.11,
3010            },
3011            "fcp": {
3012              "value": 1237.0,
3013              "unit": "millisecond",
3014            },
3015            "fid": {
3016              "value": 213.0,
3017              "unit": "millisecond",
3018            },
3019            "lcp": {
3020              "value": 6596.0,
3021              "unit": "millisecond",
3022            },
3023            "score.cls": {
3024              "value": 0.21864170607444863,
3025              "unit": "ratio",
3026            },
3027            "score.fcp": {
3028              "value": 0.10750855443790831,
3029              "unit": "ratio",
3030            },
3031            "score.fid": {
3032              "value": 0.19657361348282545,
3033              "unit": "ratio",
3034            },
3035            "score.lcp": {
3036              "value": 0.009238896571386584,
3037              "unit": "ratio",
3038            },
3039            "score.ratio.cls": {
3040              "value": 0.8745668242977945,
3041              "unit": "ratio",
3042            },
3043            "score.ratio.fcp": {
3044              "value": 0.7167236962527221,
3045              "unit": "ratio",
3046            },
3047            "score.ratio.fid": {
3048              "value": 0.6552453782760849,
3049              "unit": "ratio",
3050            },
3051            "score.ratio.lcp": {
3052              "value": 0.03079632190462195,
3053              "unit": "ratio",
3054            },
3055            "score.total": {
3056              "value": 0.531962770566569,
3057              "unit": "ratio",
3058            },
3059            "score.weight.cls": {
3060              "value": 0.25,
3061              "unit": "ratio",
3062            },
3063            "score.weight.fcp": {
3064              "value": 0.15,
3065              "unit": "ratio",
3066            },
3067            "score.weight.fid": {
3068              "value": 0.3,
3069              "unit": "ratio",
3070            },
3071            "score.weight.lcp": {
3072              "value": 0.3,
3073              "unit": "ratio",
3074            },
3075            "score.weight.ttfb": {
3076              "value": 0.0,
3077              "unit": "ratio",
3078            },
3079          },
3080        }
3081        "###);
3082    }
3083
3084    // Test performance score is calculated correctly when the sum of weights is over 1.
3085    // The expected result should normalize the weights to a sum of 1 and scale the weight measurements accordingly.
3086    #[test]
3087    fn test_computed_performance_score_with_over_normalized_weights() {
3088        let json = r#"
3089        {
3090            "type": "transaction",
3091            "timestamp": "2021-04-26T08:00:05+0100",
3092            "start_timestamp": "2021-04-26T08:00:00+0100",
3093            "measurements": {
3094                "fid": {"value": 213, "unit": "millisecond"},
3095                "fcp": {"value": 1237, "unit": "millisecond"},
3096                "lcp": {"value": 6596, "unit": "millisecond"},
3097                "cls": {"value": 0.11}
3098            },
3099            "contexts": {
3100                "browser": {
3101                    "name": "Chrome",
3102                    "version": "120.1.1",
3103                    "type": "browser"
3104                }
3105            }
3106        }
3107        "#;
3108
3109        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3110
3111        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3112            "profiles": [
3113                {
3114                    "name": "Desktop",
3115                    "scoreComponents": [
3116                        {
3117                            "measurement": "fcp",
3118                            "weight": 0.30,
3119                            "p10": 900,
3120                            "p50": 1600
3121                        },
3122                        {
3123                            "measurement": "lcp",
3124                            "weight": 0.60,
3125                            "p10": 1200,
3126                            "p50": 2400
3127                        },
3128                        {
3129                            "measurement": "fid",
3130                            "weight": 0.60,
3131                            "p10": 100,
3132                            "p50": 300
3133                        },
3134                        {
3135                            "measurement": "cls",
3136                            "weight": 0.50,
3137                            "p10": 0.1,
3138                            "p50": 0.25
3139                        },
3140                        {
3141                            "measurement": "ttfb",
3142                            "weight": 0.0,
3143                            "p10": 0.2,
3144                            "p50": 0.4
3145                        },
3146                    ],
3147                    "condition": {
3148                        "op":"eq",
3149                        "name": "event.contexts.browser.name",
3150                        "value": "Chrome"
3151                    }
3152                }
3153            ]
3154        }))
3155        .unwrap();
3156
3157        normalize_performance_score(&mut event, Some(&performance_score));
3158
3159        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3160        {
3161          "type": "transaction",
3162          "timestamp": 1619420405.0,
3163          "start_timestamp": 1619420400.0,
3164          "contexts": {
3165            "browser": {
3166              "name": "Chrome",
3167              "version": "120.1.1",
3168              "type": "browser",
3169            },
3170          },
3171          "measurements": {
3172            "cls": {
3173              "value": 0.11,
3174            },
3175            "fcp": {
3176              "value": 1237.0,
3177              "unit": "millisecond",
3178            },
3179            "fid": {
3180              "value": 213.0,
3181              "unit": "millisecond",
3182            },
3183            "lcp": {
3184              "value": 6596.0,
3185              "unit": "millisecond",
3186            },
3187            "score.cls": {
3188              "value": 0.21864170607444863,
3189              "unit": "ratio",
3190            },
3191            "score.fcp": {
3192              "value": 0.10750855443790831,
3193              "unit": "ratio",
3194            },
3195            "score.fid": {
3196              "value": 0.19657361348282545,
3197              "unit": "ratio",
3198            },
3199            "score.lcp": {
3200              "value": 0.009238896571386584,
3201              "unit": "ratio",
3202            },
3203            "score.ratio.cls": {
3204              "value": 0.8745668242977945,
3205              "unit": "ratio",
3206            },
3207            "score.ratio.fcp": {
3208              "value": 0.7167236962527221,
3209              "unit": "ratio",
3210            },
3211            "score.ratio.fid": {
3212              "value": 0.6552453782760849,
3213              "unit": "ratio",
3214            },
3215            "score.ratio.lcp": {
3216              "value": 0.03079632190462195,
3217              "unit": "ratio",
3218            },
3219            "score.total": {
3220              "value": 0.531962770566569,
3221              "unit": "ratio",
3222            },
3223            "score.weight.cls": {
3224              "value": 0.25,
3225              "unit": "ratio",
3226            },
3227            "score.weight.fcp": {
3228              "value": 0.15,
3229              "unit": "ratio",
3230            },
3231            "score.weight.fid": {
3232              "value": 0.3,
3233              "unit": "ratio",
3234            },
3235            "score.weight.lcp": {
3236              "value": 0.3,
3237              "unit": "ratio",
3238            },
3239            "score.weight.ttfb": {
3240              "value": 0.0,
3241              "unit": "ratio",
3242            },
3243          },
3244        }
3245        "###);
3246    }
3247
3248    #[test]
3249    fn test_computed_performance_score_missing_measurement() {
3250        let json = r#"
3251        {
3252            "type": "transaction",
3253            "timestamp": "2021-04-26T08:00:05+0100",
3254            "start_timestamp": "2021-04-26T08:00:00+0100",
3255            "measurements": {
3256                "a": {"value": 213, "unit": "millisecond"}
3257            },
3258            "contexts": {
3259                "browser": {
3260                    "name": "Chrome",
3261                    "version": "120.1.1",
3262                    "type": "browser"
3263                }
3264            }
3265        }
3266        "#;
3267
3268        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3269
3270        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3271            "profiles": [
3272                {
3273                    "name": "Desktop",
3274                    "scoreComponents": [
3275                        {
3276                            "measurement": "a",
3277                            "weight": 0.15,
3278                            "p10": 900,
3279                            "p50": 1600
3280                        },
3281                        {
3282                            "measurement": "b",
3283                            "weight": 0.30,
3284                            "p10": 1200,
3285                            "p50": 2400
3286                        },
3287                    ],
3288                    "condition": {
3289                        "op":"eq",
3290                        "name": "event.contexts.browser.name",
3291                        "value": "Chrome"
3292                    }
3293                }
3294            ]
3295        }))
3296        .unwrap();
3297
3298        normalize_performance_score(&mut event, Some(&performance_score));
3299
3300        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3301        {
3302          "type": "transaction",
3303          "timestamp": 1619420405.0,
3304          "start_timestamp": 1619420400.0,
3305          "contexts": {
3306            "browser": {
3307              "name": "Chrome",
3308              "version": "120.1.1",
3309              "type": "browser",
3310            },
3311          },
3312          "measurements": {
3313            "a": {
3314              "value": 213.0,
3315              "unit": "millisecond",
3316            },
3317          },
3318        }
3319        "###);
3320    }
3321
3322    #[test]
3323    fn test_computed_performance_score_optional_measurement() {
3324        let json = r#"
3325        {
3326            "type": "transaction",
3327            "timestamp": "2021-04-26T08:00:05+0100",
3328            "start_timestamp": "2021-04-26T08:00:00+0100",
3329            "measurements": {
3330                "a": {"value": 213, "unit": "millisecond"},
3331                "b": {"value": 213, "unit": "millisecond"}
3332            },
3333            "contexts": {
3334                "browser": {
3335                    "name": "Chrome",
3336                    "version": "120.1.1",
3337                    "type": "browser"
3338                }
3339            }
3340        }
3341        "#;
3342
3343        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3344
3345        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3346            "profiles": [
3347                {
3348                    "name": "Desktop",
3349                    "scoreComponents": [
3350                        {
3351                            "measurement": "a",
3352                            "weight": 0.15,
3353                            "p10": 900,
3354                            "p50": 1600,
3355                        },
3356                        {
3357                            "measurement": "b",
3358                            "weight": 0.30,
3359                            "p10": 1200,
3360                            "p50": 2400,
3361                            "optional": true
3362                        },
3363                        {
3364                            "measurement": "c",
3365                            "weight": 0.55,
3366                            "p10": 1200,
3367                            "p50": 2400,
3368                            "optional": true
3369                        },
3370                    ],
3371                    "condition": {
3372                        "op":"eq",
3373                        "name": "event.contexts.browser.name",
3374                        "value": "Chrome"
3375                    }
3376                }
3377            ]
3378        }))
3379        .unwrap();
3380
3381        normalize_performance_score(&mut event, Some(&performance_score));
3382
3383        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3384        {
3385          "type": "transaction",
3386          "timestamp": 1619420405.0,
3387          "start_timestamp": 1619420400.0,
3388          "contexts": {
3389            "browser": {
3390              "name": "Chrome",
3391              "version": "120.1.1",
3392              "type": "browser",
3393            },
3394          },
3395          "measurements": {
3396            "a": {
3397              "value": 213.0,
3398              "unit": "millisecond",
3399            },
3400            "b": {
3401              "value": 213.0,
3402              "unit": "millisecond",
3403            },
3404            "score.a": {
3405              "value": 0.33333215313291975,
3406              "unit": "ratio",
3407            },
3408            "score.b": {
3409              "value": 0.66666415149198,
3410              "unit": "ratio",
3411            },
3412            "score.ratio.a": {
3413              "value": 0.9999964593987591,
3414              "unit": "ratio",
3415            },
3416            "score.ratio.b": {
3417              "value": 0.9999962272379699,
3418              "unit": "ratio",
3419            },
3420            "score.total": {
3421              "value": 0.9999963046248997,
3422              "unit": "ratio",
3423            },
3424            "score.weight.a": {
3425              "value": 0.33333333333333337,
3426              "unit": "ratio",
3427            },
3428            "score.weight.b": {
3429              "value": 0.6666666666666667,
3430              "unit": "ratio",
3431            },
3432            "score.weight.c": {
3433              "value": 0.0,
3434              "unit": "ratio",
3435            },
3436          },
3437        }
3438        "###);
3439    }
3440
3441    #[test]
3442    fn test_computed_performance_score_weight_0() {
3443        let json = r#"
3444        {
3445            "type": "transaction",
3446            "timestamp": "2021-04-26T08:00:05+0100",
3447            "start_timestamp": "2021-04-26T08:00:00+0100",
3448            "measurements": {
3449                "cls": {"value": 0.11}
3450            }
3451        }
3452        "#;
3453
3454        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3455
3456        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3457            "profiles": [
3458                {
3459                    "name": "Desktop",
3460                    "scoreComponents": [
3461                        {
3462                            "measurement": "cls",
3463                            "weight": 0,
3464                            "p10": 0.1,
3465                            "p50": 0.25
3466                        },
3467                    ],
3468                    "condition": {
3469                        "op":"and",
3470                        "inner": []
3471                    }
3472                }
3473            ]
3474        }))
3475        .unwrap();
3476
3477        normalize_performance_score(&mut event, Some(&performance_score));
3478
3479        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3480        {
3481          "type": "transaction",
3482          "timestamp": 1619420405.0,
3483          "start_timestamp": 1619420400.0,
3484          "measurements": {
3485            "cls": {
3486              "value": 0.11,
3487            },
3488          },
3489        }
3490        "###);
3491    }
3492
3493    #[test]
3494    fn test_computed_performance_score_negative_value() {
3495        let json = r#"
3496        {
3497            "type": "transaction",
3498            "timestamp": "2021-04-26T08:00:05+0100",
3499            "start_timestamp": "2021-04-26T08:00:00+0100",
3500            "measurements": {
3501                "ttfb": {"value": -100, "unit": "millisecond"}
3502            }
3503        }
3504        "#;
3505
3506        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3507
3508        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3509            "profiles": [
3510                {
3511                    "name": "Desktop",
3512                    "scoreComponents": [
3513                        {
3514                            "measurement": "ttfb",
3515                            "weight": 1.0,
3516                            "p10": 100.0,
3517                            "p50": 250.0
3518                        },
3519                    ],
3520                    "condition": {
3521                        "op":"and",
3522                        "inner": []
3523                    }
3524                }
3525            ]
3526        }))
3527        .unwrap();
3528
3529        normalize_performance_score(&mut event, Some(&performance_score));
3530
3531        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3532        {
3533          "type": "transaction",
3534          "timestamp": 1619420405.0,
3535          "start_timestamp": 1619420400.0,
3536          "measurements": {
3537            "score.ratio.ttfb": {
3538              "value": 1.0,
3539              "unit": "ratio",
3540            },
3541            "score.total": {
3542              "value": 1.0,
3543              "unit": "ratio",
3544            },
3545            "score.ttfb": {
3546              "value": 1.0,
3547              "unit": "ratio",
3548            },
3549            "score.weight.ttfb": {
3550              "value": 1.0,
3551              "unit": "ratio",
3552            },
3553            "ttfb": {
3554              "value": -100.0,
3555              "unit": "millisecond",
3556            },
3557          },
3558        }
3559        "###);
3560    }
3561
3562    #[test]
3563    fn test_filter_negative_web_vital_measurements() {
3564        let json = r#"
3565        {
3566            "type": "transaction",
3567            "timestamp": "2021-04-26T08:00:05+0100",
3568            "start_timestamp": "2021-04-26T08:00:00+0100",
3569            "measurements": {
3570                "ttfb": {"value": -100, "unit": "millisecond"}
3571            }
3572        }
3573        "#;
3574        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3575
3576        // Allow ttfb as a builtinMeasurement with allow_negative defaulted to false.
3577        let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3578            "builtinMeasurements": [
3579                {"name": "ttfb", "unit": "millisecond"},
3580            ],
3581        }))
3582        .unwrap();
3583
3584        let dynamic_measurement_config =
3585            CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3586
3587        normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3588
3589        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3590        {
3591          "type": "transaction",
3592          "timestamp": 1619420405.0,
3593          "start_timestamp": 1619420400.0,
3594          "measurements": {},
3595          "_meta": {
3596            "measurements": {
3597              "": Meta(Some(MetaInner(
3598                err: [
3599                  [
3600                    "invalid_data",
3601                    {
3602                      "reason": "Negative value for measurement ttfb not allowed: -100",
3603                    },
3604                  ],
3605                ],
3606                val: Some({
3607                  "ttfb": {
3608                    "unit": "millisecond",
3609                    "value": -100.0,
3610                  },
3611                }),
3612              ))),
3613            },
3614          },
3615        }
3616        "###);
3617    }
3618
3619    #[test]
3620    fn test_computed_performance_score_multiple_profiles() {
3621        let json = r#"
3622        {
3623            "type": "transaction",
3624            "timestamp": "2021-04-26T08:00:05+0100",
3625            "start_timestamp": "2021-04-26T08:00:00+0100",
3626            "measurements": {
3627                "cls": {"value": 0.11},
3628                "inp": {"value": 120.0}
3629            }
3630        }
3631        "#;
3632
3633        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3634
3635        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3636            "profiles": [
3637                {
3638                    "name": "Desktop",
3639                    "scoreComponents": [
3640                        {
3641                            "measurement": "cls",
3642                            "weight": 0,
3643                            "p10": 0.1,
3644                            "p50": 0.25
3645                        },
3646                    ],
3647                    "condition": {
3648                        "op":"and",
3649                        "inner": []
3650                    }
3651                },
3652                {
3653                    "name": "Desktop",
3654                    "scoreComponents": [
3655                        {
3656                            "measurement": "inp",
3657                            "weight": 1.0,
3658                            "p10": 0.1,
3659                            "p50": 0.25
3660                        },
3661                    ],
3662                    "condition": {
3663                        "op":"and",
3664                        "inner": []
3665                    }
3666                }
3667            ]
3668        }))
3669        .unwrap();
3670
3671        normalize_performance_score(&mut event, Some(&performance_score));
3672
3673        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3674        {
3675          "type": "transaction",
3676          "timestamp": 1619420405.0,
3677          "start_timestamp": 1619420400.0,
3678          "measurements": {
3679            "cls": {
3680              "value": 0.11,
3681            },
3682            "inp": {
3683              "value": 120.0,
3684            },
3685            "score.inp": {
3686              "value": 0.0,
3687              "unit": "ratio",
3688            },
3689            "score.ratio.inp": {
3690              "value": 0.0,
3691              "unit": "ratio",
3692            },
3693            "score.total": {
3694              "value": 0.0,
3695              "unit": "ratio",
3696            },
3697            "score.weight.inp": {
3698              "value": 1.0,
3699              "unit": "ratio",
3700            },
3701          },
3702        }
3703        "###);
3704    }
3705
3706    #[test]
3707    fn test_compute_performance_score_for_mobile_ios_profile() {
3708        let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3709            .unwrap()
3710            .0
3711            .unwrap();
3712
3713        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3714            "profiles": [
3715                {
3716                    "name": "Mobile",
3717                    "scoreComponents": [
3718                        {
3719                            "measurement": "time_to_initial_display",
3720                            "weight": 0.25,
3721                            "p10": 1800.0,
3722                            "p50": 3000.0,
3723                            "optional": true
3724                        },
3725                        {
3726                            "measurement": "time_to_full_display",
3727                            "weight": 0.25,
3728                            "p10": 2500.0,
3729                            "p50": 4000.0,
3730                            "optional": true
3731                        },
3732                        {
3733                            "measurement": "app_start_warm",
3734                            "weight": 0.25,
3735                            "p10": 200.0,
3736                            "p50": 500.0,
3737                            "optional": true
3738                        },
3739                        {
3740                            "measurement": "app_start_cold",
3741                            "weight": 0.25,
3742                            "p10": 200.0,
3743                            "p50": 500.0,
3744                            "optional": true
3745                        }
3746                    ],
3747                    "condition": {
3748                        "op": "and",
3749                        "inner": [
3750                            {
3751                                "op": "or",
3752                                "inner": [
3753                                    {
3754                                        "op": "eq",
3755                                        "name": "event.sdk.name",
3756                                        "value": "sentry.cocoa"
3757                                    },
3758                                    {
3759                                        "op": "eq",
3760                                        "name": "event.sdk.name",
3761                                        "value": "sentry.java.android"
3762                                    }
3763                                ]
3764                            },
3765                            {
3766                                "op": "eq",
3767                                "name": "event.contexts.trace.op",
3768                                "value": "ui.load"
3769                            }
3770                        ]
3771                    }
3772                }
3773            ]
3774        }))
3775        .unwrap();
3776
3777        normalize_performance_score(&mut event, Some(&performance_score));
3778
3779        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3780    }
3781
3782    #[test]
3783    fn test_compute_performance_score_for_mobile_android_profile() {
3784        let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3785            .unwrap()
3786            .0
3787            .unwrap();
3788
3789        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3790            "profiles": [
3791                {
3792                    "name": "Mobile",
3793                    "scoreComponents": [
3794                        {
3795                            "measurement": "time_to_initial_display",
3796                            "weight": 0.25,
3797                            "p10": 1800.0,
3798                            "p50": 3000.0,
3799                            "optional": true
3800                        },
3801                        {
3802                            "measurement": "time_to_full_display",
3803                            "weight": 0.25,
3804                            "p10": 2500.0,
3805                            "p50": 4000.0,
3806                            "optional": true
3807                        },
3808                        {
3809                            "measurement": "app_start_warm",
3810                            "weight": 0.25,
3811                            "p10": 200.0,
3812                            "p50": 500.0,
3813                            "optional": true
3814                        },
3815                        {
3816                            "measurement": "app_start_cold",
3817                            "weight": 0.25,
3818                            "p10": 200.0,
3819                            "p50": 500.0,
3820                            "optional": true
3821                        }
3822                    ],
3823                    "condition": {
3824                        "op": "and",
3825                        "inner": [
3826                            {
3827                                "op": "or",
3828                                "inner": [
3829                                    {
3830                                        "op": "eq",
3831                                        "name": "event.sdk.name",
3832                                        "value": "sentry.cocoa"
3833                                    },
3834                                    {
3835                                        "op": "eq",
3836                                        "name": "event.sdk.name",
3837                                        "value": "sentry.java.android"
3838                                    }
3839                                ]
3840                            },
3841                            {
3842                                "op": "eq",
3843                                "name": "event.contexts.trace.op",
3844                                "value": "ui.load"
3845                            }
3846                        ]
3847                    }
3848                }
3849            ]
3850        }))
3851        .unwrap();
3852
3853        normalize_performance_score(&mut event, Some(&performance_score));
3854
3855        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3856    }
3857
3858    #[test]
3859    fn test_computes_performance_score_and_tags_with_profile_version() {
3860        let json = r#"
3861        {
3862            "type": "transaction",
3863            "timestamp": "2021-04-26T08:00:05+0100",
3864            "start_timestamp": "2021-04-26T08:00:00+0100",
3865            "measurements": {
3866                "inp": {"value": 120.0}
3867            }
3868        }
3869        "#;
3870
3871        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3872
3873        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3874            "profiles": [
3875                {
3876                    "name": "Desktop",
3877                    "scoreComponents": [
3878                        {
3879                            "measurement": "inp",
3880                            "weight": 1.0,
3881                            "p10": 0.1,
3882                            "p50": 0.25
3883                        },
3884                    ],
3885                    "condition": {
3886                        "op":"and",
3887                        "inner": []
3888                    },
3889                    "version": "beta"
3890                }
3891            ]
3892        }))
3893        .unwrap();
3894
3895        normalize(
3896            &mut event,
3897            &mut Meta::default(),
3898            &NormalizationConfig {
3899                performance_score: Some(&performance_score),
3900                ..Default::default()
3901            },
3902        );
3903
3904        insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3905        {
3906          "performance_score": {
3907            "score_profile_version": "beta",
3908            "type": "performancescore",
3909          },
3910        }
3911        "###);
3912        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3913        {
3914          "inp": {
3915            "value": 120.0,
3916            "unit": "millisecond",
3917          },
3918          "score.inp": {
3919            "value": 0.0,
3920            "unit": "ratio",
3921          },
3922          "score.ratio.inp": {
3923            "value": 0.0,
3924            "unit": "ratio",
3925          },
3926          "score.total": {
3927            "value": 0.0,
3928            "unit": "ratio",
3929          },
3930          "score.weight.inp": {
3931            "value": 1.0,
3932            "unit": "ratio",
3933          },
3934        }
3935        "###);
3936    }
3937
3938    #[test]
3939    fn test_computes_standalone_cls_performance_score() {
3940        let json = r#"
3941        {
3942            "type": "transaction",
3943            "timestamp": "2021-04-26T08:00:05+0100",
3944            "start_timestamp": "2021-04-26T08:00:00+0100",
3945            "measurements": {
3946                "cls": {"value": 0.5}
3947            }
3948        }
3949        "#;
3950
3951        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3952
3953        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3954            "profiles": [
3955            {
3956                "name": "Default",
3957                "scoreComponents": [
3958                    {
3959                        "measurement": "fcp",
3960                        "weight": 0.15,
3961                        "p10": 900.0,
3962                        "p50": 1600.0,
3963                        "optional": true,
3964                    },
3965                    {
3966                        "measurement": "lcp",
3967                        "weight": 0.30,
3968                        "p10": 1200.0,
3969                        "p50": 2400.0,
3970                        "optional": true,
3971                    },
3972                    {
3973                        "measurement": "cls",
3974                        "weight": 0.15,
3975                        "p10": 0.1,
3976                        "p50": 0.25,
3977                        "optional": true,
3978                    },
3979                    {
3980                        "measurement": "ttfb",
3981                        "weight": 0.10,
3982                        "p10": 200.0,
3983                        "p50": 400.0,
3984                        "optional": true,
3985                    },
3986                ],
3987                "condition": {
3988                    "op": "and",
3989                    "inner": [],
3990                },
3991            }
3992            ]
3993        }))
3994        .unwrap();
3995
3996        normalize(
3997            &mut event,
3998            &mut Meta::default(),
3999            &NormalizationConfig {
4000                performance_score: Some(&performance_score),
4001                ..Default::default()
4002            },
4003        );
4004
4005        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4006        {
4007          "cls": {
4008            "value": 0.5,
4009            "unit": "none",
4010          },
4011          "score.cls": {
4012            "value": 0.16615877613713903,
4013            "unit": "ratio",
4014          },
4015          "score.ratio.cls": {
4016            "value": 0.16615877613713903,
4017            "unit": "ratio",
4018          },
4019          "score.total": {
4020            "value": 0.16615877613713903,
4021            "unit": "ratio",
4022          },
4023          "score.weight.cls": {
4024            "value": 1.0,
4025            "unit": "ratio",
4026          },
4027          "score.weight.fcp": {
4028            "value": 0.0,
4029            "unit": "ratio",
4030          },
4031          "score.weight.lcp": {
4032            "value": 0.0,
4033            "unit": "ratio",
4034          },
4035          "score.weight.ttfb": {
4036            "value": 0.0,
4037            "unit": "ratio",
4038          },
4039        }
4040        "###);
4041    }
4042
4043    #[test]
4044    fn test_computes_standalone_lcp_performance_score() {
4045        let json = r#"
4046        {
4047            "type": "transaction",
4048            "timestamp": "2021-04-26T08:00:05+0100",
4049            "start_timestamp": "2021-04-26T08:00:00+0100",
4050            "measurements": {
4051                "lcp": {"value": 1200.0}
4052            }
4053        }
4054        "#;
4055
4056        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4057
4058        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4059            "profiles": [
4060            {
4061                "name": "Default",
4062                "scoreComponents": [
4063                    {
4064                        "measurement": "fcp",
4065                        "weight": 0.15,
4066                        "p10": 900.0,
4067                        "p50": 1600.0,
4068                        "optional": true,
4069                    },
4070                    {
4071                        "measurement": "lcp",
4072                        "weight": 0.30,
4073                        "p10": 1200.0,
4074                        "p50": 2400.0,
4075                        "optional": true,
4076                    },
4077                    {
4078                        "measurement": "cls",
4079                        "weight": 0.15,
4080                        "p10": 0.1,
4081                        "p50": 0.25,
4082                        "optional": true,
4083                    },
4084                    {
4085                        "measurement": "ttfb",
4086                        "weight": 0.10,
4087                        "p10": 200.0,
4088                        "p50": 400.0,
4089                        "optional": true,
4090                    },
4091                ],
4092                "condition": {
4093                    "op": "and",
4094                    "inner": [],
4095                },
4096            }
4097            ]
4098        }))
4099        .unwrap();
4100
4101        normalize(
4102            &mut event,
4103            &mut Meta::default(),
4104            &NormalizationConfig {
4105                performance_score: Some(&performance_score),
4106                ..Default::default()
4107            },
4108        );
4109
4110        insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4111        {
4112          "lcp": {
4113            "value": 1200.0,
4114            "unit": "millisecond",
4115          },
4116          "score.lcp": {
4117            "value": 0.8999999314038525,
4118            "unit": "ratio",
4119          },
4120          "score.ratio.lcp": {
4121            "value": 0.8999999314038525,
4122            "unit": "ratio",
4123          },
4124          "score.total": {
4125            "value": 0.8999999314038525,
4126            "unit": "ratio",
4127          },
4128          "score.weight.cls": {
4129            "value": 0.0,
4130            "unit": "ratio",
4131          },
4132          "score.weight.fcp": {
4133            "value": 0.0,
4134            "unit": "ratio",
4135          },
4136          "score.weight.lcp": {
4137            "value": 1.0,
4138            "unit": "ratio",
4139          },
4140          "score.weight.ttfb": {
4141            "value": 0.0,
4142            "unit": "ratio",
4143          },
4144        }
4145        "###);
4146    }
4147
4148    #[test]
4149    fn test_computed_performance_score_uses_first_matching_profile() {
4150        let json = r#"
4151        {
4152            "type": "transaction",
4153            "timestamp": "2021-04-26T08:00:05+0100",
4154            "start_timestamp": "2021-04-26T08:00:00+0100",
4155            "measurements": {
4156                "a": {"value": 213, "unit": "millisecond"},
4157                "b": {"value": 213, "unit": "millisecond"}
4158            },
4159            "contexts": {
4160                "browser": {
4161                    "name": "Chrome",
4162                    "version": "120.1.1",
4163                    "type": "browser"
4164                }
4165            }
4166        }
4167        "#;
4168
4169        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4170
4171        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4172            "profiles": [
4173                {
4174                    "name": "Mobile",
4175                    "scoreComponents": [
4176                        {
4177                            "measurement": "a",
4178                            "weight": 0.15,
4179                            "p10": 100,
4180                            "p50": 200,
4181                        },
4182                        {
4183                            "measurement": "b",
4184                            "weight": 0.30,
4185                            "p10": 100,
4186                            "p50": 200,
4187                            "optional": true
4188                        },
4189                        {
4190                            "measurement": "c",
4191                            "weight": 0.55,
4192                            "p10": 100,
4193                            "p50": 200,
4194                            "optional": true
4195                        },
4196                    ],
4197                    "condition": {
4198                        "op":"eq",
4199                        "name": "event.contexts.browser.name",
4200                        "value": "Chrome Mobile"
4201                    }
4202                },
4203                {
4204                    "name": "Desktop",
4205                    "scoreComponents": [
4206                        {
4207                            "measurement": "a",
4208                            "weight": 0.15,
4209                            "p10": 900,
4210                            "p50": 1600,
4211                        },
4212                        {
4213                            "measurement": "b",
4214                            "weight": 0.30,
4215                            "p10": 1200,
4216                            "p50": 2400,
4217                            "optional": true
4218                        },
4219                        {
4220                            "measurement": "c",
4221                            "weight": 0.55,
4222                            "p10": 1200,
4223                            "p50": 2400,
4224                            "optional": true
4225                        },
4226                    ],
4227                    "condition": {
4228                        "op":"eq",
4229                        "name": "event.contexts.browser.name",
4230                        "value": "Chrome"
4231                    }
4232                },
4233                {
4234                    "name": "Default",
4235                    "scoreComponents": [
4236                        {
4237                            "measurement": "a",
4238                            "weight": 0.15,
4239                            "p10": 100,
4240                            "p50": 200,
4241                        },
4242                        {
4243                            "measurement": "b",
4244                            "weight": 0.30,
4245                            "p10": 100,
4246                            "p50": 200,
4247                            "optional": true
4248                        },
4249                        {
4250                            "measurement": "c",
4251                            "weight": 0.55,
4252                            "p10": 100,
4253                            "p50": 200,
4254                            "optional": true
4255                        },
4256                    ],
4257                    "condition": {
4258                        "op": "and",
4259                        "inner": [],
4260                    }
4261                }
4262            ]
4263        }))
4264        .unwrap();
4265
4266        normalize_performance_score(&mut event, Some(&performance_score));
4267
4268        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4269        {
4270          "type": "transaction",
4271          "timestamp": 1619420405.0,
4272          "start_timestamp": 1619420400.0,
4273          "contexts": {
4274            "browser": {
4275              "name": "Chrome",
4276              "version": "120.1.1",
4277              "type": "browser",
4278            },
4279          },
4280          "measurements": {
4281            "a": {
4282              "value": 213.0,
4283              "unit": "millisecond",
4284            },
4285            "b": {
4286              "value": 213.0,
4287              "unit": "millisecond",
4288            },
4289            "score.a": {
4290              "value": 0.33333215313291975,
4291              "unit": "ratio",
4292            },
4293            "score.b": {
4294              "value": 0.66666415149198,
4295              "unit": "ratio",
4296            },
4297            "score.ratio.a": {
4298              "value": 0.9999964593987591,
4299              "unit": "ratio",
4300            },
4301            "score.ratio.b": {
4302              "value": 0.9999962272379699,
4303              "unit": "ratio",
4304            },
4305            "score.total": {
4306              "value": 0.9999963046248997,
4307              "unit": "ratio",
4308            },
4309            "score.weight.a": {
4310              "value": 0.33333333333333337,
4311              "unit": "ratio",
4312            },
4313            "score.weight.b": {
4314              "value": 0.6666666666666667,
4315              "unit": "ratio",
4316            },
4317            "score.weight.c": {
4318              "value": 0.0,
4319              "unit": "ratio",
4320            },
4321          },
4322        }
4323        "###);
4324    }
4325
4326    #[test]
4327    fn test_computed_performance_score_falls_back_to_default_profile() {
4328        let json = r#"
4329        {
4330            "type": "transaction",
4331            "timestamp": "2021-04-26T08:00:05+0100",
4332            "start_timestamp": "2021-04-26T08:00:00+0100",
4333            "measurements": {
4334                "a": {"value": 213, "unit": "millisecond"},
4335                "b": {"value": 213, "unit": "millisecond"}
4336            },
4337            "contexts": {}
4338        }
4339        "#;
4340
4341        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4342
4343        let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4344            "profiles": [
4345                {
4346                    "name": "Mobile",
4347                    "scoreComponents": [
4348                        {
4349                            "measurement": "a",
4350                            "weight": 0.15,
4351                            "p10": 900,
4352                            "p50": 1600,
4353                            "optional": true
4354                        },
4355                        {
4356                            "measurement": "b",
4357                            "weight": 0.30,
4358                            "p10": 1200,
4359                            "p50": 2400,
4360                            "optional": true
4361                        },
4362                        {
4363                            "measurement": "c",
4364                            "weight": 0.55,
4365                            "p10": 1200,
4366                            "p50": 2400,
4367                            "optional": true
4368                        },
4369                    ],
4370                    "condition": {
4371                        "op":"eq",
4372                        "name": "event.contexts.browser.name",
4373                        "value": "Chrome Mobile"
4374                    }
4375                },
4376                {
4377                    "name": "Desktop",
4378                    "scoreComponents": [
4379                        {
4380                            "measurement": "a",
4381                            "weight": 0.15,
4382                            "p10": 900,
4383                            "p50": 1600,
4384                            "optional": true
4385                        },
4386                        {
4387                            "measurement": "b",
4388                            "weight": 0.30,
4389                            "p10": 1200,
4390                            "p50": 2400,
4391                            "optional": true
4392                        },
4393                        {
4394                            "measurement": "c",
4395                            "weight": 0.55,
4396                            "p10": 1200,
4397                            "p50": 2400,
4398                            "optional": true
4399                        },
4400                    ],
4401                    "condition": {
4402                        "op":"eq",
4403                        "name": "event.contexts.browser.name",
4404                        "value": "Chrome"
4405                    }
4406                },
4407                {
4408                    "name": "Default",
4409                    "scoreComponents": [
4410                        {
4411                            "measurement": "a",
4412                            "weight": 0.15,
4413                            "p10": 100,
4414                            "p50": 200,
4415                            "optional": true
4416                        },
4417                        {
4418                            "measurement": "b",
4419                            "weight": 0.30,
4420                            "p10": 100,
4421                            "p50": 200,
4422                            "optional": true
4423                        },
4424                        {
4425                            "measurement": "c",
4426                            "weight": 0.55,
4427                            "p10": 100,
4428                            "p50": 200,
4429                            "optional": true
4430                        },
4431                    ],
4432                    "condition": {
4433                        "op": "and",
4434                        "inner": [],
4435                    }
4436                }
4437            ]
4438        }))
4439        .unwrap();
4440
4441        normalize_performance_score(&mut event, Some(&performance_score));
4442
4443        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4444        {
4445          "type": "transaction",
4446          "timestamp": 1619420405.0,
4447          "start_timestamp": 1619420400.0,
4448          "contexts": {},
4449          "measurements": {
4450            "a": {
4451              "value": 213.0,
4452              "unit": "millisecond",
4453            },
4454            "b": {
4455              "value": 213.0,
4456              "unit": "millisecond",
4457            },
4458            "score.a": {
4459              "value": 0.15121816827413334,
4460              "unit": "ratio",
4461            },
4462            "score.b": {
4463              "value": 0.3024363365482667,
4464              "unit": "ratio",
4465            },
4466            "score.ratio.a": {
4467              "value": 0.45365450482239994,
4468              "unit": "ratio",
4469            },
4470            "score.ratio.b": {
4471              "value": 0.45365450482239994,
4472              "unit": "ratio",
4473            },
4474            "score.total": {
4475              "value": 0.4536545048224,
4476              "unit": "ratio",
4477            },
4478            "score.weight.a": {
4479              "value": 0.33333333333333337,
4480              "unit": "ratio",
4481            },
4482            "score.weight.b": {
4483              "value": 0.6666666666666667,
4484              "unit": "ratio",
4485            },
4486            "score.weight.c": {
4487              "value": 0.0,
4488              "unit": "ratio",
4489            },
4490          },
4491        }
4492        "###);
4493    }
4494
4495    #[test]
4496    fn test_normalization_removes_reprocessing_context() {
4497        let json = r#"{
4498            "contexts": {
4499                "reprocessing": {}
4500            }
4501        }"#;
4502        let mut event = Annotated::<Event>::from_json(json).unwrap();
4503        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4504        normalize_event(&mut event, &NormalizationConfig::default());
4505        assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4506    }
4507
4508    #[test]
4509    fn test_renormalization_does_not_remove_reprocessing_context() {
4510        let json = r#"{
4511            "contexts": {
4512                "reprocessing": {}
4513            }
4514        }"#;
4515        let mut event = Annotated::<Event>::from_json(json).unwrap();
4516        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4517        normalize_event(
4518            &mut event,
4519            &NormalizationConfig {
4520                is_renormalize: true,
4521                ..Default::default()
4522            },
4523        );
4524        assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4525    }
4526
4527    #[test]
4528    fn test_normalize_user() {
4529        let json = r#"{
4530            "user": {
4531                "id": "123456",
4532                "username": "john",
4533                "other": "value"
4534            }
4535        }"#;
4536        let mut event = Annotated::<Event>::from_json(json).unwrap();
4537        normalize_user(event.value_mut().as_mut().unwrap());
4538
4539        let user = event.value().unwrap().user.value().unwrap();
4540        assert_eq!(user.data, {
4541            let mut map = Object::new();
4542            map.insert(
4543                "other".to_owned(),
4544                Annotated::new(Value::String("value".to_owned())),
4545            );
4546            Annotated::new(map)
4547        });
4548        assert_eq!(user.other, Object::new());
4549        assert_eq!(user.username, Annotated::new("john".to_owned().into()));
4550        assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
4551    }
4552
4553    #[test]
4554    fn test_handle_types_in_spaced_exception_values() {
4555        let mut exception = Annotated::new(Exception {
4556            value: Annotated::new("ValueError: unauthorized".to_owned().into()),
4557            ..Exception::default()
4558        });
4559        normalize_exception(&mut exception);
4560
4561        let exception = exception.value().unwrap();
4562        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4563        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4564    }
4565
4566    #[test]
4567    fn test_handle_types_in_non_spaced_excepton_values() {
4568        let mut exception = Annotated::new(Exception {
4569            value: Annotated::new("ValueError:unauthorized".to_owned().into()),
4570            ..Exception::default()
4571        });
4572        normalize_exception(&mut exception);
4573
4574        let exception = exception.value().unwrap();
4575        assert_eq!(exception.value.as_str(), Some("unauthorized"));
4576        assert_eq!(exception.ty.as_str(), Some("ValueError"));
4577    }
4578
4579    #[test]
4580    fn test_rejects_empty_exception_fields() {
4581        let mut exception = Annotated::new(Exception {
4582            value: Annotated::new("".to_owned().into()),
4583            ty: Annotated::new("".to_owned()),
4584            ..Default::default()
4585        });
4586
4587        normalize_exception(&mut exception);
4588
4589        assert!(exception.value().is_none());
4590        assert!(exception.meta().has_errors());
4591    }
4592
4593    #[test]
4594    fn test_json_value() {
4595        let mut exception = Annotated::new(Exception {
4596            value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
4597            ..Exception::default()
4598        });
4599
4600        normalize_exception(&mut exception);
4601
4602        let exception = exception.value().unwrap();
4603
4604        // Don't split a json-serialized value on the colon
4605        assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4606        assert_eq!(exception.ty.value(), None);
4607    }
4608
4609    #[test]
4610    fn test_exception_invalid() {
4611        let mut exception = Annotated::new(Exception::default());
4612
4613        normalize_exception(&mut exception);
4614
4615        let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4616            error.insert("attribute", "type or value");
4617        });
4618        assert_eq!(
4619            exception.meta().iter_errors().collect_tuple(),
4620            Some((&expected,))
4621        );
4622    }
4623
4624    #[test]
4625    fn test_normalize_exception() {
4626        let mut event = Annotated::new(Event {
4627            exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4628                // Exception with missing type and value
4629                ty: Annotated::empty(),
4630                value: Annotated::empty(),
4631                ..Default::default()
4632            })])),
4633            ..Default::default()
4634        });
4635
4636        normalize_event(&mut event, &NormalizationConfig::default());
4637
4638        let exception = event
4639            .value()
4640            .unwrap()
4641            .exceptions
4642            .value()
4643            .unwrap()
4644            .values
4645            .value()
4646            .unwrap()
4647            .first()
4648            .unwrap();
4649
4650        assert_debug_snapshot!(exception.meta(), @r###"
4651        Meta {
4652            remarks: [],
4653            errors: [
4654                Error {
4655                    kind: MissingAttribute,
4656                    data: {
4657                        "attribute": String(
4658                            "type or value",
4659                        ),
4660                    },
4661                },
4662            ],
4663            original_length: None,
4664            original_value: Some(
4665                Object(
4666                    {
4667                        "mechanism": ~,
4668                        "module": ~,
4669                        "raw_stacktrace": ~,
4670                        "stacktrace": ~,
4671                        "thread_id": ~,
4672                        "type": ~,
4673                        "value": ~,
4674                    },
4675                ),
4676            ),
4677        }
4678        "###);
4679    }
4680
4681    #[test]
4682    fn test_normalize_breadcrumbs() {
4683        let mut event = Event {
4684            breadcrumbs: Annotated::new(Values {
4685                values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4686                ..Default::default()
4687            }),
4688            ..Default::default()
4689        };
4690        normalize_breadcrumbs(&mut event);
4691
4692        let breadcrumb = event
4693            .breadcrumbs
4694            .value()
4695            .unwrap()
4696            .values
4697            .value()
4698            .unwrap()
4699            .first()
4700            .unwrap()
4701            .value()
4702            .unwrap();
4703        assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4704        assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4705    }
4706
4707    #[test]
4708    fn test_other_debug_images_have_meta_errors() {
4709        let mut event = Event {
4710            debug_meta: Annotated::new(DebugMeta {
4711                images: Annotated::new(vec![Annotated::new(
4712                    DebugImage::Other(BTreeMap::default()),
4713                )]),
4714                ..Default::default()
4715            }),
4716            ..Default::default()
4717        };
4718        normalize_debug_meta(&mut event);
4719
4720        let debug_image_meta = event
4721            .debug_meta
4722            .value()
4723            .unwrap()
4724            .images
4725            .value()
4726            .unwrap()
4727            .first()
4728            .unwrap()
4729            .meta();
4730        assert_debug_snapshot!(debug_image_meta, @r###"
4731        Meta {
4732            remarks: [],
4733            errors: [
4734                Error {
4735                    kind: InvalidData,
4736                    data: {
4737                        "reason": String(
4738                            "unsupported debug image type",
4739                        ),
4740                    },
4741                },
4742            ],
4743            original_length: None,
4744            original_value: Some(
4745                Object(
4746                    {},
4747                ),
4748            ),
4749        }
4750        "###);
4751    }
4752
4753    #[test]
4754    fn test_skip_span_normalization_when_configured() {
4755        let json = r#"{
4756            "type": "transaction",
4757            "start_timestamp": 1,
4758            "timestamp": 2,
4759            "contexts": {
4760                "trace": {
4761                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4762                    "span_id": "aaaaaaaaaaaaaaaa"
4763                }
4764            },
4765            "spans": [
4766                {
4767                    "op": "db",
4768                    "description": "SELECT * FROM table;",
4769                    "start_timestamp": 1,
4770                    "timestamp": 2,
4771                    "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4772                    "span_id": "bbbbbbbbbbbbbbbb",
4773                    "parent_span_id": "aaaaaaaaaaaaaaaa"
4774                }
4775            ]
4776        }"#;
4777
4778        let mut event = Annotated::<Event>::from_json(json).unwrap();
4779        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4780        normalize_event(
4781            &mut event,
4782            &NormalizationConfig {
4783                is_renormalize: true,
4784                ..Default::default()
4785            },
4786        );
4787        assert!(get_value!(event.spans[0].exclusive_time).is_none());
4788        normalize_event(
4789            &mut event,
4790            &NormalizationConfig {
4791                is_renormalize: false,
4792                ..Default::default()
4793            },
4794        );
4795        assert!(get_value!(event.spans[0].exclusive_time).is_some());
4796    }
4797
4798    #[test]
4799    fn test_normalize_trace_context_tags_extracts_lcp_info() {
4800        let json = r#"{
4801            "type": "transaction",
4802            "start_timestamp": 1,
4803            "timestamp": 2,
4804            "contexts": {
4805                "trace": {
4806                    "data": {
4807                        "lcp.element": "body > div#app > div > h1#header",
4808                        "lcp.size": 24827,
4809                        "lcp.id": "header",
4810                        "lcp.url": "http://example.com/image.jpg"
4811                    }
4812                }
4813            },
4814            "measurements": {
4815                "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4816            }
4817        }"#;
4818        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4819        normalize_trace_context_tags(&mut event);
4820        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4821        {
4822          "type": "transaction",
4823          "timestamp": 2.0,
4824          "start_timestamp": 1.0,
4825          "contexts": {
4826            "trace": {
4827              "data": {
4828                "lcp.element": "body > div#app > div > h1#header",
4829                "lcp.size": 24827,
4830                "lcp.id": "header",
4831                "lcp.url": "http://example.com/image.jpg",
4832              },
4833              "type": "trace",
4834            },
4835          },
4836          "tags": [
4837            [
4838              "lcp.element",
4839              "body > div#app > div > h1#header",
4840            ],
4841            [
4842              "lcp.size",
4843              "24827",
4844            ],
4845            [
4846              "lcp.id",
4847              "header",
4848            ],
4849            [
4850              "lcp.url",
4851              "http://example.com/image.jpg",
4852            ],
4853          ],
4854          "measurements": {
4855            "lcp": {
4856              "value": 146.20000000298023,
4857              "unit": "millisecond",
4858            },
4859          },
4860        }
4861        "###);
4862    }
4863
4864    #[test]
4865    fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4866        let json = r#"{
4867          "type": "transaction",
4868          "start_timestamp": 1,
4869          "timestamp": 2,
4870          "contexts": {
4871              "trace": {
4872                  "data": {
4873                      "lcp.element": "body > div#app > div > h1#id",
4874                      "lcp.size": 33333,
4875                      "lcp.id": "id",
4876                      "lcp.url": "http://example.com/another-image.jpg"
4877                  }
4878              }
4879          },
4880          "tags": {
4881              "lcp.element": "body > div#app > div > h1#header",
4882              "lcp.size": 24827,
4883              "lcp.id": "header",
4884              "lcp.url": "http://example.com/image.jpg"
4885          },
4886          "measurements": {
4887              "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4888          }
4889        }"#;
4890        let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4891        normalize_trace_context_tags(&mut event);
4892        insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4893        {
4894          "type": "transaction",
4895          "timestamp": 2.0,
4896          "start_timestamp": 1.0,
4897          "contexts": {
4898            "trace": {
4899              "data": {
4900                "lcp.element": "body > div#app > div > h1#id",
4901                "lcp.size": 33333,
4902                "lcp.id": "id",
4903                "lcp.url": "http://example.com/another-image.jpg",
4904              },
4905              "type": "trace",
4906            },
4907          },
4908          "tags": [
4909            [
4910              "lcp.element",
4911              "body > div#app > div > h1#header",
4912            ],
4913            [
4914              "lcp.id",
4915              "header",
4916            ],
4917            [
4918              "lcp.size",
4919              "24827",
4920            ],
4921            [
4922              "lcp.url",
4923              "http://example.com/image.jpg",
4924            ],
4925          ],
4926          "measurements": {
4927            "lcp": {
4928              "value": 146.20000000298023,
4929              "unit": "millisecond",
4930            },
4931          },
4932        }
4933        "###);
4934    }
4935}