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