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
1707 use relay_event_schema::protocol::SpanData;
1708 use relay_pattern::Pattern;
1709 use relay_protocol::assert_annotated_snapshot;
1710 use std::collections::BTreeMap;
1711 use std::collections::HashMap;
1712
1713 use insta::assert_debug_snapshot;
1714 use itertools::Itertools;
1715 use relay_event_schema::protocol::{Breadcrumb, Csp, DebugMeta, DeviceContext, Values};
1716 use relay_protocol::{SerializableAnnotated, get_value};
1717 use serde_json::json;
1718
1719 use super::*;
1720 use crate::eap;
1721 use crate::{ClientHints, MeasurementsConfig, ModelCostV2, ModelMetadataEntry};
1722
1723 const IOS_MOBILE_EVENT: &str = r#"
1724 {
1725 "sdk": {"name": "sentry.cocoa"},
1726 "contexts": {
1727 "trace": {
1728 "op": "ui.load"
1729 }
1730 },
1731 "measurements": {
1732 "app_start_warm": {
1733 "value": 8049.345970153808,
1734 "unit": "millisecond"
1735 },
1736 "time_to_full_display": {
1737 "value": 8240.571022033691,
1738 "unit": "millisecond"
1739 },
1740 "time_to_initial_display": {
1741 "value": 8049.345970153808,
1742 "unit": "millisecond"
1743 }
1744 }
1745 }
1746 "#;
1747
1748 const ANDROID_MOBILE_EVENT: &str = r#"
1749 {
1750 "sdk": {"name": "sentry.java.android"},
1751 "contexts": {
1752 "trace": {
1753 "op": "ui.load"
1754 }
1755 },
1756 "measurements": {
1757 "app_start_cold": {
1758 "value": 22648,
1759 "unit": "millisecond"
1760 },
1761 "time_to_full_display": {
1762 "value": 22647,
1763 "unit": "millisecond"
1764 },
1765 "time_to_initial_display": {
1766 "value": 22647,
1767 "unit": "millisecond"
1768 }
1769 }
1770 }
1771 "#;
1772
1773 fn collect_span_data<const N: usize>(event: Annotated<Event>) -> [Annotated<SpanData>; N] {
1774 get_value!(event.spans!)
1775 .iter()
1776 .map(|span| Annotated::new(get_value!(span.data!).clone()))
1777 .collect::<Vec<_>>()
1778 .try_into()
1779 .unwrap()
1780 }
1781
1782 fn trace_context_data(event: &Event) -> &Annotated<SpanData> {
1783 &event.context::<TraceContext>().unwrap().data
1784 }
1785
1786 fn app_vitals_start_screen_event(
1787 ty: &str,
1788 transaction: Option<&str>,
1789 trace_op: &str,
1790 measurement: Option<&str>,
1791 existing_screen: Option<&str>,
1792 ) -> Event {
1793 let mut payload = json!({
1794 "type": ty,
1795 "contexts": {"trace": {"op": trace_op}},
1796 "measurements": {},
1797 });
1798
1799 if let Some(transaction) = transaction {
1800 payload["transaction"] = json!(transaction);
1801 }
1802
1803 if let Some(measurement) = measurement {
1804 payload["measurements"] = json!({
1805 measurement: {"value": 1234.0, "unit": "millisecond"}
1806 });
1807 }
1808
1809 if let Some(screen) = existing_screen {
1810 payload["contexts"]["trace"]["data"] = json!({APP__VITALS__START__SCREEN: screen});
1811 }
1812
1813 Annotated::<Event>::from_json(&payload.to_string())
1814 .unwrap()
1815 .into_value()
1816 .unwrap()
1817 }
1818
1819 #[test]
1820 fn test_normalize_dist_none() {
1821 let mut dist = Annotated::default();
1822 normalize_dist(&mut dist);
1823 assert_eq!(dist.value(), None);
1824 }
1825
1826 #[test]
1827 fn test_normalize_dist_empty() {
1828 let mut dist = Annotated::new("".to_owned());
1829 normalize_dist(&mut dist);
1830 assert_eq!(dist.value(), None);
1831 }
1832
1833 #[test]
1834 fn test_normalize_dist_trim() {
1835 let mut dist = Annotated::new(" foo ".to_owned());
1836 normalize_dist(&mut dist);
1837 assert_eq!(dist.value(), Some(&"foo".to_owned()));
1838 }
1839
1840 #[test]
1841 fn test_normalize_dist_whitespace() {
1842 let mut dist = Annotated::new(" ".to_owned());
1843 normalize_dist(&mut dist);
1844 assert_eq!(dist.value(), None);
1845 }
1846
1847 #[test]
1848 fn test_normalize_platform_and_level_with_transaction_event() {
1849 let json = r#"
1850 {
1851 "type": "transaction"
1852 }
1853 "#;
1854
1855 let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1856 else {
1857 panic!("Invalid transaction json");
1858 };
1859
1860 normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1861
1862 assert_eq!(event.level.value().unwrap().to_string(), "info");
1863 assert_eq!(event.ty.value().unwrap().to_string(), "transaction");
1864 assert_eq!(event.platform.as_str().unwrap(), "other");
1865 }
1866
1867 #[test]
1868 fn test_normalize_platform_and_level_with_error_event() {
1869 let json = r#"
1870 {
1871 "type": "error",
1872 "exception": {
1873 "values": [{"type": "ValueError", "value": "Should not happen"}]
1874 }
1875 }
1876 "#;
1877
1878 let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1879 else {
1880 panic!("Invalid error json");
1881 };
1882
1883 normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1884
1885 assert_eq!(event.level.value().unwrap().to_string(), "error");
1886 assert_eq!(event.ty.value().unwrap().to_string(), "error");
1887 assert_eq!(event.platform.value().unwrap().to_owned(), "other");
1888 }
1889
1890 #[test]
1891 fn test_computed_measurements() {
1892 let json = r#"
1893 {
1894 "type": "transaction",
1895 "timestamp": "2021-04-26T08:00:05+0100",
1896 "start_timestamp": "2021-04-26T08:00:00+0100",
1897 "measurements": {
1898 "frames_slow": {"value": 1},
1899 "frames_frozen": {"value": 2},
1900 "frames_total": {"value": 4},
1901 "stall_total_time": {"value": 4000, "unit": "millisecond"}
1902 }
1903 }
1904 "#;
1905
1906 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1907
1908 normalize_event_measurements(&mut event, None, None);
1909
1910 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1911 {
1912 "type": "transaction",
1913 "timestamp": 1619420405.0,
1914 "start_timestamp": 1619420400.0,
1915 "measurements": {
1916 "frames_frozen": {
1917 "value": 2.0,
1918 "unit": "none",
1919 },
1920 "frames_frozen_rate": {
1921 "value": 0.5,
1922 "unit": "ratio",
1923 },
1924 "frames_slow": {
1925 "value": 1.0,
1926 "unit": "none",
1927 },
1928 "frames_slow_rate": {
1929 "value": 0.25,
1930 "unit": "ratio",
1931 },
1932 "frames_total": {
1933 "value": 4.0,
1934 "unit": "none",
1935 },
1936 "stall_percentage": {
1937 "value": 0.8,
1938 "unit": "ratio",
1939 },
1940 "stall_total_time": {
1941 "value": 4000.0,
1942 "unit": "millisecond",
1943 },
1944 },
1945 }
1946 "###);
1947 }
1948
1949 #[test]
1950 fn test_filter_custom_measurements() {
1951 let json = r#"
1952 {
1953 "type": "transaction",
1954 "timestamp": "2021-04-26T08:00:05+0100",
1955 "start_timestamp": "2021-04-26T08:00:00+0100",
1956 "measurements": {
1957 "my_custom_measurement_1": {"value": 123},
1958 "frames_frozen": {"value": 666, "unit": "invalid_unit"},
1959 "frames_slow": {"value": 1},
1960 "my_custom_measurement_3": {"value": 456},
1961 "my_custom_measurement_2": {"value": 789}
1962 }
1963 }
1964 "#;
1965 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1966
1967 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
1968 "builtinMeasurements": [
1969 {"name": "frames_frozen", "unit": "none"},
1970 {"name": "frames_slow", "unit": "none"}
1971 ],
1972 "maxCustomMeasurements": 2,
1973 "stray_key": "zzz"
1974 }))
1975 .unwrap();
1976
1977 let dynamic_measurement_config =
1978 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
1979
1980 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
1981
1982 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1984 {
1985 "type": "transaction",
1986 "timestamp": 1619420405.0,
1987 "start_timestamp": 1619420400.0,
1988 "measurements": {
1989 "frames_slow": {
1990 "value": 1.0,
1991 "unit": "none",
1992 },
1993 "my_custom_measurement_1": {
1994 "value": 123.0,
1995 "unit": "none",
1996 },
1997 "my_custom_measurement_2": {
1998 "value": 789.0,
1999 "unit": "none",
2000 },
2001 },
2002 "_meta": {
2003 "measurements": {
2004 "": Meta(Some(MetaInner(
2005 err: [
2006 [
2007 "invalid_data",
2008 {
2009 "reason": "Too many measurements: my_custom_measurement_3",
2010 },
2011 ],
2012 ],
2013 val: Some({
2014 "my_custom_measurement_3": {
2015 "unit": "none",
2016 "value": 456.0,
2017 },
2018 }),
2019 ))),
2020 },
2021 },
2022 }
2023 "###);
2024 }
2025
2026 #[test]
2027 fn test_normalize_units() {
2028 let mut measurements = Annotated::<Measurements>::from_json(
2029 r#"{
2030 "fcp": {"value": 1.1},
2031 "stall_count": {"value": 3.3},
2032 "foo": {"value": 8.8}
2033 }"#,
2034 )
2035 .unwrap()
2036 .into_value()
2037 .unwrap();
2038 insta::assert_debug_snapshot!(measurements, @r###"
2039 Measurements(
2040 {
2041 "fcp": Measurement {
2042 value: 1.1,
2043 unit: ~,
2044 },
2045 "foo": Measurement {
2046 value: 8.8,
2047 unit: ~,
2048 },
2049 "stall_count": Measurement {
2050 value: 3.3,
2051 unit: ~,
2052 },
2053 },
2054 )
2055 "###);
2056 normalize_units(&mut measurements);
2057 insta::assert_debug_snapshot!(measurements, @r###"
2058 Measurements(
2059 {
2060 "fcp": Measurement {
2061 value: 1.1,
2062 unit: Duration(
2063 MilliSecond,
2064 ),
2065 },
2066 "foo": Measurement {
2067 value: 8.8,
2068 unit: None,
2069 },
2070 "stall_count": Measurement {
2071 value: 3.3,
2072 unit: None,
2073 },
2074 },
2075 )
2076 "###);
2077 }
2078
2079 #[test]
2080 fn test_normalize_security_report() {
2081 let mut event = Event {
2082 csp: Annotated::from(Csp::default()),
2083 ..Default::default()
2084 };
2085 let ipaddr = IpAddr("213.164.1.114".to_owned());
2086
2087 let client_ip = Some(&ipaddr);
2088
2089 let user_agent = RawUserAgentInfo {
2090 user_agent: Some(
2091 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0",
2092 ),
2093 client_hints: ClientHints {
2094 sec_ch_ua_platform: Some("macOS"),
2095 sec_ch_ua_platform_version: Some("13.2.0"),
2096 sec_ch_ua: Some(
2097 r#""Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110""#,
2098 ),
2099 sec_ch_ua_model: Some("some model"),
2100 },
2101 };
2102
2103 normalize_security_report(&mut event, client_ip, &user_agent);
2106
2107 let headers = event
2108 .request
2109 .value_mut()
2110 .get_or_insert_with(Request::default)
2111 .headers
2112 .value_mut()
2113 .get_or_insert_with(Headers::default);
2114
2115 assert_eq!(
2116 event.user.value().unwrap().ip_address,
2117 Annotated::from(ipaddr)
2118 );
2119 assert_eq!(
2120 headers.get_header(RawUserAgentInfo::USER_AGENT),
2121 user_agent.user_agent
2122 );
2123 assert_eq!(
2124 headers.get_header(ClientHints::SEC_CH_UA),
2125 user_agent.client_hints.sec_ch_ua,
2126 );
2127 assert_eq!(
2128 headers.get_header(ClientHints::SEC_CH_UA_MODEL),
2129 user_agent.client_hints.sec_ch_ua_model,
2130 );
2131 assert_eq!(
2132 headers.get_header(ClientHints::SEC_CH_UA_PLATFORM),
2133 user_agent.client_hints.sec_ch_ua_platform,
2134 );
2135 assert_eq!(
2136 headers.get_header(ClientHints::SEC_CH_UA_PLATFORM_VERSION),
2137 user_agent.client_hints.sec_ch_ua_platform_version,
2138 );
2139
2140 assert!(
2141 std::mem::size_of_val(&ClientHints::<&str>::default()) == 64,
2142 "If you add new fields, update the test accordingly"
2143 );
2144 }
2145
2146 #[test]
2147 fn test_no_device_class() {
2148 let mut event = Event {
2149 ..Default::default()
2150 };
2151 normalize_device_class(&mut event);
2152 let tags = &event.tags.value_mut().get_or_insert_with(Tags::default).0;
2153 assert_eq!(None, tags.get("device_class"));
2154 }
2155
2156 #[test]
2157 fn test_apple_low_device_class() {
2158 let mut event = Event {
2159 contexts: {
2160 let mut contexts = Contexts::new();
2161 contexts.add(DeviceContext {
2162 family: "iPhone".to_owned().into(),
2163 model: "iPhone8,4".to_owned().into(),
2164 ..Default::default()
2165 });
2166 Annotated::new(contexts)
2167 },
2168 ..Default::default()
2169 };
2170 normalize_device_class(&mut event);
2171 assert_debug_snapshot!(event.tags, @r###"
2172 Tags(
2173 PairList(
2174 [
2175 TagEntry(
2176 "device.class",
2177 "1",
2178 ),
2179 ],
2180 ),
2181 )
2182 "###);
2183 }
2184
2185 #[test]
2186 fn test_apple_medium_device_class() {
2187 let mut event = Event {
2188 contexts: {
2189 let mut contexts = Contexts::new();
2190 contexts.add(DeviceContext {
2191 family: "iPhone".to_owned().into(),
2192 model: "iPhone12,8".to_owned().into(),
2193 ..Default::default()
2194 });
2195 Annotated::new(contexts)
2196 },
2197 ..Default::default()
2198 };
2199 normalize_device_class(&mut event);
2200 assert_debug_snapshot!(event.tags, @r###"
2201 Tags(
2202 PairList(
2203 [
2204 TagEntry(
2205 "device.class",
2206 "2",
2207 ),
2208 ],
2209 ),
2210 )
2211 "###);
2212 }
2213
2214 #[test]
2215 fn test_android_low_device_class() {
2216 let mut event = Event {
2217 contexts: {
2218 let mut contexts = Contexts::new();
2219 contexts.add(DeviceContext {
2220 family: "android".to_owned().into(),
2221 processor_frequency: 1000.into(),
2222 processor_count: 6.into(),
2223 memory_size: (2 * 1024 * 1024 * 1024).into(),
2224 ..Default::default()
2225 });
2226 Annotated::new(contexts)
2227 },
2228 ..Default::default()
2229 };
2230 normalize_device_class(&mut event);
2231 assert_debug_snapshot!(event.tags, @r###"
2232 Tags(
2233 PairList(
2234 [
2235 TagEntry(
2236 "device.class",
2237 "1",
2238 ),
2239 ],
2240 ),
2241 )
2242 "###);
2243 }
2244
2245 #[test]
2246 fn test_android_medium_device_class() {
2247 let mut event = Event {
2248 contexts: {
2249 let mut contexts = Contexts::new();
2250 contexts.add(DeviceContext {
2251 family: "android".to_owned().into(),
2252 processor_frequency: 2000.into(),
2253 processor_count: 8.into(),
2254 memory_size: (6 * 1024 * 1024 * 1024).into(),
2255 ..Default::default()
2256 });
2257 Annotated::new(contexts)
2258 },
2259 ..Default::default()
2260 };
2261 normalize_device_class(&mut event);
2262 assert_debug_snapshot!(event.tags, @r###"
2263 Tags(
2264 PairList(
2265 [
2266 TagEntry(
2267 "device.class",
2268 "2",
2269 ),
2270 ],
2271 ),
2272 )
2273 "###);
2274 }
2275
2276 #[test]
2277 fn test_android_high_device_class() {
2278 let mut event = Event {
2279 contexts: {
2280 let mut contexts = Contexts::new();
2281 contexts.add(DeviceContext {
2282 family: "android".to_owned().into(),
2283 processor_frequency: 2500.into(),
2284 processor_count: 8.into(),
2285 memory_size: (6 * 1024 * 1024 * 1024).into(),
2286 ..Default::default()
2287 });
2288 Annotated::new(contexts)
2289 },
2290 ..Default::default()
2291 };
2292 normalize_device_class(&mut event);
2293 assert_debug_snapshot!(event.tags, @r###"
2294 Tags(
2295 PairList(
2296 [
2297 TagEntry(
2298 "device.class",
2299 "3",
2300 ),
2301 ],
2302 ),
2303 )
2304 "###);
2305 }
2306
2307 #[test]
2308 fn test_keeps_valid_measurement() {
2309 let name = "lcp";
2310 let measurement = Measurement {
2311 value: Annotated::new(420.69.try_into().unwrap()),
2312 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2313 };
2314
2315 assert!(!is_measurement_dropped(name, measurement));
2316 }
2317
2318 #[test]
2319 fn test_drops_too_long_measurement_names() {
2320 let name = "lcpppppppppppppppppppppppppppp";
2321 let measurement = Measurement {
2322 value: Annotated::new(420.69.try_into().unwrap()),
2323 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2324 };
2325
2326 assert!(is_measurement_dropped(name, measurement));
2327 }
2328
2329 #[test]
2330 fn test_drops_measurements_with_invalid_characters() {
2331 let name = "i æm frøm nørwåy";
2332 let measurement = Measurement {
2333 value: Annotated::new(420.69.try_into().unwrap()),
2334 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2335 };
2336
2337 assert!(is_measurement_dropped(name, measurement));
2338 }
2339
2340 fn is_measurement_dropped(name: &str, measurement: Measurement) -> bool {
2341 let max_name_and_unit_len = Some(30);
2342
2343 let mut measurements: BTreeMap<String, Annotated<Measurement>> = Object::new();
2344 measurements.insert(name.to_owned(), Annotated::new(measurement));
2345
2346 let mut measurements = Measurements(measurements);
2347 let mut meta = Meta::default();
2348 let measurements_config = MeasurementsConfig {
2349 max_custom_measurements: 1,
2350 ..Default::default()
2351 };
2352
2353 let dynamic_config = CombinedMeasurementsConfig::new(Some(&measurements_config), None);
2354
2355 assert_eq!(measurements.len(), 1);
2358
2359 remove_invalid_measurements(
2360 &mut measurements,
2361 &mut meta,
2362 dynamic_config,
2363 max_name_and_unit_len,
2364 );
2365
2366 measurements.is_empty()
2368 }
2369
2370 #[test]
2371 fn test_custom_measurements_not_dropped() {
2372 let mut measurements = Measurements(BTreeMap::from([(
2373 "custom_measurement".to_owned(),
2374 Annotated::new(Measurement {
2375 value: Annotated::new(42.0.try_into().unwrap()),
2376 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2377 }),
2378 )]));
2379
2380 let original = measurements.clone();
2381 remove_invalid_measurements(
2382 &mut measurements,
2383 &mut Meta::default(),
2384 CombinedMeasurementsConfig::new(None, None),
2385 Some(30),
2386 );
2387
2388 assert_eq!(original, measurements);
2389 }
2390
2391 #[test]
2392 fn test_normalize_app_start_measurements_does_not_add_measurements() {
2393 let mut measurements = Annotated::<Measurements>::from_json(r###"{}"###)
2394 .unwrap()
2395 .into_value()
2396 .unwrap();
2397 insta::assert_debug_snapshot!(measurements, @r###"
2398 Measurements(
2399 {},
2400 )
2401 "###);
2402 normalize_app_start_measurements(&mut measurements);
2403 insta::assert_debug_snapshot!(measurements, @r###"
2404 Measurements(
2405 {},
2406 )
2407 "###);
2408 }
2409
2410 #[test]
2411 fn test_normalize_app_start_cold_measurements() {
2412 let mut measurements =
2413 Annotated::<Measurements>::from_json(r#"{"app.start.cold": {"value": 1.1}}"#)
2414 .unwrap()
2415 .into_value()
2416 .unwrap();
2417 insta::assert_debug_snapshot!(measurements, @r###"
2418 Measurements(
2419 {
2420 "app.start.cold": Measurement {
2421 value: 1.1,
2422 unit: ~,
2423 },
2424 },
2425 )
2426 "###);
2427 normalize_app_start_measurements(&mut measurements);
2428 insta::assert_debug_snapshot!(measurements, @r###"
2429 Measurements(
2430 {
2431 "app_start_cold": Measurement {
2432 value: 1.1,
2433 unit: ~,
2434 },
2435 },
2436 )
2437 "###);
2438 }
2439
2440 #[test]
2441 fn test_normalize_app_start_warm_measurements() {
2442 let mut measurements =
2443 Annotated::<Measurements>::from_json(r#"{"app.start.warm": {"value": 1.1}}"#)
2444 .unwrap()
2445 .into_value()
2446 .unwrap();
2447 insta::assert_debug_snapshot!(measurements, @r###"
2448 Measurements(
2449 {
2450 "app.start.warm": Measurement {
2451 value: 1.1,
2452 unit: ~,
2453 },
2454 },
2455 )
2456 "###);
2457 normalize_app_start_measurements(&mut measurements);
2458 insta::assert_debug_snapshot!(measurements, @r###"
2459 Measurements(
2460 {
2461 "app_start_warm": Measurement {
2462 value: 1.1,
2463 unit: ~,
2464 },
2465 },
2466 )
2467 "###);
2468 }
2469
2470 #[test]
2471 fn test_ai_legacy_measurements() {
2472 let json = r#"
2473 {
2474 "spans": [
2475 {
2476 "timestamp": 1702474613.0495,
2477 "start_timestamp": 1702474613.0175,
2478 "description": "OpenAI ",
2479 "op": "ai.chat_completions.openai",
2480 "span_id": "9c01bd820a083e63",
2481 "parent_span_id": "a1e13f3f06239d69",
2482 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2483 "measurements": {
2484 "ai_prompt_tokens_used": {
2485 "value": 1000
2486 },
2487 "ai_completion_tokens_used": {
2488 "value": 2000
2489 }
2490 },
2491 "data": {
2492 "gen_ai.request.model": "claude-2.1"
2493 }
2494 },
2495 {
2496 "timestamp": 1702474613.0495,
2497 "start_timestamp": 1702474613.0175,
2498 "description": "OpenAI ",
2499 "op": "ai.chat_completions.openai",
2500 "span_id": "ac01bd820a083e63",
2501 "parent_span_id": "a1e13f3f06239d69",
2502 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2503 "measurements": {
2504 "ai_prompt_tokens_used": {
2505 "value": 1000
2506 },
2507 "ai_completion_tokens_used": {
2508 "value": 2000
2509 }
2510 },
2511 "data": {
2512 "gen_ai.request.model": "gpt4-21-04"
2513 }
2514 }
2515 ]
2516 }
2517 "#;
2518
2519 let mut event = Annotated::<Event>::from_json(json).unwrap();
2520
2521 normalize_event(
2522 &mut event,
2523 &NormalizationConfig {
2524 ai_model_metadata: Some(&ModelMetadata {
2525 version: 1,
2526 models: HashMap::from([
2527 (
2528 Pattern::new("claude-2.1").unwrap(),
2529 ModelMetadataEntry {
2530 costs: Some(ModelCostV2 {
2531 input_per_token: 0.01,
2532 output_per_token: 0.02,
2533 output_reasoning_per_token: 0.03,
2534 input_cached_per_token: 0.0,
2535 input_cache_write_per_token: 0.0,
2536 }),
2537 context_size: None,
2538 },
2539 ),
2540 (
2541 Pattern::new("gpt4-21-04").unwrap(),
2542 ModelMetadataEntry {
2543 costs: Some(ModelCostV2 {
2544 input_per_token: 0.02,
2545 output_per_token: 0.03,
2546 output_reasoning_per_token: 0.04,
2547 input_cached_per_token: 0.0,
2548 input_cache_write_per_token: 0.0,
2549 }),
2550 context_size: None,
2551 },
2552 ),
2553 ]),
2554 }),
2555 ..NormalizationConfig::default()
2556 },
2557 );
2558
2559 let [span1, span2] = collect_span_data(event);
2560
2561 assert_annotated_snapshot!(span1, @r#"
2562 {
2563 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2564 "gen_ai.cost.cache_read.input_tokens": 0.0,
2565 "gen_ai.cost.input_tokens": 10.0,
2566 "gen_ai.cost.output_tokens": 40.0,
2567 "gen_ai.cost.reasoning.output_tokens": 0.0,
2568 "gen_ai.cost.total_tokens": 50.0,
2569 "gen_ai.operation.type": "ai_client",
2570 "gen_ai.request.model": "claude-2.1",
2571 "gen_ai.response.model": "claude-2.1",
2572 "gen_ai.response.tokens_per_second": 62500.0,
2573 "gen_ai.usage.input_tokens": 1000.0,
2574 "gen_ai.usage.output_tokens": 2000.0,
2575 "gen_ai.usage.total_tokens": 3000.0
2576 }
2577 "#);
2578 assert_annotated_snapshot!(span2, @r#"
2579 {
2580 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2581 "gen_ai.cost.cache_read.input_tokens": 0.0,
2582 "gen_ai.cost.input_tokens": 20.0,
2583 "gen_ai.cost.output_tokens": 60.0,
2584 "gen_ai.cost.reasoning.output_tokens": 0.0,
2585 "gen_ai.cost.total_tokens": 80.0,
2586 "gen_ai.operation.type": "ai_client",
2587 "gen_ai.request.model": "gpt4-21-04",
2588 "gen_ai.response.model": "gpt4-21-04",
2589 "gen_ai.response.tokens_per_second": 62500.0,
2590 "gen_ai.usage.input_tokens": 1000.0,
2591 "gen_ai.usage.output_tokens": 2000.0,
2592 "gen_ai.usage.total_tokens": 3000.0
2593 }
2594 "#);
2595 }
2596
2597 #[test]
2598 fn test_ai_data() {
2599 let json = r#"
2600 {
2601 "spans": [
2602 {
2603 "timestamp": 1702474614.0175,
2604 "start_timestamp": 1702474613.0175,
2605 "description": "OpenAI ",
2606 "op": "gen_ai.chat_completions.openai",
2607 "span_id": "9c01bd820a083e63",
2608 "parent_span_id": "a1e13f3f06239d69",
2609 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2610 "data": {
2611 "gen_ai.usage.input_tokens": 1000,
2612 "gen_ai.usage.output_tokens": 2000,
2613 "gen_ai.usage.reasoning.output_tokens": 1000,
2614 "gen_ai.usage.cache_read.input_tokens": 500,
2615 "gen_ai.request.model": "claude-2.1"
2616 }
2617 },
2618 {
2619 "timestamp": 1702474614.0175,
2620 "start_timestamp": 1702474613.0175,
2621 "description": "OpenAI ",
2622 "op": "gen_ai.chat_completions.openai",
2623 "span_id": "ac01bd820a083e63",
2624 "parent_span_id": "a1e13f3f06239d69",
2625 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2626 "data": {
2627 "gen_ai.usage.input_tokens": 1000,
2628 "gen_ai.usage.output_tokens": 2000,
2629 "gen_ai.request.model": "gpt4-21-04"
2630 }
2631 },
2632 {
2633 "timestamp": 1702474614.0175,
2634 "start_timestamp": 1702474613.0175,
2635 "description": "OpenAI ",
2636 "op": "gen_ai.chat_completions.openai",
2637 "span_id": "ac01bd820a083e63",
2638 "parent_span_id": "a1e13f3f06239d69",
2639 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2640 "data": {
2641 "gen_ai.usage.input_tokens": 1000,
2642 "gen_ai.usage.output_tokens": 2000,
2643 "gen_ai.response.model": "gpt4-21-04"
2644 }
2645 }
2646 ]
2647 }
2648 "#;
2649
2650 let mut event = Annotated::<Event>::from_json(json).unwrap();
2651
2652 normalize_event(
2653 &mut event,
2654 &NormalizationConfig {
2655 ai_model_metadata: Some(&ModelMetadata {
2656 version: 1,
2657 models: HashMap::from([
2658 (
2659 Pattern::new("claude-2.1").unwrap(),
2660 ModelMetadataEntry {
2661 costs: Some(ModelCostV2 {
2662 input_per_token: 0.01,
2663 output_per_token: 0.02,
2664 output_reasoning_per_token: 0.03,
2665 input_cached_per_token: 0.04,
2666 input_cache_write_per_token: 0.0,
2667 }),
2668 context_size: None,
2669 },
2670 ),
2671 (
2672 Pattern::new("gpt4-21-04").unwrap(),
2673 ModelMetadataEntry {
2674 costs: Some(ModelCostV2 {
2675 input_per_token: 0.09,
2676 output_per_token: 0.05,
2677 output_reasoning_per_token: 0.0,
2678 input_cached_per_token: 0.0,
2679 input_cache_write_per_token: 0.0,
2680 }),
2681 context_size: None,
2682 },
2683 ),
2684 ]),
2685 }),
2686 ..NormalizationConfig::default()
2687 },
2688 );
2689
2690 let [span1, span2, span3] = collect_span_data(event);
2691
2692 assert_annotated_snapshot!(span1, @r#"
2693 {
2694 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2695 "gen_ai.cost.cache_read.input_tokens": 20.0,
2696 "gen_ai.cost.input_tokens": 25.0,
2697 "gen_ai.cost.output_tokens": 50.0,
2698 "gen_ai.cost.reasoning.output_tokens": 30.0,
2699 "gen_ai.cost.total_tokens": 75.0,
2700 "gen_ai.operation.type": "ai_client",
2701 "gen_ai.request.model": "claude-2.1",
2702 "gen_ai.response.model": "claude-2.1",
2703 "gen_ai.response.tokens_per_second": 2000.0,
2704 "gen_ai.usage.cache_read.input_tokens": 500,
2705 "gen_ai.usage.input_tokens": 1000,
2706 "gen_ai.usage.output_tokens": 2000,
2707 "gen_ai.usage.reasoning.output_tokens": 1000,
2708 "gen_ai.usage.total_tokens": 3000.0
2709 }
2710 "#);
2711 assert_annotated_snapshot!(span2, @r#"
2712 {
2713 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2714 "gen_ai.cost.cache_read.input_tokens": 0.0,
2715 "gen_ai.cost.input_tokens": 90.0,
2716 "gen_ai.cost.output_tokens": 100.0,
2717 "gen_ai.cost.reasoning.output_tokens": 0.0,
2718 "gen_ai.cost.total_tokens": 190.0,
2719 "gen_ai.operation.type": "ai_client",
2720 "gen_ai.request.model": "gpt4-21-04",
2721 "gen_ai.response.model": "gpt4-21-04",
2722 "gen_ai.response.tokens_per_second": 2000.0,
2723 "gen_ai.usage.input_tokens": 1000,
2724 "gen_ai.usage.output_tokens": 2000,
2725 "gen_ai.usage.total_tokens": 3000.0
2726 }
2727 "#);
2728 assert_annotated_snapshot!(span3, @r#"
2729 {
2730 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2731 "gen_ai.cost.cache_read.input_tokens": 0.0,
2732 "gen_ai.cost.input_tokens": 90.0,
2733 "gen_ai.cost.output_tokens": 100.0,
2734 "gen_ai.cost.reasoning.output_tokens": 0.0,
2735 "gen_ai.cost.total_tokens": 190.0,
2736 "gen_ai.operation.type": "ai_client",
2737 "gen_ai.response.model": "gpt4-21-04",
2738 "gen_ai.response.tokens_per_second": 2000.0,
2739 "gen_ai.usage.input_tokens": 1000,
2740 "gen_ai.usage.output_tokens": 2000,
2741 "gen_ai.usage.total_tokens": 3000.0
2742 }
2743 "#);
2744 }
2745
2746 #[test]
2747 fn test_ai_data_with_no_tokens() {
2748 let json = r#"
2749 {
2750 "spans": [
2751 {
2752 "timestamp": 1702474613.0495,
2753 "start_timestamp": 1702474613.0175,
2754 "description": "OpenAI ",
2755 "op": "gen_ai.invoke_agent",
2756 "span_id": "9c01bd820a083e63",
2757 "parent_span_id": "a1e13f3f06239d69",
2758 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2759 "data": {
2760 "gen_ai.request.model": "claude-2.1"
2761 }
2762 }
2763 ]
2764 }
2765 "#;
2766
2767 let mut event = Annotated::<Event>::from_json(json).unwrap();
2768
2769 normalize_event(
2770 &mut event,
2771 &NormalizationConfig {
2772 ai_model_metadata: Some(&ModelMetadata {
2773 version: 1,
2774 models: HashMap::from([(
2775 Pattern::new("claude-2.1").unwrap(),
2776 ModelMetadataEntry {
2777 costs: Some(ModelCostV2 {
2778 input_per_token: 0.01,
2779 output_per_token: 0.02,
2780 output_reasoning_per_token: 0.03,
2781 input_cached_per_token: 0.0,
2782 input_cache_write_per_token: 0.0,
2783 }),
2784 context_size: None,
2785 },
2786 )]),
2787 }),
2788 ..NormalizationConfig::default()
2789 },
2790 );
2791
2792 let [span] = collect_span_data(event);
2793
2794 assert_annotated_snapshot!(span, @r#"
2795 {
2796 "gen_ai.operation.type": "agent",
2797 "gen_ai.request.model": "claude-2.1",
2798 "gen_ai.response.model": "claude-2.1"
2799 }
2800 "#);
2801 }
2802
2803 #[test]
2804 fn test_ai_data_with_ai_op_prefix() {
2805 let json = r#"
2806 {
2807 "spans": [
2808 {
2809 "timestamp": 1702474613.0495,
2810 "start_timestamp": 1702474613.0175,
2811 "description": "OpenAI ",
2812 "op": "ai.chat_completions.openai",
2813 "span_id": "9c01bd820a083e63",
2814 "parent_span_id": "a1e13f3f06239d69",
2815 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2816 "data": {
2817 "gen_ai.usage.input_tokens": 1000,
2818 "gen_ai.usage.output_tokens": 2000,
2819 "gen_ai.usage.reasoning.output_tokens": 1000,
2820 "gen_ai.usage.cache_read.input_tokens": 500,
2821 "gen_ai.request.model": "claude-2.1"
2822 }
2823 },
2824 {
2825 "timestamp": 1702474613.0495,
2826 "start_timestamp": 1702474613.0175,
2827 "description": "OpenAI ",
2828 "op": "ai.chat_completions.openai",
2829 "span_id": "ac01bd820a083e63",
2830 "parent_span_id": "a1e13f3f06239d69",
2831 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2832 "data": {
2833 "gen_ai.usage.input_tokens": 1000,
2834 "gen_ai.usage.output_tokens": 2000,
2835 "gen_ai.request.model": "gpt4-21-04"
2836 }
2837 }
2838 ]
2839 }
2840 "#;
2841
2842 let mut event = Annotated::<Event>::from_json(json).unwrap();
2843
2844 normalize_event(
2845 &mut event,
2846 &NormalizationConfig {
2847 ai_model_metadata: Some(&ModelMetadata {
2848 version: 1,
2849 models: HashMap::from([
2850 (
2851 Pattern::new("claude-2.1").unwrap(),
2852 ModelMetadataEntry {
2853 costs: Some(ModelCostV2 {
2854 input_per_token: 0.01,
2855 output_per_token: 0.02,
2856 output_reasoning_per_token: 0.0,
2857 input_cached_per_token: 0.04,
2858 input_cache_write_per_token: 0.0,
2859 }),
2860 context_size: None,
2861 },
2862 ),
2863 (
2864 Pattern::new("gpt4-21-04").unwrap(),
2865 ModelMetadataEntry {
2866 costs: Some(ModelCostV2 {
2867 input_per_token: 0.09,
2868 output_per_token: 0.05,
2869 output_reasoning_per_token: 0.06,
2870 input_cached_per_token: 0.0,
2871 input_cache_write_per_token: 0.0,
2872 }),
2873 context_size: None,
2874 },
2875 ),
2876 ]),
2877 }),
2878 ..NormalizationConfig::default()
2879 },
2880 );
2881
2882 let [span1, span2] = collect_span_data(event);
2883
2884 assert_annotated_snapshot!(span1, @r#"
2885 {
2886 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2887 "gen_ai.cost.cache_read.input_tokens": 20.0,
2888 "gen_ai.cost.input_tokens": 25.0,
2889 "gen_ai.cost.output_tokens": 40.0,
2890 "gen_ai.cost.reasoning.output_tokens": 20.0,
2891 "gen_ai.cost.total_tokens": 65.0,
2892 "gen_ai.operation.type": "ai_client",
2893 "gen_ai.request.model": "claude-2.1",
2894 "gen_ai.response.model": "claude-2.1",
2895 "gen_ai.response.tokens_per_second": 62500.0,
2896 "gen_ai.usage.cache_read.input_tokens": 500,
2897 "gen_ai.usage.input_tokens": 1000,
2898 "gen_ai.usage.output_tokens": 2000,
2899 "gen_ai.usage.reasoning.output_tokens": 1000,
2900 "gen_ai.usage.total_tokens": 3000.0
2901 }
2902 "#);
2903 assert_annotated_snapshot!(span2, @r#"
2904 {
2905 "gen_ai.cost.cache_creation.input_tokens": 0.0,
2906 "gen_ai.cost.cache_read.input_tokens": 0.0,
2907 "gen_ai.cost.input_tokens": 90.0,
2908 "gen_ai.cost.output_tokens": 100.0,
2909 "gen_ai.cost.reasoning.output_tokens": 0.0,
2910 "gen_ai.cost.total_tokens": 190.0,
2911 "gen_ai.operation.type": "ai_client",
2912 "gen_ai.request.model": "gpt4-21-04",
2913 "gen_ai.response.model": "gpt4-21-04",
2914 "gen_ai.response.tokens_per_second": 62500.0,
2915 "gen_ai.usage.input_tokens": 1000,
2916 "gen_ai.usage.output_tokens": 2000,
2917 "gen_ai.usage.total_tokens": 3000.0
2918 }
2919 "#);
2920 }
2921
2922 #[test]
2923 fn test_ai_response_tokens_per_second_no_output_tokens() {
2924 let json = r#"
2925 {
2926 "spans": [
2927 {
2928 "timestamp": 1702474614.0175,
2929 "start_timestamp": 1702474613.0175,
2930 "op": "gen_ai.chat_completions",
2931 "span_id": "9c01bd820a083e63",
2932 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2933 "data": {
2934 "gen_ai.usage.input_tokens": 500
2935 }
2936 }
2937 ]
2938 }
2939 "#;
2940
2941 let mut event = Annotated::<Event>::from_json(json).unwrap();
2942
2943 normalize_event(
2944 &mut event,
2945 &NormalizationConfig {
2946 ai_model_metadata: Some(&ModelMetadata {
2947 version: 1,
2948 models: HashMap::new(),
2949 }),
2950 ..NormalizationConfig::default()
2951 },
2952 );
2953
2954 let [span] = collect_span_data(event);
2955
2956 assert_annotated_snapshot!(span, @r#"
2958 {
2959 "gen_ai.operation.type": "ai_client",
2960 "gen_ai.usage.input_tokens": 500,
2961 "gen_ai.usage.total_tokens": 500.0
2962 }
2963 "#);
2964 }
2965
2966 #[test]
2967 fn test_ai_response_tokens_per_second_zero_duration() {
2968 let json = r#"
2969 {
2970 "spans": [
2971 {
2972 "timestamp": 1702474613.0175,
2973 "start_timestamp": 1702474613.0175,
2974 "op": "gen_ai.chat_completions",
2975 "span_id": "9c01bd820a083e63",
2976 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2977 "data": {
2978 "gen_ai.usage.output_tokens": 1000
2979 }
2980 }
2981 ]
2982 }
2983 "#;
2984
2985 let mut event = Annotated::<Event>::from_json(json).unwrap();
2986
2987 normalize_event(
2988 &mut event,
2989 &NormalizationConfig {
2990 ai_model_metadata: Some(&ModelMetadata {
2991 version: 1,
2992 models: HashMap::new(),
2993 }),
2994 ..NormalizationConfig::default()
2995 },
2996 );
2997
2998 let [span] = collect_span_data(event);
2999
3000 assert_annotated_snapshot!(span, @r#"
3002 {
3003 "gen_ai.operation.type": "ai_client",
3004 "gen_ai.usage.output_tokens": 1000,
3005 "gen_ai.usage.total_tokens": 1000.0
3006 }
3007 "#);
3008 }
3009
3010 #[test]
3011 fn test_ai_operation_type_mapping() {
3012 let json = r#"
3013 {
3014 "type": "transaction",
3015 "transaction": "test-transaction",
3016 "spans": [
3017 {
3018 "op": "gen_ai.chat",
3019 "description": "AI chat completion",
3020 "data": {}
3021 },
3022 {
3023 "op": "gen_ai.handoff",
3024 "description": "AI agent handoff",
3025 "data": {}
3026 },
3027 {
3028 "op": "gen_ai.unknown",
3029 "description": "Unknown AI operation",
3030 "data": {}
3031 }
3032 ]
3033 }
3034 "#;
3035
3036 let mut event = Annotated::<Event>::from_json(json).unwrap();
3037
3038 normalize_event(&mut event, &NormalizationConfig::default());
3039
3040 let [span1, span2, span3] = collect_span_data(event);
3041
3042 assert_annotated_snapshot!(span1, @r#"
3043 {
3044 "gen_ai.operation.type": "ai_client"
3045 }
3046 "#);
3047 assert_annotated_snapshot!(span2, @r#"
3048 {
3049 "gen_ai.operation.type": "handoff"
3050 }
3051 "#);
3052 assert_annotated_snapshot!(span3, @r#"
3053 {
3054 "gen_ai.operation.type": "ai_client"
3055 }
3056 "#);
3057 }
3058
3059 #[test]
3060 fn test_apple_high_device_class() {
3061 let mut event = Event {
3062 contexts: {
3063 let mut contexts = Contexts::new();
3064 contexts.add(DeviceContext {
3065 family: "iPhone".to_owned().into(),
3066 model: "iPhone15,3".to_owned().into(),
3067 ..Default::default()
3068 });
3069 Annotated::new(contexts)
3070 },
3071 ..Default::default()
3072 };
3073 normalize_device_class(&mut event);
3074 assert_debug_snapshot!(event.tags, @r###"
3075 Tags(
3076 PairList(
3077 [
3078 TagEntry(
3079 "device.class",
3080 "3",
3081 ),
3082 ],
3083 ),
3084 )
3085 "###);
3086 }
3087
3088 #[test]
3089 fn test_filter_mobile_outliers() {
3090 let mut measurements =
3091 Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
3092 .unwrap()
3093 .into_value()
3094 .unwrap();
3095 assert_eq!(measurements.len(), 1);
3096 filter_mobile_outliers(&mut measurements);
3097 assert_eq!(measurements.len(), 0);
3098 }
3099
3100 #[test]
3101 fn test_backfill_app_vitals_start_cold() {
3102 let json = r#"{
3103 "type": "transaction",
3104 "timestamp": "2021-04-26T08:00:05+0100",
3105 "start_timestamp": "2021-04-26T08:00:00+0100",
3106 "measurements": {"app_start_cold": {"value": 1234.0, "unit": "millisecond"}}
3107 }"#;
3108 let mut event = Annotated::<Event>::from_json(json)
3109 .unwrap()
3110 .into_value()
3111 .unwrap();
3112 backfill_app_vitals_start(&mut event);
3113 assert_debug_snapshot!(event.measurements, @r#"
3114 Measurements(
3115 {
3116 "app.vitals.start.value": Measurement {
3117 value: 1234.0,
3118 unit: Duration(
3119 MilliSecond,
3120 ),
3121 },
3122 "app_start_cold": Measurement {
3123 value: 1234.0,
3124 unit: Duration(
3125 MilliSecond,
3126 ),
3127 },
3128 },
3129 )
3130 "#);
3131 assert_debug_snapshot!(event.tags, @r#"
3132 Tags(
3133 PairList(
3134 [
3135 TagEntry(
3136 "app.vitals.start.type",
3137 "cold",
3138 ),
3139 ],
3140 ),
3141 )
3142 "#);
3143 }
3144
3145 #[test]
3146 fn test_backfill_app_vitals_start_warm() {
3147 let json = r#"{
3148 "type": "transaction",
3149 "timestamp": "2021-04-26T08:00:05+0100",
3150 "start_timestamp": "2021-04-26T08:00:00+0100",
3151 "measurements": {"app_start_warm": {"value": 567.0, "unit": "millisecond"}}
3152 }"#;
3153 let mut event = Annotated::<Event>::from_json(json)
3154 .unwrap()
3155 .into_value()
3156 .unwrap();
3157 backfill_app_vitals_start(&mut event);
3158 assert_debug_snapshot!(event.measurements, @r#"
3159 Measurements(
3160 {
3161 "app.vitals.start.value": Measurement {
3162 value: 567.0,
3163 unit: Duration(
3164 MilliSecond,
3165 ),
3166 },
3167 "app_start_warm": Measurement {
3168 value: 567.0,
3169 unit: Duration(
3170 MilliSecond,
3171 ),
3172 },
3173 },
3174 )
3175 "#);
3176 assert_debug_snapshot!(event.tags, @r#"
3177 Tags(
3178 PairList(
3179 [
3180 TagEntry(
3181 "app.vitals.start.type",
3182 "warm",
3183 ),
3184 ],
3185 ),
3186 )
3187 "#);
3188 }
3189
3190 #[test]
3191 fn test_backfill_app_vitals_start_cold_preferred_over_warm() {
3192 let json = r#"{
3193 "type": "transaction",
3194 "timestamp": "2021-04-26T08:00:05+0100",
3195 "start_timestamp": "2021-04-26T08:00:00+0100",
3196 "measurements": {
3197 "app_start_cold": {"value": 100.0, "unit": "millisecond"},
3198 "app_start_warm": {"value": 200.0, "unit": "millisecond"}
3199 }
3200 }"#;
3201 let mut event = Annotated::<Event>::from_json(json)
3202 .unwrap()
3203 .into_value()
3204 .unwrap();
3205 backfill_app_vitals_start(&mut event);
3206 assert_debug_snapshot!(event.measurements, @r#"
3207 Measurements(
3208 {
3209 "app.vitals.start.value": Measurement {
3210 value: 100.0,
3211 unit: Duration(
3212 MilliSecond,
3213 ),
3214 },
3215 "app_start_cold": Measurement {
3216 value: 100.0,
3217 unit: Duration(
3218 MilliSecond,
3219 ),
3220 },
3221 "app_start_warm": Measurement {
3222 value: 200.0,
3223 unit: Duration(
3224 MilliSecond,
3225 ),
3226 },
3227 },
3228 )
3229 "#);
3230 assert_debug_snapshot!(event.tags, @r#"
3231 Tags(
3232 PairList(
3233 [
3234 TagEntry(
3235 "app.vitals.start.type",
3236 "cold",
3237 ),
3238 ],
3239 ),
3240 )
3241 "#);
3242 }
3243
3244 #[test]
3245 fn test_backfill_app_vitals_start_no_app_start_noop() {
3246 let json = r#"{
3247 "type": "transaction",
3248 "timestamp": "2021-04-26T08:00:05+0100",
3249 "start_timestamp": "2021-04-26T08:00:00+0100",
3250 "measurements": {"lcp": {"value": 100.0}}
3251 }"#;
3252 let mut event = Annotated::<Event>::from_json(json)
3253 .unwrap()
3254 .into_value()
3255 .unwrap();
3256 backfill_app_vitals_start(&mut event);
3257 assert_debug_snapshot!(event.measurements, @r#"
3258 Measurements(
3259 {
3260 "lcp": Measurement {
3261 value: 100.0,
3262 unit: ~,
3263 },
3264 },
3265 )
3266 "#);
3267 assert_debug_snapshot!(event.tags, @"~");
3268 }
3269
3270 #[test]
3271 fn test_backfill_app_vitals_start_respects_outlier_filter() {
3272 let json = r#"{
3273 "type": "transaction",
3274 "timestamp": "2021-04-26T08:00:05+0100",
3275 "start_timestamp": "2021-04-26T08:00:00+0100",
3276 "measurements": {"app_start_cold": {"value": 180001.0, "unit": "millisecond"}}
3277 }"#;
3278 let mut event = Annotated::<Event>::from_json(json)
3279 .unwrap()
3280 .into_value()
3281 .unwrap();
3282 normalize_event_measurements(&mut event, None, None);
3283 backfill_app_vitals_start(&mut event);
3284 assert_debug_snapshot!(event.measurements, @"
3285 Measurements(
3286 {},
3287 )
3288 ");
3289 assert_debug_snapshot!(event.tags, @"~");
3290 }
3291
3292 #[test]
3293 fn test_backfill_app_vitals_start_non_transaction_payload_noop() {
3294 let json = r#"{
3295 "type": "error",
3296 "measurements": {
3297 "app_start_cold": {"value": 1234.0, "unit": "millisecond"}
3298 }
3299 }"#;
3300 let mut event = Annotated::<Event>::from_json(json)
3301 .unwrap()
3302 .into_value()
3303 .unwrap();
3304 backfill_app_vitals_start(&mut event);
3305 assert_debug_snapshot!(event.measurements, @r#"
3306 Measurements(
3307 {
3308 "app_start_cold": Measurement {
3309 value: 1234.0,
3310 unit: Duration(
3311 MilliSecond,
3312 ),
3313 },
3314 },
3315 )
3316 "#);
3317 assert_debug_snapshot!(event.tags, @"~");
3318 }
3319
3320 #[test]
3321 fn test_backfill_app_vitals_start_does_not_overwrite_value() {
3322 let json = r#"{
3323 "type": "transaction",
3324 "timestamp": "2021-04-26T08:00:05+0100",
3325 "start_timestamp": "2021-04-26T08:00:00+0100",
3326 "measurements": {
3327 "app_start_cold": {"value": 100.0, "unit": "millisecond"},
3328 "app.vitals.start.value": {"value": 999.0, "unit": "millisecond"}
3329 }
3330 }"#;
3331 let mut event = Annotated::<Event>::from_json(json)
3332 .unwrap()
3333 .into_value()
3334 .unwrap();
3335 backfill_app_vitals_start(&mut event);
3336 assert_debug_snapshot!(event.measurements, @r#"
3337 Measurements(
3338 {
3339 "app.vitals.start.value": Measurement {
3340 value: 999.0,
3341 unit: Duration(
3342 MilliSecond,
3343 ),
3344 },
3345 "app_start_cold": Measurement {
3346 value: 100.0,
3347 unit: Duration(
3348 MilliSecond,
3349 ),
3350 },
3351 },
3352 )
3353 "#);
3354 assert_debug_snapshot!(event.tags, @"~");
3355 }
3356
3357 #[test]
3358 fn test_backfill_app_vitals_start_does_not_overwrite_type() {
3359 let json = r#"{
3360 "type": "transaction",
3361 "timestamp": "2021-04-26T08:00:05+0100",
3362 "start_timestamp": "2021-04-26T08:00:00+0100",
3363 "measurements": {"app_start_cold": {"value": 100.0, "unit": "millisecond"}}
3364 }"#;
3365 let mut event = Annotated::<Event>::from_json(json)
3366 .unwrap()
3367 .into_value()
3368 .unwrap();
3369 event
3370 .tags
3371 .value_mut()
3372 .get_or_insert_with(Tags::default)
3373 .0
3374 .insert(
3375 String::from(APP__VITALS__START__TYPE),
3376 Annotated::new("warm".to_owned()),
3377 );
3378
3379 backfill_app_vitals_start(&mut event);
3380
3381 assert_debug_snapshot!(event.measurements, @r#"
3382 Measurements(
3383 {
3384 "app_start_cold": Measurement {
3385 value: 100.0,
3386 unit: Duration(
3387 MilliSecond,
3388 ),
3389 },
3390 },
3391 )
3392 "#);
3393 assert_debug_snapshot!(event.tags, @r#"
3394 Tags(
3395 PairList(
3396 [
3397 TagEntry(
3398 "app.vitals.start.type",
3399 "warm",
3400 ),
3401 ],
3402 ),
3403 )
3404 "#);
3405 }
3406
3407 #[test]
3408 fn test_backfill_app_vitals_start_invalid_unit_noop() {
3409 let json = r#"{
3410 "type": "transaction",
3411 "timestamp": "2021-04-26T08:00:05+0100",
3412 "start_timestamp": "2021-04-26T08:00:00+0100",
3413 "measurements": {"app_start_cold": {"value": 1.5, "unit": "second"}}
3414 }"#;
3415 let mut event = Annotated::<Event>::from_json(json)
3416 .unwrap()
3417 .into_value()
3418 .unwrap();
3419 backfill_app_vitals_start(&mut event);
3420 assert_debug_snapshot!(event.measurements, @r#"
3421 Measurements(
3422 {
3423 "app_start_cold": Measurement {
3424 value: 1.5,
3425 unit: Duration(
3426 Second,
3427 ),
3428 },
3429 },
3430 )
3431 "#);
3432 assert_debug_snapshot!(event.tags, @"~");
3433 }
3434
3435 #[test]
3436 fn test_backfill_app_vitals_start_screen_from_legacy_measurement() {
3437 let json = r#"{
3438 "type": "transaction",
3439 "transaction": "MainActivity",
3440 "contexts": {
3441 "trace": {
3442 "op": "ui.load"
3443 }
3444 },
3445 "measurements": {
3446 "app_start_cold": {
3447 "value": 1234.0,
3448 "unit": "millisecond"
3449 }
3450 }
3451 }"#;
3452 let mut event = Annotated::<Event>::from_json(json)
3453 .unwrap()
3454 .into_value()
3455 .unwrap();
3456
3457 backfill_app_vitals_start(&mut event);
3458
3459 assert_annotated_snapshot!(trace_context_data(&event), @r#"
3460 {
3461 "app.vitals.start.screen": "MainActivity"
3462 }
3463 "#);
3464 }
3465
3466 #[test]
3467 fn test_backfill_app_vitals_start_screen_from_dotted_measurement() {
3468 let mut event = app_vitals_start_screen_event(
3469 "transaction",
3470 Some("SettingsActivity"),
3471 "ui.load",
3472 Some(APP__VITALS__START__WARM__VALUE),
3473 None,
3474 );
3475
3476 backfill_app_vitals_start(&mut event);
3477
3478 assert_annotated_snapshot!(trace_context_data(&event), @r#"
3479 {
3480 "app.vitals.start.screen": "SettingsActivity"
3481 }
3482 "#);
3483 }
3484
3485 #[test]
3486 fn test_backfill_app_vitals_start_screen_from_start_value_measurement() {
3487 let mut event = app_vitals_start_screen_event(
3488 "transaction",
3489 Some("ProfileActivity"),
3490 "ui.load",
3491 Some(APP__VITALS__START__VALUE),
3492 None,
3493 );
3494
3495 backfill_app_vitals_start(&mut event);
3496
3497 assert_annotated_snapshot!(trace_context_data(&event), @r#"
3498 {
3499 "app.vitals.start.screen": "ProfileActivity"
3500 }
3501 "#);
3502 }
3503
3504 #[test]
3505 fn test_backfill_app_vitals_start_screen_requires_ui_load() {
3506 let mut event = app_vitals_start_screen_event(
3507 "transaction",
3508 Some("MainActivity"),
3509 "navigation",
3510 Some("app_start_cold"),
3511 None,
3512 );
3513
3514 backfill_app_vitals_start(&mut event);
3515
3516 assert_annotated_snapshot!(trace_context_data(&event), @"{}");
3517 }
3518
3519 #[test]
3520 fn test_backfill_app_vitals_start_screen_requires_app_start_measurement() {
3521 let mut event = app_vitals_start_screen_event(
3522 "transaction",
3523 Some("MainActivity"),
3524 "ui.load",
3525 None,
3526 None,
3527 );
3528
3529 backfill_app_vitals_start(&mut event);
3530
3531 assert_annotated_snapshot!(trace_context_data(&event), @"{}");
3532 }
3533
3534 #[test]
3535 fn test_backfill_app_vitals_start_screen_only_requires_measurement_key() {
3536 let json = r#"{
3537 "type": "transaction",
3538 "transaction": "MainActivity",
3539 "contexts": {
3540 "trace": {
3541 "op": "ui.load"
3542 }
3543 },
3544 "measurements": {
3545 "app_start_cold": {
3546 "unit": "millisecond"
3547 }
3548 }
3549 }"#;
3550 let mut event = Annotated::<Event>::from_json(json)
3551 .unwrap()
3552 .into_value()
3553 .unwrap();
3554
3555 backfill_app_vitals_start(&mut event);
3556
3557 assert_annotated_snapshot!(trace_context_data(&event), @r#"
3558 {
3559 "app.vitals.start.screen": "MainActivity"
3560 }
3561 "#);
3562 }
3563
3564 #[test]
3565 fn test_backfill_app_vitals_start_screen_preserves_existing_value() {
3566 let mut event = app_vitals_start_screen_event(
3567 "transaction",
3568 Some("MainActivity"),
3569 "ui.load",
3570 Some("app_start_cold"),
3571 Some("SDKScreen"),
3572 );
3573
3574 backfill_app_vitals_start(&mut event);
3575
3576 assert_annotated_snapshot!(trace_context_data(&event), @r#"
3577 {
3578 "app.vitals.start.screen": "SDKScreen"
3579 }
3580 "#);
3581 }
3582
3583 #[test]
3584 fn test_backfill_app_vitals_start_screen_requires_transaction_name() {
3585 let mut event = app_vitals_start_screen_event(
3586 "transaction",
3587 Some("<unlabeled transaction>"),
3588 "ui.load",
3589 Some("app_start_cold"),
3590 None,
3591 );
3592
3593 backfill_app_vitals_start(&mut event);
3594
3595 assert_annotated_snapshot!(trace_context_data(&event), @"{}");
3596 }
3597
3598 #[test]
3599 fn test_computed_performance_score_transaction() {
3600 let json = r#"
3601 {
3602 "type": "transaction",
3603 "timestamp": "2021-04-26T08:00:05+0100",
3604 "start_timestamp": "2021-04-26T08:00:00+0100",
3605 "measurements": {
3606 "fid": {"value": 213, "unit": "millisecond"},
3607 "fcp": {"value": 1237, "unit": "millisecond"},
3608 "lcp": {"value": 6596, "unit": "millisecond"},
3609 "cls": {"value": 0.11}
3610 },
3611 "contexts": {
3612 "browser": {
3613 "name": "Chrome",
3614 "version": "120.1.1",
3615 "type": "browser"
3616 }
3617 }
3618 }
3619 "#;
3620
3621 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3622
3623 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3624 "profiles": [
3625 {
3626 "name": "Desktop",
3627 "scoreComponents": [
3628 {
3629 "measurement": "fcp",
3630 "weight": 0.15,
3631 "p10": 900,
3632 "p50": 1600
3633 },
3634 {
3635 "measurement": "lcp",
3636 "weight": 0.30,
3637 "p10": 1200,
3638 "p50": 2400
3639 },
3640 {
3641 "measurement": "fid",
3642 "weight": 0.30,
3643 "p10": 100,
3644 "p50": 300
3645 },
3646 {
3647 "measurement": "cls",
3648 "weight": 0.25,
3649 "p10": 0.1,
3650 "p50": 0.25
3651 },
3652 {
3653 "measurement": "ttfb",
3654 "weight": 0.0,
3655 "p10": 0.2,
3656 "p50": 0.4
3657 },
3658 ],
3659 "condition": {
3660 "op":"eq",
3661 "name": "event.contexts.browser.name",
3662 "value": "Chrome"
3663 }
3664 }
3665 ]
3666 }))
3667 .unwrap();
3668
3669 normalize_performance_score(&mut event, Some(&performance_score));
3670
3671 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3672 {
3673 "type": "transaction",
3674 "timestamp": 1619420405.0,
3675 "start_timestamp": 1619420400.0,
3676 "contexts": {
3677 "browser": {
3678 "name": "Chrome",
3679 "version": "120.1.1",
3680 "type": "browser",
3681 },
3682 },
3683 "measurements": {
3684 "cls": {
3685 "value": 0.11,
3686 },
3687 "fcp": {
3688 "value": 1237.0,
3689 "unit": "millisecond",
3690 },
3691 "fid": {
3692 "value": 213.0,
3693 "unit": "millisecond",
3694 },
3695 "lcp": {
3696 "value": 6596.0,
3697 "unit": "millisecond",
3698 },
3699 "score.cls": {
3700 "value": 0.21864170607444863,
3701 "unit": "ratio",
3702 },
3703 "score.fcp": {
3704 "value": 0.10750855443790831,
3705 "unit": "ratio",
3706 },
3707 "score.fid": {
3708 "value": 0.19657361348282545,
3709 "unit": "ratio",
3710 },
3711 "score.lcp": {
3712 "value": 0.009238896571386584,
3713 "unit": "ratio",
3714 },
3715 "score.ratio.cls": {
3716 "value": 0.8745668242977945,
3717 "unit": "ratio",
3718 },
3719 "score.ratio.fcp": {
3720 "value": 0.7167236962527221,
3721 "unit": "ratio",
3722 },
3723 "score.ratio.fid": {
3724 "value": 0.6552453782760849,
3725 "unit": "ratio",
3726 },
3727 "score.ratio.lcp": {
3728 "value": 0.03079632190462195,
3729 "unit": "ratio",
3730 },
3731 "score.total": {
3732 "value": 0.531962770566569,
3733 "unit": "ratio",
3734 },
3735 "score.weight.cls": {
3736 "value": 0.25,
3737 "unit": "ratio",
3738 },
3739 "score.weight.fcp": {
3740 "value": 0.15,
3741 "unit": "ratio",
3742 },
3743 "score.weight.fid": {
3744 "value": 0.3,
3745 "unit": "ratio",
3746 },
3747 "score.weight.lcp": {
3748 "value": 0.3,
3749 "unit": "ratio",
3750 },
3751 "score.weight.ttfb": {
3752 "value": 0.0,
3753 "unit": "ratio",
3754 },
3755 },
3756 }
3757 "###);
3758 }
3759
3760 #[test]
3767 fn test_computed_performance_score_spanv2() {
3768 let json = r#"
3769 {
3770 "end_timestamp": "2021-04-26T08:00:05+0100",
3771 "start_timestamp": "2021-04-26T08:00:00+0100",
3772 "attributes": {
3773 "browser.name": {"value": "Chrome", "type": "string"},
3774 "browser.version": {"value": "120.1.1", "type": "string"},
3775 "fid": {"value": 213, "type": "double"},
3776 "browser.web_vital.fcp.value": {"value": 1237.0, "type": "double"},
3777 "lcp": {"value": 6596, "type": "double"},
3778 "browser.web_vital.cls.value": {"value": 0.11, "type": "double"}
3779 }
3780 }
3781 "#;
3782
3783 let mut span = Annotated::<SpanV2>::from_json(json).unwrap().0.unwrap();
3784
3785 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3786 "profiles": [
3787 {
3788 "name": "Desktop",
3789 "scoreComponents": [
3790 {
3791 "measurement": "fcp",
3792 "weight": 0.15,
3793 "p10": 900,
3794 "p50": 1600
3795 },
3796 {
3797 "measurement": "lcp",
3798 "weight": 0.30,
3799 "p10": 1200,
3800 "p50": 2400
3801 },
3802 {
3803 "measurement": "fid",
3804 "weight": 0.30,
3805 "p10": 100,
3806 "p50": 300
3807 },
3808 {
3809 "measurement": "cls",
3810 "weight": 0.25,
3811 "p10": 0.1,
3812 "p50": 0.25
3813 },
3814 {
3815 "measurement": "ttfb",
3816 "weight": 0.0,
3817 "p10": 0.2,
3818 "p50": 0.4
3819 },
3820 ],
3821 "condition": {
3822 "op": "or",
3823 "inner": [{
3824 "op":"eq",
3825 "name": "event.context.browser.name",
3826 "value": "Chrome"
3827 }, {
3828 "op":"eq",
3829 "name": "span.attributes.browser.name.value",
3830 "value": "Chrome"
3831 }]
3832 }
3833 }
3834 ]
3835 }))
3836 .unwrap();
3837
3838 eap::normalize_attribute_names(&mut span.attributes);
3839 normalize_performance_score(&mut span, Some(&performance_score));
3840
3841 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(span)), {}, @r###"
3842 {
3843 "start_timestamp": 1619420400.0,
3844 "end_timestamp": 1619420405.0,
3845 "attributes": {
3846 "browser.name": {
3847 "type": "string",
3848 "value": "Chrome",
3849 },
3850 "browser.version": {
3851 "type": "string",
3852 "value": "120.1.1",
3853 },
3854 "browser.web_vital.cls.value": {
3855 "type": "double",
3856 "value": 0.11,
3857 },
3858 "browser.web_vital.fcp.value": {
3859 "type": "double",
3860 "value": 1237.0,
3861 },
3862 "browser.web_vital.lcp.value": {
3863 "type": "double",
3864 "value": 6596,
3865 },
3866 "fid": {
3867 "type": "double",
3868 "value": 213,
3869 },
3870 "lcp": {
3871 "type": "double",
3872 "value": 6596,
3873 },
3874 "score.cls": {
3875 "type": "double",
3876 "value": 0.21864170607444863,
3877 },
3878 "score.fcp": {
3879 "type": "double",
3880 "value": 0.10750855443790831,
3881 },
3882 "score.fid": {
3883 "type": "double",
3884 "value": 0.19657361348282545,
3885 },
3886 "score.lcp": {
3887 "type": "double",
3888 "value": 0.009238896571386584,
3889 },
3890 "score.ratio.cls": {
3891 "type": "double",
3892 "value": 0.8745668242977945,
3893 },
3894 "score.ratio.fcp": {
3895 "type": "double",
3896 "value": 0.7167236962527221,
3897 },
3898 "score.ratio.fid": {
3899 "type": "double",
3900 "value": 0.6552453782760849,
3901 },
3902 "score.ratio.lcp": {
3903 "type": "double",
3904 "value": 0.03079632190462195,
3905 },
3906 "score.total": {
3907 "type": "double",
3908 "value": 0.531962770566569,
3909 },
3910 "score.weight.cls": {
3911 "type": "double",
3912 "value": 0.25,
3913 },
3914 "score.weight.fcp": {
3915 "type": "double",
3916 "value": 0.15,
3917 },
3918 "score.weight.fid": {
3919 "type": "double",
3920 "value": 0.3,
3921 },
3922 "score.weight.lcp": {
3923 "type": "double",
3924 "value": 0.3,
3925 },
3926 "score.weight.ttfb": {
3927 "type": "double",
3928 "value": 0.0,
3929 },
3930 },
3931 }
3932 "###);
3933 }
3934
3935 #[test]
3938 fn test_computed_performance_score_with_under_normalized_weights() {
3939 let json = r#"
3940 {
3941 "type": "transaction",
3942 "timestamp": "2021-04-26T08:00:05+0100",
3943 "start_timestamp": "2021-04-26T08:00:00+0100",
3944 "measurements": {
3945 "fid": {"value": 213, "unit": "millisecond"},
3946 "fcp": {"value": 1237, "unit": "millisecond"},
3947 "lcp": {"value": 6596, "unit": "millisecond"},
3948 "cls": {"value": 0.11}
3949 },
3950 "contexts": {
3951 "browser": {
3952 "name": "Chrome",
3953 "version": "120.1.1",
3954 "type": "browser"
3955 }
3956 }
3957 }
3958 "#;
3959
3960 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3961
3962 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3963 "profiles": [
3964 {
3965 "name": "Desktop",
3966 "scoreComponents": [
3967 {
3968 "measurement": "fcp",
3969 "weight": 0.03,
3970 "p10": 900,
3971 "p50": 1600
3972 },
3973 {
3974 "measurement": "lcp",
3975 "weight": 0.06,
3976 "p10": 1200,
3977 "p50": 2400
3978 },
3979 {
3980 "measurement": "fid",
3981 "weight": 0.06,
3982 "p10": 100,
3983 "p50": 300
3984 },
3985 {
3986 "measurement": "cls",
3987 "weight": 0.05,
3988 "p10": 0.1,
3989 "p50": 0.25
3990 },
3991 {
3992 "measurement": "ttfb",
3993 "weight": 0.0,
3994 "p10": 0.2,
3995 "p50": 0.4
3996 },
3997 ],
3998 "condition": {
3999 "op":"eq",
4000 "name": "event.contexts.browser.name",
4001 "value": "Chrome"
4002 }
4003 }
4004 ]
4005 }))
4006 .unwrap();
4007
4008 normalize_performance_score(&mut event, Some(&performance_score));
4009
4010 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4011 {
4012 "type": "transaction",
4013 "timestamp": 1619420405.0,
4014 "start_timestamp": 1619420400.0,
4015 "contexts": {
4016 "browser": {
4017 "name": "Chrome",
4018 "version": "120.1.1",
4019 "type": "browser",
4020 },
4021 },
4022 "measurements": {
4023 "cls": {
4024 "value": 0.11,
4025 },
4026 "fcp": {
4027 "value": 1237.0,
4028 "unit": "millisecond",
4029 },
4030 "fid": {
4031 "value": 213.0,
4032 "unit": "millisecond",
4033 },
4034 "lcp": {
4035 "value": 6596.0,
4036 "unit": "millisecond",
4037 },
4038 "score.cls": {
4039 "value": 0.21864170607444863,
4040 "unit": "ratio",
4041 },
4042 "score.fcp": {
4043 "value": 0.10750855443790831,
4044 "unit": "ratio",
4045 },
4046 "score.fid": {
4047 "value": 0.19657361348282545,
4048 "unit": "ratio",
4049 },
4050 "score.lcp": {
4051 "value": 0.009238896571386584,
4052 "unit": "ratio",
4053 },
4054 "score.ratio.cls": {
4055 "value": 0.8745668242977945,
4056 "unit": "ratio",
4057 },
4058 "score.ratio.fcp": {
4059 "value": 0.7167236962527221,
4060 "unit": "ratio",
4061 },
4062 "score.ratio.fid": {
4063 "value": 0.6552453782760849,
4064 "unit": "ratio",
4065 },
4066 "score.ratio.lcp": {
4067 "value": 0.03079632190462195,
4068 "unit": "ratio",
4069 },
4070 "score.total": {
4071 "value": 0.531962770566569,
4072 "unit": "ratio",
4073 },
4074 "score.weight.cls": {
4075 "value": 0.25,
4076 "unit": "ratio",
4077 },
4078 "score.weight.fcp": {
4079 "value": 0.15,
4080 "unit": "ratio",
4081 },
4082 "score.weight.fid": {
4083 "value": 0.3,
4084 "unit": "ratio",
4085 },
4086 "score.weight.lcp": {
4087 "value": 0.3,
4088 "unit": "ratio",
4089 },
4090 "score.weight.ttfb": {
4091 "value": 0.0,
4092 "unit": "ratio",
4093 },
4094 },
4095 }
4096 "###);
4097 }
4098
4099 #[test]
4102 fn test_computed_performance_score_with_over_normalized_weights() {
4103 let json = r#"
4104 {
4105 "type": "transaction",
4106 "timestamp": "2021-04-26T08:00:05+0100",
4107 "start_timestamp": "2021-04-26T08:00:00+0100",
4108 "measurements": {
4109 "fid": {"value": 213, "unit": "millisecond"},
4110 "fcp": {"value": 1237, "unit": "millisecond"},
4111 "lcp": {"value": 6596, "unit": "millisecond"},
4112 "cls": {"value": 0.11}
4113 },
4114 "contexts": {
4115 "browser": {
4116 "name": "Chrome",
4117 "version": "120.1.1",
4118 "type": "browser"
4119 }
4120 }
4121 }
4122 "#;
4123
4124 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4125
4126 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4127 "profiles": [
4128 {
4129 "name": "Desktop",
4130 "scoreComponents": [
4131 {
4132 "measurement": "fcp",
4133 "weight": 0.30,
4134 "p10": 900,
4135 "p50": 1600
4136 },
4137 {
4138 "measurement": "lcp",
4139 "weight": 0.60,
4140 "p10": 1200,
4141 "p50": 2400
4142 },
4143 {
4144 "measurement": "fid",
4145 "weight": 0.60,
4146 "p10": 100,
4147 "p50": 300
4148 },
4149 {
4150 "measurement": "cls",
4151 "weight": 0.50,
4152 "p10": 0.1,
4153 "p50": 0.25
4154 },
4155 {
4156 "measurement": "ttfb",
4157 "weight": 0.0,
4158 "p10": 0.2,
4159 "p50": 0.4
4160 },
4161 ],
4162 "condition": {
4163 "op":"eq",
4164 "name": "event.contexts.browser.name",
4165 "value": "Chrome"
4166 }
4167 }
4168 ]
4169 }))
4170 .unwrap();
4171
4172 normalize_performance_score(&mut event, Some(&performance_score));
4173
4174 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4175 {
4176 "type": "transaction",
4177 "timestamp": 1619420405.0,
4178 "start_timestamp": 1619420400.0,
4179 "contexts": {
4180 "browser": {
4181 "name": "Chrome",
4182 "version": "120.1.1",
4183 "type": "browser",
4184 },
4185 },
4186 "measurements": {
4187 "cls": {
4188 "value": 0.11,
4189 },
4190 "fcp": {
4191 "value": 1237.0,
4192 "unit": "millisecond",
4193 },
4194 "fid": {
4195 "value": 213.0,
4196 "unit": "millisecond",
4197 },
4198 "lcp": {
4199 "value": 6596.0,
4200 "unit": "millisecond",
4201 },
4202 "score.cls": {
4203 "value": 0.21864170607444863,
4204 "unit": "ratio",
4205 },
4206 "score.fcp": {
4207 "value": 0.10750855443790831,
4208 "unit": "ratio",
4209 },
4210 "score.fid": {
4211 "value": 0.19657361348282545,
4212 "unit": "ratio",
4213 },
4214 "score.lcp": {
4215 "value": 0.009238896571386584,
4216 "unit": "ratio",
4217 },
4218 "score.ratio.cls": {
4219 "value": 0.8745668242977945,
4220 "unit": "ratio",
4221 },
4222 "score.ratio.fcp": {
4223 "value": 0.7167236962527221,
4224 "unit": "ratio",
4225 },
4226 "score.ratio.fid": {
4227 "value": 0.6552453782760849,
4228 "unit": "ratio",
4229 },
4230 "score.ratio.lcp": {
4231 "value": 0.03079632190462195,
4232 "unit": "ratio",
4233 },
4234 "score.total": {
4235 "value": 0.531962770566569,
4236 "unit": "ratio",
4237 },
4238 "score.weight.cls": {
4239 "value": 0.25,
4240 "unit": "ratio",
4241 },
4242 "score.weight.fcp": {
4243 "value": 0.15,
4244 "unit": "ratio",
4245 },
4246 "score.weight.fid": {
4247 "value": 0.3,
4248 "unit": "ratio",
4249 },
4250 "score.weight.lcp": {
4251 "value": 0.3,
4252 "unit": "ratio",
4253 },
4254 "score.weight.ttfb": {
4255 "value": 0.0,
4256 "unit": "ratio",
4257 },
4258 },
4259 }
4260 "###);
4261 }
4262
4263 #[test]
4264 fn test_computed_performance_score_missing_measurement() {
4265 let json = r#"
4266 {
4267 "type": "transaction",
4268 "timestamp": "2021-04-26T08:00:05+0100",
4269 "start_timestamp": "2021-04-26T08:00:00+0100",
4270 "measurements": {
4271 "a": {"value": 213, "unit": "millisecond"}
4272 },
4273 "contexts": {
4274 "browser": {
4275 "name": "Chrome",
4276 "version": "120.1.1",
4277 "type": "browser"
4278 }
4279 }
4280 }
4281 "#;
4282
4283 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4284
4285 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4286 "profiles": [
4287 {
4288 "name": "Desktop",
4289 "scoreComponents": [
4290 {
4291 "measurement": "a",
4292 "weight": 0.15,
4293 "p10": 900,
4294 "p50": 1600
4295 },
4296 {
4297 "measurement": "b",
4298 "weight": 0.30,
4299 "p10": 1200,
4300 "p50": 2400
4301 },
4302 ],
4303 "condition": {
4304 "op":"eq",
4305 "name": "event.contexts.browser.name",
4306 "value": "Chrome"
4307 }
4308 }
4309 ]
4310 }))
4311 .unwrap();
4312
4313 normalize_performance_score(&mut event, Some(&performance_score));
4314
4315 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4316 {
4317 "type": "transaction",
4318 "timestamp": 1619420405.0,
4319 "start_timestamp": 1619420400.0,
4320 "contexts": {
4321 "browser": {
4322 "name": "Chrome",
4323 "version": "120.1.1",
4324 "type": "browser",
4325 },
4326 },
4327 "measurements": {
4328 "a": {
4329 "value": 213.0,
4330 "unit": "millisecond",
4331 },
4332 },
4333 }
4334 "###);
4335 }
4336
4337 #[test]
4338 fn test_computed_performance_score_optional_measurement() {
4339 let json = r#"
4340 {
4341 "type": "transaction",
4342 "timestamp": "2021-04-26T08:00:05+0100",
4343 "start_timestamp": "2021-04-26T08:00:00+0100",
4344 "measurements": {
4345 "a": {"value": 213, "unit": "millisecond"},
4346 "b": {"value": 213, "unit": "millisecond"}
4347 },
4348 "contexts": {
4349 "browser": {
4350 "name": "Chrome",
4351 "version": "120.1.1",
4352 "type": "browser"
4353 }
4354 }
4355 }
4356 "#;
4357
4358 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4359
4360 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4361 "profiles": [
4362 {
4363 "name": "Desktop",
4364 "scoreComponents": [
4365 {
4366 "measurement": "a",
4367 "weight": 0.15,
4368 "p10": 900,
4369 "p50": 1600,
4370 },
4371 {
4372 "measurement": "b",
4373 "weight": 0.30,
4374 "p10": 1200,
4375 "p50": 2400,
4376 "optional": true
4377 },
4378 {
4379 "measurement": "c",
4380 "weight": 0.55,
4381 "p10": 1200,
4382 "p50": 2400,
4383 "optional": true
4384 },
4385 ],
4386 "condition": {
4387 "op":"eq",
4388 "name": "event.contexts.browser.name",
4389 "value": "Chrome"
4390 }
4391 }
4392 ]
4393 }))
4394 .unwrap();
4395
4396 normalize_performance_score(&mut event, Some(&performance_score));
4397
4398 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4399 {
4400 "type": "transaction",
4401 "timestamp": 1619420405.0,
4402 "start_timestamp": 1619420400.0,
4403 "contexts": {
4404 "browser": {
4405 "name": "Chrome",
4406 "version": "120.1.1",
4407 "type": "browser",
4408 },
4409 },
4410 "measurements": {
4411 "a": {
4412 "value": 213.0,
4413 "unit": "millisecond",
4414 },
4415 "b": {
4416 "value": 213.0,
4417 "unit": "millisecond",
4418 },
4419 "score.a": {
4420 "value": 0.33333215313291975,
4421 "unit": "ratio",
4422 },
4423 "score.b": {
4424 "value": 0.66666415149198,
4425 "unit": "ratio",
4426 },
4427 "score.ratio.a": {
4428 "value": 0.9999964593987591,
4429 "unit": "ratio",
4430 },
4431 "score.ratio.b": {
4432 "value": 0.9999962272379699,
4433 "unit": "ratio",
4434 },
4435 "score.total": {
4436 "value": 0.9999963046248997,
4437 "unit": "ratio",
4438 },
4439 "score.weight.a": {
4440 "value": 0.33333333333333337,
4441 "unit": "ratio",
4442 },
4443 "score.weight.b": {
4444 "value": 0.6666666666666667,
4445 "unit": "ratio",
4446 },
4447 "score.weight.c": {
4448 "value": 0.0,
4449 "unit": "ratio",
4450 },
4451 },
4452 }
4453 "###);
4454 }
4455
4456 #[test]
4457 fn test_computed_performance_score_weight_0() {
4458 let json = r#"
4459 {
4460 "type": "transaction",
4461 "timestamp": "2021-04-26T08:00:05+0100",
4462 "start_timestamp": "2021-04-26T08:00:00+0100",
4463 "measurements": {
4464 "cls": {"value": 0.11}
4465 }
4466 }
4467 "#;
4468
4469 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4470
4471 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4472 "profiles": [
4473 {
4474 "name": "Desktop",
4475 "scoreComponents": [
4476 {
4477 "measurement": "cls",
4478 "weight": 0,
4479 "p10": 0.1,
4480 "p50": 0.25
4481 },
4482 ],
4483 "condition": {
4484 "op":"and",
4485 "inner": []
4486 }
4487 }
4488 ]
4489 }))
4490 .unwrap();
4491
4492 normalize_performance_score(&mut event, Some(&performance_score));
4493
4494 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4495 {
4496 "type": "transaction",
4497 "timestamp": 1619420405.0,
4498 "start_timestamp": 1619420400.0,
4499 "measurements": {
4500 "cls": {
4501 "value": 0.11,
4502 },
4503 },
4504 }
4505 "###);
4506 }
4507
4508 #[test]
4509 fn test_computed_performance_score_negative_value() {
4510 let json = r#"
4511 {
4512 "type": "transaction",
4513 "timestamp": "2021-04-26T08:00:05+0100",
4514 "start_timestamp": "2021-04-26T08:00:00+0100",
4515 "measurements": {
4516 "ttfb": {"value": -100, "unit": "millisecond"}
4517 }
4518 }
4519 "#;
4520
4521 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4522
4523 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4524 "profiles": [
4525 {
4526 "name": "Desktop",
4527 "scoreComponents": [
4528 {
4529 "measurement": "ttfb",
4530 "weight": 1.0,
4531 "p10": 100.0,
4532 "p50": 250.0
4533 },
4534 ],
4535 "condition": {
4536 "op":"and",
4537 "inner": []
4538 }
4539 }
4540 ]
4541 }))
4542 .unwrap();
4543
4544 normalize_performance_score(&mut event, Some(&performance_score));
4545
4546 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4547 {
4548 "type": "transaction",
4549 "timestamp": 1619420405.0,
4550 "start_timestamp": 1619420400.0,
4551 "measurements": {
4552 "score.ratio.ttfb": {
4553 "value": 1.0,
4554 "unit": "ratio",
4555 },
4556 "score.total": {
4557 "value": 1.0,
4558 "unit": "ratio",
4559 },
4560 "score.ttfb": {
4561 "value": 1.0,
4562 "unit": "ratio",
4563 },
4564 "score.weight.ttfb": {
4565 "value": 1.0,
4566 "unit": "ratio",
4567 },
4568 "ttfb": {
4569 "value": -100.0,
4570 "unit": "millisecond",
4571 },
4572 },
4573 }
4574 "###);
4575 }
4576
4577 #[test]
4578 fn test_filter_negative_web_vital_measurements() {
4579 let json = r#"
4580 {
4581 "type": "transaction",
4582 "timestamp": "2021-04-26T08:00:05+0100",
4583 "start_timestamp": "2021-04-26T08:00:00+0100",
4584 "measurements": {
4585 "ttfb": {"value": -100, "unit": "millisecond"}
4586 }
4587 }
4588 "#;
4589 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4590
4591 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
4593 "builtinMeasurements": [
4594 {"name": "ttfb", "unit": "millisecond"},
4595 ],
4596 }))
4597 .unwrap();
4598
4599 let dynamic_measurement_config =
4600 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
4601
4602 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
4603
4604 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4605 {
4606 "type": "transaction",
4607 "timestamp": 1619420405.0,
4608 "start_timestamp": 1619420400.0,
4609 "measurements": {},
4610 "_meta": {
4611 "measurements": {
4612 "": Meta(Some(MetaInner(
4613 err: [
4614 [
4615 "invalid_data",
4616 {
4617 "reason": "Negative value for measurement ttfb not allowed: -100",
4618 },
4619 ],
4620 ],
4621 val: Some({
4622 "ttfb": {
4623 "unit": "millisecond",
4624 "value": -100.0,
4625 },
4626 }),
4627 ))),
4628 },
4629 },
4630 }
4631 "###);
4632 }
4633
4634 #[test]
4635 fn test_computed_performance_score_multiple_profiles() {
4636 let json = r#"
4637 {
4638 "type": "transaction",
4639 "timestamp": "2021-04-26T08:00:05+0100",
4640 "start_timestamp": "2021-04-26T08:00:00+0100",
4641 "measurements": {
4642 "cls": {"value": 0.11},
4643 "inp": {"value": 120.0}
4644 }
4645 }
4646 "#;
4647
4648 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4649
4650 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4651 "profiles": [
4652 {
4653 "name": "Desktop",
4654 "scoreComponents": [
4655 {
4656 "measurement": "cls",
4657 "weight": 0,
4658 "p10": 0.1,
4659 "p50": 0.25
4660 },
4661 ],
4662 "condition": {
4663 "op":"and",
4664 "inner": []
4665 }
4666 },
4667 {
4668 "name": "Desktop",
4669 "scoreComponents": [
4670 {
4671 "measurement": "inp",
4672 "weight": 1.0,
4673 "p10": 0.1,
4674 "p50": 0.25
4675 },
4676 ],
4677 "condition": {
4678 "op":"and",
4679 "inner": []
4680 }
4681 }
4682 ]
4683 }))
4684 .unwrap();
4685
4686 normalize_performance_score(&mut event, Some(&performance_score));
4687
4688 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4689 {
4690 "type": "transaction",
4691 "timestamp": 1619420405.0,
4692 "start_timestamp": 1619420400.0,
4693 "measurements": {
4694 "cls": {
4695 "value": 0.11,
4696 },
4697 "inp": {
4698 "value": 120.0,
4699 },
4700 "score.inp": {
4701 "value": 0.0,
4702 "unit": "ratio",
4703 },
4704 "score.ratio.inp": {
4705 "value": 0.0,
4706 "unit": "ratio",
4707 },
4708 "score.total": {
4709 "value": 0.0,
4710 "unit": "ratio",
4711 },
4712 "score.weight.inp": {
4713 "value": 1.0,
4714 "unit": "ratio",
4715 },
4716 },
4717 }
4718 "###);
4719 }
4720
4721 #[test]
4722 fn test_compute_performance_score_for_mobile_ios_profile() {
4723 let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
4724 .unwrap()
4725 .0
4726 .unwrap();
4727
4728 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4729 "profiles": [
4730 {
4731 "name": "Mobile",
4732 "scoreComponents": [
4733 {
4734 "measurement": "time_to_initial_display",
4735 "weight": 0.25,
4736 "p10": 1800.0,
4737 "p50": 3000.0,
4738 "optional": true
4739 },
4740 {
4741 "measurement": "time_to_full_display",
4742 "weight": 0.25,
4743 "p10": 2500.0,
4744 "p50": 4000.0,
4745 "optional": true
4746 },
4747 {
4748 "measurement": "app_start_warm",
4749 "weight": 0.25,
4750 "p10": 200.0,
4751 "p50": 500.0,
4752 "optional": true
4753 },
4754 {
4755 "measurement": "app_start_cold",
4756 "weight": 0.25,
4757 "p10": 200.0,
4758 "p50": 500.0,
4759 "optional": true
4760 }
4761 ],
4762 "condition": {
4763 "op": "and",
4764 "inner": [
4765 {
4766 "op": "or",
4767 "inner": [
4768 {
4769 "op": "eq",
4770 "name": "event.sdk.name",
4771 "value": "sentry.cocoa"
4772 },
4773 {
4774 "op": "eq",
4775 "name": "event.sdk.name",
4776 "value": "sentry.java.android"
4777 }
4778 ]
4779 },
4780 {
4781 "op": "eq",
4782 "name": "event.contexts.trace.op",
4783 "value": "ui.load"
4784 }
4785 ]
4786 }
4787 }
4788 ]
4789 }))
4790 .unwrap();
4791
4792 normalize_performance_score(&mut event, Some(&performance_score));
4793
4794 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
4795 }
4796
4797 #[test]
4798 fn test_compute_performance_score_for_mobile_android_profile() {
4799 let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
4800 .unwrap()
4801 .0
4802 .unwrap();
4803
4804 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4805 "profiles": [
4806 {
4807 "name": "Mobile",
4808 "scoreComponents": [
4809 {
4810 "measurement": "time_to_initial_display",
4811 "weight": 0.25,
4812 "p10": 1800.0,
4813 "p50": 3000.0,
4814 "optional": true
4815 },
4816 {
4817 "measurement": "time_to_full_display",
4818 "weight": 0.25,
4819 "p10": 2500.0,
4820 "p50": 4000.0,
4821 "optional": true
4822 },
4823 {
4824 "measurement": "app_start_warm",
4825 "weight": 0.25,
4826 "p10": 200.0,
4827 "p50": 500.0,
4828 "optional": true
4829 },
4830 {
4831 "measurement": "app_start_cold",
4832 "weight": 0.25,
4833 "p10": 200.0,
4834 "p50": 500.0,
4835 "optional": true
4836 }
4837 ],
4838 "condition": {
4839 "op": "and",
4840 "inner": [
4841 {
4842 "op": "or",
4843 "inner": [
4844 {
4845 "op": "eq",
4846 "name": "event.sdk.name",
4847 "value": "sentry.cocoa"
4848 },
4849 {
4850 "op": "eq",
4851 "name": "event.sdk.name",
4852 "value": "sentry.java.android"
4853 }
4854 ]
4855 },
4856 {
4857 "op": "eq",
4858 "name": "event.contexts.trace.op",
4859 "value": "ui.load"
4860 }
4861 ]
4862 }
4863 }
4864 ]
4865 }))
4866 .unwrap();
4867
4868 normalize_performance_score(&mut event, Some(&performance_score));
4869
4870 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
4871 }
4872
4873 #[test]
4874 fn test_computes_performance_score_and_tags_with_profile_version() {
4875 let json = r#"
4876 {
4877 "type": "transaction",
4878 "timestamp": "2021-04-26T08:00:05+0100",
4879 "start_timestamp": "2021-04-26T08:00:00+0100",
4880 "measurements": {
4881 "inp": {"value": 120.0}
4882 }
4883 }
4884 "#;
4885
4886 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4887
4888 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4889 "profiles": [
4890 {
4891 "name": "Desktop",
4892 "scoreComponents": [
4893 {
4894 "measurement": "inp",
4895 "weight": 1.0,
4896 "p10": 0.1,
4897 "p50": 0.25
4898 },
4899 ],
4900 "condition": {
4901 "op":"and",
4902 "inner": []
4903 },
4904 "version": "beta"
4905 }
4906 ]
4907 }))
4908 .unwrap();
4909
4910 normalize(
4911 &mut event,
4912 &mut Meta::default(),
4913 &NormalizationConfig {
4914 performance_score: Some(&performance_score),
4915 ..Default::default()
4916 },
4917 );
4918
4919 insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
4920 {
4921 "performance_score": {
4922 "score_profile_version": "beta",
4923 "type": "performancescore",
4924 },
4925 }
4926 "###);
4927 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4928 {
4929 "inp": {
4930 "value": 120.0,
4931 "unit": "millisecond",
4932 },
4933 "score.inp": {
4934 "value": 0.0,
4935 "unit": "ratio",
4936 },
4937 "score.ratio.inp": {
4938 "value": 0.0,
4939 "unit": "ratio",
4940 },
4941 "score.total": {
4942 "value": 0.0,
4943 "unit": "ratio",
4944 },
4945 "score.weight.inp": {
4946 "value": 1.0,
4947 "unit": "ratio",
4948 },
4949 }
4950 "###);
4951 }
4952
4953 #[test]
4954 fn test_normalize_adds_trace_context() {
4955 let json = r#"
4956 {
4957 "type": "error",
4958 "exception": {
4959 "values": [{"type": "ValueError", "value": "Should not happen"}]
4960 }
4961 }
4962 "#;
4963
4964 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4965
4966 normalize(
4967 &mut event,
4968 &mut Meta::default(),
4969 &NormalizationConfig {
4970 force_trace_context: true,
4971 ..Default::default()
4972 },
4973 );
4974
4975 insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {
4976 ".event_id" => "[event-id]",
4977 ".trace.trace_id" => "[trace-id]",
4978 ".trace.span_id" => "[span-id]"
4979 }, @r#"
4980 {
4981 "trace": {
4982 "trace_id": "[trace-id]",
4983 "span_id": "[span-id]",
4984 "status": "unknown",
4985 "type": "trace",
4986 },
4987 }
4988 "#);
4989 }
4990
4991 #[test]
4992 fn test_computes_standalone_cls_performance_score() {
4993 let json = r#"
4994 {
4995 "type": "transaction",
4996 "timestamp": "2021-04-26T08:00:05+0100",
4997 "start_timestamp": "2021-04-26T08:00:00+0100",
4998 "measurements": {
4999 "cls": {"value": 0.5}
5000 }
5001 }
5002 "#;
5003
5004 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5005
5006 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
5007 "profiles": [
5008 {
5009 "name": "Default",
5010 "scoreComponents": [
5011 {
5012 "measurement": "fcp",
5013 "weight": 0.15,
5014 "p10": 900.0,
5015 "p50": 1600.0,
5016 "optional": true,
5017 },
5018 {
5019 "measurement": "lcp",
5020 "weight": 0.30,
5021 "p10": 1200.0,
5022 "p50": 2400.0,
5023 "optional": true,
5024 },
5025 {
5026 "measurement": "cls",
5027 "weight": 0.15,
5028 "p10": 0.1,
5029 "p50": 0.25,
5030 "optional": true,
5031 },
5032 {
5033 "measurement": "ttfb",
5034 "weight": 0.10,
5035 "p10": 200.0,
5036 "p50": 400.0,
5037 "optional": true,
5038 },
5039 ],
5040 "condition": {
5041 "op": "and",
5042 "inner": [],
5043 },
5044 }
5045 ]
5046 }))
5047 .unwrap();
5048
5049 normalize(
5050 &mut event,
5051 &mut Meta::default(),
5052 &NormalizationConfig {
5053 performance_score: Some(&performance_score),
5054 ..Default::default()
5055 },
5056 );
5057
5058 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
5059 {
5060 "cls": {
5061 "value": 0.5,
5062 "unit": "none",
5063 },
5064 "score.cls": {
5065 "value": 0.16615877613713903,
5066 "unit": "ratio",
5067 },
5068 "score.ratio.cls": {
5069 "value": 0.16615877613713903,
5070 "unit": "ratio",
5071 },
5072 "score.total": {
5073 "value": 0.16615877613713903,
5074 "unit": "ratio",
5075 },
5076 "score.weight.cls": {
5077 "value": 1.0,
5078 "unit": "ratio",
5079 },
5080 "score.weight.fcp": {
5081 "value": 0.0,
5082 "unit": "ratio",
5083 },
5084 "score.weight.lcp": {
5085 "value": 0.0,
5086 "unit": "ratio",
5087 },
5088 "score.weight.ttfb": {
5089 "value": 0.0,
5090 "unit": "ratio",
5091 },
5092 }
5093 "###);
5094 }
5095
5096 #[test]
5097 fn test_computes_standalone_lcp_performance_score() {
5098 let json = r#"
5099 {
5100 "type": "transaction",
5101 "timestamp": "2021-04-26T08:00:05+0100",
5102 "start_timestamp": "2021-04-26T08:00:00+0100",
5103 "measurements": {
5104 "lcp": {"value": 1200.0}
5105 }
5106 }
5107 "#;
5108
5109 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5110
5111 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
5112 "profiles": [
5113 {
5114 "name": "Default",
5115 "scoreComponents": [
5116 {
5117 "measurement": "fcp",
5118 "weight": 0.15,
5119 "p10": 900.0,
5120 "p50": 1600.0,
5121 "optional": true,
5122 },
5123 {
5124 "measurement": "lcp",
5125 "weight": 0.30,
5126 "p10": 1200.0,
5127 "p50": 2400.0,
5128 "optional": true,
5129 },
5130 {
5131 "measurement": "cls",
5132 "weight": 0.15,
5133 "p10": 0.1,
5134 "p50": 0.25,
5135 "optional": true,
5136 },
5137 {
5138 "measurement": "ttfb",
5139 "weight": 0.10,
5140 "p10": 200.0,
5141 "p50": 400.0,
5142 "optional": true,
5143 },
5144 ],
5145 "condition": {
5146 "op": "and",
5147 "inner": [],
5148 },
5149 }
5150 ]
5151 }))
5152 .unwrap();
5153
5154 normalize(
5155 &mut event,
5156 &mut Meta::default(),
5157 &NormalizationConfig {
5158 performance_score: Some(&performance_score),
5159 ..Default::default()
5160 },
5161 );
5162
5163 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
5164 {
5165 "lcp": {
5166 "value": 1200.0,
5167 "unit": "millisecond",
5168 },
5169 "score.lcp": {
5170 "value": 0.8999999314038525,
5171 "unit": "ratio",
5172 },
5173 "score.ratio.lcp": {
5174 "value": 0.8999999314038525,
5175 "unit": "ratio",
5176 },
5177 "score.total": {
5178 "value": 0.8999999314038525,
5179 "unit": "ratio",
5180 },
5181 "score.weight.cls": {
5182 "value": 0.0,
5183 "unit": "ratio",
5184 },
5185 "score.weight.fcp": {
5186 "value": 0.0,
5187 "unit": "ratio",
5188 },
5189 "score.weight.lcp": {
5190 "value": 1.0,
5191 "unit": "ratio",
5192 },
5193 "score.weight.ttfb": {
5194 "value": 0.0,
5195 "unit": "ratio",
5196 },
5197 }
5198 "###);
5199 }
5200
5201 #[test]
5202 fn test_computed_performance_score_uses_first_matching_profile() {
5203 let json = r#"
5204 {
5205 "type": "transaction",
5206 "timestamp": "2021-04-26T08:00:05+0100",
5207 "start_timestamp": "2021-04-26T08:00:00+0100",
5208 "measurements": {
5209 "a": {"value": 213, "unit": "millisecond"},
5210 "b": {"value": 213, "unit": "millisecond"}
5211 },
5212 "contexts": {
5213 "browser": {
5214 "name": "Chrome",
5215 "version": "120.1.1",
5216 "type": "browser"
5217 }
5218 }
5219 }
5220 "#;
5221
5222 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5223
5224 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
5225 "profiles": [
5226 {
5227 "name": "Mobile",
5228 "scoreComponents": [
5229 {
5230 "measurement": "a",
5231 "weight": 0.15,
5232 "p10": 100,
5233 "p50": 200,
5234 },
5235 {
5236 "measurement": "b",
5237 "weight": 0.30,
5238 "p10": 100,
5239 "p50": 200,
5240 "optional": true
5241 },
5242 {
5243 "measurement": "c",
5244 "weight": 0.55,
5245 "p10": 100,
5246 "p50": 200,
5247 "optional": true
5248 },
5249 ],
5250 "condition": {
5251 "op":"eq",
5252 "name": "event.contexts.browser.name",
5253 "value": "Chrome Mobile"
5254 }
5255 },
5256 {
5257 "name": "Desktop",
5258 "scoreComponents": [
5259 {
5260 "measurement": "a",
5261 "weight": 0.15,
5262 "p10": 900,
5263 "p50": 1600,
5264 },
5265 {
5266 "measurement": "b",
5267 "weight": 0.30,
5268 "p10": 1200,
5269 "p50": 2400,
5270 "optional": true
5271 },
5272 {
5273 "measurement": "c",
5274 "weight": 0.55,
5275 "p10": 1200,
5276 "p50": 2400,
5277 "optional": true
5278 },
5279 ],
5280 "condition": {
5281 "op":"eq",
5282 "name": "event.contexts.browser.name",
5283 "value": "Chrome"
5284 }
5285 },
5286 {
5287 "name": "Default",
5288 "scoreComponents": [
5289 {
5290 "measurement": "a",
5291 "weight": 0.15,
5292 "p10": 100,
5293 "p50": 200,
5294 },
5295 {
5296 "measurement": "b",
5297 "weight": 0.30,
5298 "p10": 100,
5299 "p50": 200,
5300 "optional": true
5301 },
5302 {
5303 "measurement": "c",
5304 "weight": 0.55,
5305 "p10": 100,
5306 "p50": 200,
5307 "optional": true
5308 },
5309 ],
5310 "condition": {
5311 "op": "and",
5312 "inner": [],
5313 }
5314 }
5315 ]
5316 }))
5317 .unwrap();
5318
5319 normalize_performance_score(&mut event, Some(&performance_score));
5320
5321 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
5322 {
5323 "type": "transaction",
5324 "timestamp": 1619420405.0,
5325 "start_timestamp": 1619420400.0,
5326 "contexts": {
5327 "browser": {
5328 "name": "Chrome",
5329 "version": "120.1.1",
5330 "type": "browser",
5331 },
5332 },
5333 "measurements": {
5334 "a": {
5335 "value": 213.0,
5336 "unit": "millisecond",
5337 },
5338 "b": {
5339 "value": 213.0,
5340 "unit": "millisecond",
5341 },
5342 "score.a": {
5343 "value": 0.33333215313291975,
5344 "unit": "ratio",
5345 },
5346 "score.b": {
5347 "value": 0.66666415149198,
5348 "unit": "ratio",
5349 },
5350 "score.ratio.a": {
5351 "value": 0.9999964593987591,
5352 "unit": "ratio",
5353 },
5354 "score.ratio.b": {
5355 "value": 0.9999962272379699,
5356 "unit": "ratio",
5357 },
5358 "score.total": {
5359 "value": 0.9999963046248997,
5360 "unit": "ratio",
5361 },
5362 "score.weight.a": {
5363 "value": 0.33333333333333337,
5364 "unit": "ratio",
5365 },
5366 "score.weight.b": {
5367 "value": 0.6666666666666667,
5368 "unit": "ratio",
5369 },
5370 "score.weight.c": {
5371 "value": 0.0,
5372 "unit": "ratio",
5373 },
5374 },
5375 }
5376 "###);
5377 }
5378
5379 #[test]
5380 fn test_computed_performance_score_falls_back_to_default_profile() {
5381 let json = r#"
5382 {
5383 "type": "transaction",
5384 "timestamp": "2021-04-26T08:00:05+0100",
5385 "start_timestamp": "2021-04-26T08:00:00+0100",
5386 "measurements": {
5387 "a": {"value": 213, "unit": "millisecond"},
5388 "b": {"value": 213, "unit": "millisecond"}
5389 },
5390 "contexts": {}
5391 }
5392 "#;
5393
5394 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5395
5396 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
5397 "profiles": [
5398 {
5399 "name": "Mobile",
5400 "scoreComponents": [
5401 {
5402 "measurement": "a",
5403 "weight": 0.15,
5404 "p10": 900,
5405 "p50": 1600,
5406 "optional": true
5407 },
5408 {
5409 "measurement": "b",
5410 "weight": 0.30,
5411 "p10": 1200,
5412 "p50": 2400,
5413 "optional": true
5414 },
5415 {
5416 "measurement": "c",
5417 "weight": 0.55,
5418 "p10": 1200,
5419 "p50": 2400,
5420 "optional": true
5421 },
5422 ],
5423 "condition": {
5424 "op":"eq",
5425 "name": "event.contexts.browser.name",
5426 "value": "Chrome Mobile"
5427 }
5428 },
5429 {
5430 "name": "Desktop",
5431 "scoreComponents": [
5432 {
5433 "measurement": "a",
5434 "weight": 0.15,
5435 "p10": 900,
5436 "p50": 1600,
5437 "optional": true
5438 },
5439 {
5440 "measurement": "b",
5441 "weight": 0.30,
5442 "p10": 1200,
5443 "p50": 2400,
5444 "optional": true
5445 },
5446 {
5447 "measurement": "c",
5448 "weight": 0.55,
5449 "p10": 1200,
5450 "p50": 2400,
5451 "optional": true
5452 },
5453 ],
5454 "condition": {
5455 "op":"eq",
5456 "name": "event.contexts.browser.name",
5457 "value": "Chrome"
5458 }
5459 },
5460 {
5461 "name": "Default",
5462 "scoreComponents": [
5463 {
5464 "measurement": "a",
5465 "weight": 0.15,
5466 "p10": 100,
5467 "p50": 200,
5468 "optional": true
5469 },
5470 {
5471 "measurement": "b",
5472 "weight": 0.30,
5473 "p10": 100,
5474 "p50": 200,
5475 "optional": true
5476 },
5477 {
5478 "measurement": "c",
5479 "weight": 0.55,
5480 "p10": 100,
5481 "p50": 200,
5482 "optional": true
5483 },
5484 ],
5485 "condition": {
5486 "op": "and",
5487 "inner": [],
5488 }
5489 }
5490 ]
5491 }))
5492 .unwrap();
5493
5494 normalize_performance_score(&mut event, Some(&performance_score));
5495
5496 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
5497 {
5498 "type": "transaction",
5499 "timestamp": 1619420405.0,
5500 "start_timestamp": 1619420400.0,
5501 "contexts": {},
5502 "measurements": {
5503 "a": {
5504 "value": 213.0,
5505 "unit": "millisecond",
5506 },
5507 "b": {
5508 "value": 213.0,
5509 "unit": "millisecond",
5510 },
5511 "score.a": {
5512 "value": 0.15121816827413334,
5513 "unit": "ratio",
5514 },
5515 "score.b": {
5516 "value": 0.3024363365482667,
5517 "unit": "ratio",
5518 },
5519 "score.ratio.a": {
5520 "value": 0.45365450482239994,
5521 "unit": "ratio",
5522 },
5523 "score.ratio.b": {
5524 "value": 0.45365450482239994,
5525 "unit": "ratio",
5526 },
5527 "score.total": {
5528 "value": 0.4536545048224,
5529 "unit": "ratio",
5530 },
5531 "score.weight.a": {
5532 "value": 0.33333333333333337,
5533 "unit": "ratio",
5534 },
5535 "score.weight.b": {
5536 "value": 0.6666666666666667,
5537 "unit": "ratio",
5538 },
5539 "score.weight.c": {
5540 "value": 0.0,
5541 "unit": "ratio",
5542 },
5543 },
5544 }
5545 "###);
5546 }
5547
5548 #[test]
5549 fn test_normalization_removes_reprocessing_context() {
5550 let json = r#"{
5551 "contexts": {
5552 "reprocessing": {}
5553 }
5554 }"#;
5555 let mut event = Annotated::<Event>::from_json(json).unwrap();
5556 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
5557 normalize_event(&mut event, &NormalizationConfig::default());
5558 assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
5559 }
5560
5561 #[test]
5562 fn test_renormalization_does_not_remove_reprocessing_context() {
5563 let json = r#"{
5564 "contexts": {
5565 "reprocessing": {}
5566 }
5567 }"#;
5568 let mut event = Annotated::<Event>::from_json(json).unwrap();
5569 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
5570 normalize_event(
5571 &mut event,
5572 &NormalizationConfig {
5573 is_renormalize: true,
5574 ..Default::default()
5575 },
5576 );
5577 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
5578 }
5579
5580 #[test]
5581 fn test_normalize_user() {
5582 let json = r#"{
5583 "user": {
5584 "id": "123456",
5585 "username": "john",
5586 "other": "value"
5587 }
5588 }"#;
5589 let mut event = Annotated::<Event>::from_json(json).unwrap();
5590 normalize_user(event.value_mut().as_mut().unwrap());
5591
5592 let user = event.value().unwrap().user.value().unwrap();
5593 assert_eq!(user.data, {
5594 let mut map = Object::new();
5595 map.insert(
5596 "other".to_owned(),
5597 Annotated::new(Value::String("value".to_owned())),
5598 );
5599 Annotated::new(map)
5600 });
5601 assert_eq!(user.other, Object::new());
5602 assert_eq!(user.username, Annotated::new("john".to_owned().into()));
5603 assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
5604 }
5605
5606 #[test]
5607 fn test_handle_types_in_spaced_exception_values() {
5608 let mut exception = Annotated::new(Exception {
5609 value: Annotated::new("ValueError: unauthorized".to_owned().into()),
5610 ..Exception::default()
5611 });
5612 normalize_exception(&mut exception);
5613
5614 let exception = exception.value().unwrap();
5615 assert_eq!(exception.value.as_str(), Some("unauthorized"));
5616 assert_eq!(exception.ty.as_str(), Some("ValueError"));
5617 }
5618
5619 #[test]
5620 fn test_handle_types_in_non_spaced_excepton_values() {
5621 let mut exception = Annotated::new(Exception {
5622 value: Annotated::new("ValueError:unauthorized".to_owned().into()),
5623 ..Exception::default()
5624 });
5625 normalize_exception(&mut exception);
5626
5627 let exception = exception.value().unwrap();
5628 assert_eq!(exception.value.as_str(), Some("unauthorized"));
5629 assert_eq!(exception.ty.as_str(), Some("ValueError"));
5630 }
5631
5632 #[test]
5633 fn test_rejects_empty_exception_fields() {
5634 let mut exception = Annotated::new(Exception {
5635 value: Annotated::new("".to_owned().into()),
5636 ty: Annotated::new("".to_owned()),
5637 ..Default::default()
5638 });
5639
5640 normalize_exception(&mut exception);
5641
5642 assert!(exception.value().is_none());
5643 assert!(exception.meta().has_errors());
5644 }
5645
5646 #[test]
5647 fn test_json_value() {
5648 let mut exception = Annotated::new(Exception {
5649 value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
5650 ..Exception::default()
5651 });
5652
5653 normalize_exception(&mut exception);
5654
5655 let exception = exception.value().unwrap();
5656
5657 assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
5659 assert_eq!(exception.ty.value(), None);
5660 }
5661
5662 #[test]
5663 fn test_exception_invalid() {
5664 let mut exception = Annotated::new(Exception::default());
5665
5666 normalize_exception(&mut exception);
5667
5668 let expected = Error::with(ErrorKind::MissingAttribute, |error| {
5669 error.insert("attribute", "type or value");
5670 });
5671 assert_eq!(
5672 exception.meta().iter_errors().collect_tuple(),
5673 Some((&expected,))
5674 );
5675 }
5676
5677 #[test]
5678 fn test_normalize_exception() {
5679 let mut event = Annotated::new(Event {
5680 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
5681 ty: Annotated::empty(),
5683 value: Annotated::empty(),
5684 ..Default::default()
5685 })])),
5686 ..Default::default()
5687 });
5688
5689 normalize_event(&mut event, &NormalizationConfig::default());
5690
5691 let exception = event
5692 .value()
5693 .unwrap()
5694 .exceptions
5695 .value()
5696 .unwrap()
5697 .values
5698 .value()
5699 .unwrap()
5700 .first()
5701 .unwrap();
5702
5703 assert_debug_snapshot!(exception.meta(), @r###"
5704 Meta {
5705 remarks: [],
5706 errors: [
5707 Error {
5708 kind: MissingAttribute,
5709 data: {
5710 "attribute": String(
5711 "type or value",
5712 ),
5713 },
5714 },
5715 ],
5716 original_length: None,
5717 original_value: Some(
5718 Object(
5719 {
5720 "mechanism": ~,
5721 "module": ~,
5722 "raw_stacktrace": ~,
5723 "stacktrace": ~,
5724 "thread_id": ~,
5725 "type": ~,
5726 "value": ~,
5727 },
5728 ),
5729 ),
5730 }
5731 "###);
5732 }
5733
5734 #[test]
5735 fn test_normalize_breadcrumbs() {
5736 let mut event = Event {
5737 breadcrumbs: Annotated::new(Values {
5738 values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
5739 ..Default::default()
5740 }),
5741 ..Default::default()
5742 };
5743 normalize_breadcrumbs(&mut event);
5744
5745 let breadcrumb = event
5746 .breadcrumbs
5747 .value()
5748 .unwrap()
5749 .values
5750 .value()
5751 .unwrap()
5752 .first()
5753 .unwrap()
5754 .value()
5755 .unwrap();
5756 assert_eq!(breadcrumb.ty.value().unwrap(), "default");
5757 assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
5758 }
5759
5760 #[test]
5761 fn test_other_debug_images_have_meta_errors() {
5762 let mut event = Event {
5763 debug_meta: Annotated::new(DebugMeta {
5764 images: Annotated::new(vec![Annotated::new(
5765 DebugImage::Other(BTreeMap::default()),
5766 )]),
5767 ..Default::default()
5768 }),
5769 ..Default::default()
5770 };
5771 normalize_debug_meta(&mut event);
5772
5773 let debug_image_meta = event
5774 .debug_meta
5775 .value()
5776 .unwrap()
5777 .images
5778 .value()
5779 .unwrap()
5780 .first()
5781 .unwrap()
5782 .meta();
5783 assert_debug_snapshot!(debug_image_meta, @r###"
5784 Meta {
5785 remarks: [],
5786 errors: [
5787 Error {
5788 kind: InvalidData,
5789 data: {
5790 "reason": String(
5791 "unsupported debug image type",
5792 ),
5793 },
5794 },
5795 ],
5796 original_length: None,
5797 original_value: Some(
5798 Object(
5799 {},
5800 ),
5801 ),
5802 }
5803 "###);
5804 }
5805
5806 #[test]
5807 fn test_skip_span_normalization_when_configured() {
5808 let json = r#"{
5809 "type": "transaction",
5810 "start_timestamp": 1,
5811 "timestamp": 2,
5812 "contexts": {
5813 "trace": {
5814 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
5815 "span_id": "aaaaaaaaaaaaaaaa"
5816 }
5817 },
5818 "spans": [
5819 {
5820 "op": "db",
5821 "description": "SELECT * FROM table;",
5822 "start_timestamp": 1,
5823 "timestamp": 2,
5824 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
5825 "span_id": "bbbbbbbbbbbbbbbb",
5826 "parent_span_id": "aaaaaaaaaaaaaaaa"
5827 }
5828 ]
5829 }"#;
5830
5831 let mut event = Annotated::<Event>::from_json(json).unwrap();
5832 assert!(get_value!(event.spans[0].exclusive_time).is_none());
5833 normalize_event(
5834 &mut event,
5835 &NormalizationConfig {
5836 is_renormalize: true,
5837 ..Default::default()
5838 },
5839 );
5840 assert!(get_value!(event.spans[0].exclusive_time).is_none());
5841 normalize_event(
5842 &mut event,
5843 &NormalizationConfig {
5844 is_renormalize: false,
5845 ..Default::default()
5846 },
5847 );
5848 assert!(get_value!(event.spans[0].exclusive_time).is_some());
5849 }
5850
5851 #[test]
5852 fn test_normalize_trace_context_tags_extracts_lcp_info() {
5853 let json = r#"{
5854 "type": "transaction",
5855 "start_timestamp": 1,
5856 "timestamp": 2,
5857 "contexts": {
5858 "trace": {
5859 "data": {
5860 "browser.web_vital.lcp.element": "body > div#app > div > h1#header",
5861 "browser.web_vital.lcp.size": 24827,
5862 "browser.web_vital.lcp.id": "header",
5863 "browser.web_vital.lcp.url": "http://example.com/image.jpg"
5864 }
5865 }
5866 },
5867 "measurements": {
5868 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
5869 }
5870 }"#;
5871 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5872 normalize_trace_context_tags(&mut event);
5873 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
5874 {
5875 "type": "transaction",
5876 "timestamp": 2.0,
5877 "start_timestamp": 1.0,
5878 "contexts": {
5879 "trace": {
5880 "data": {
5881 "browser.web_vital.lcp.element": "body > div#app > div > h1#header",
5882 "browser.web_vital.lcp.id": "header",
5883 "browser.web_vital.lcp.size": 24827,
5884 "browser.web_vital.lcp.url": "http://example.com/image.jpg",
5885 },
5886 "type": "trace",
5887 },
5888 },
5889 "tags": [
5890 [
5891 "lcp.element",
5892 "body > div#app > div > h1#header",
5893 ],
5894 [
5895 "lcp.size",
5896 "24827",
5897 ],
5898 [
5899 "lcp.id",
5900 "header",
5901 ],
5902 [
5903 "lcp.url",
5904 "http://example.com/image.jpg",
5905 ],
5906 ],
5907 "measurements": {
5908 "lcp": {
5909 "value": 146.20000000298023,
5910 "unit": "millisecond",
5911 },
5912 },
5913 }
5914 "###);
5915 }
5916
5917 #[test]
5918 fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
5919 let json = r#"{
5920 "type": "transaction",
5921 "start_timestamp": 1,
5922 "timestamp": 2,
5923 "contexts": {
5924 "trace": {
5925 "data": {
5926 "browser.web_vital.lcp.element": "body > div#app > div > h1#id",
5927 "browser.web_vital.lcp.size": 33333,
5928 "browser.web_vital.lcp.id": "id",
5929 "browser.web_vital.lcp.url": "http://example.com/another-image.jpg"
5930 }
5931 }
5932 },
5933 "tags": {
5934 "lcp.element": "body > div#app > div > h1#header",
5935 "lcp.size": 24827,
5936 "lcp.id": "header",
5937 "lcp.url": "http://example.com/image.jpg"
5938 },
5939 "measurements": {
5940 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
5941 }
5942 }"#;
5943 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
5944 normalize_trace_context_tags(&mut event);
5945 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
5946 {
5947 "type": "transaction",
5948 "timestamp": 2.0,
5949 "start_timestamp": 1.0,
5950 "contexts": {
5951 "trace": {
5952 "data": {
5953 "browser.web_vital.lcp.element": "body > div#app > div > h1#id",
5954 "browser.web_vital.lcp.id": "id",
5955 "browser.web_vital.lcp.size": 33333,
5956 "browser.web_vital.lcp.url": "http://example.com/another-image.jpg",
5957 },
5958 "type": "trace",
5959 },
5960 },
5961 "tags": [
5962 [
5963 "lcp.element",
5964 "body > div#app > div > h1#header",
5965 ],
5966 [
5967 "lcp.id",
5968 "header",
5969 ],
5970 [
5971 "lcp.size",
5972 "24827",
5973 ],
5974 [
5975 "lcp.url",
5976 "http://example.com/image.jpg",
5977 ],
5978 ],
5979 "measurements": {
5980 "lcp": {
5981 "value": 146.20000000298023,
5982 "unit": "millisecond",
5983 },
5984 },
5985 }
5986 "###);
5987 }
5988
5989 #[test]
5990 fn test_tags_are_trimmed() {
5991 let json = r#"
5992 {
5993 "tags": {
5994 "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_",
5995 "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"
5996 }
5997 }
5998 "#;
5999
6000 let mut event = Annotated::<Event>::from_json(json).unwrap();
6001
6002 normalize_event(
6003 &mut event,
6004 &NormalizationConfig {
6005 enable_trimming: true,
6006 ..NormalizationConfig::default()
6007 },
6008 );
6009
6010 insta::assert_debug_snapshot!(get_value!(event.tags!), @r###"
6011 Tags(
6012 PairList(
6013 [
6014 TagEntry(
6015 "key",
6016 Annotated(
6017 "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...",
6018 Meta {
6019 remarks: [
6020 Remark {
6021 ty: Substituted,
6022 rule_id: "!limit",
6023 range: Some(
6024 (
6025 197,
6026 200,
6027 ),
6028 ),
6029 },
6030 ],
6031 errors: [],
6032 original_length: Some(
6033 210,
6034 ),
6035 original_value: None,
6036 },
6037 ),
6038 ),
6039 TagEntry(
6040 Annotated(
6041 "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...",
6042 Meta {
6043 remarks: [
6044 Remark {
6045 ty: Substituted,
6046 rule_id: "!limit",
6047 range: Some(
6048 (
6049 197,
6050 200,
6051 ),
6052 ),
6053 },
6054 ],
6055 errors: [],
6056 original_length: Some(
6057 210,
6058 ),
6059 original_value: None,
6060 },
6061 ),
6062 "value",
6063 ),
6064 ],
6065 ),
6066 )
6067 "###);
6068 }
6069}