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