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