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