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