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