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