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