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