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