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