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