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
1396 .max_custom_measurements()
1397 .unwrap_or(usize::MAX);
1398
1399 let mut custom_measurements_count = 0;
1400 let mut removed_measurements = Object::new();
1401
1402 measurements.retain(|name, value| {
1403 let measurement = match value.value_mut() {
1404 Some(m) => m,
1405 None => return false,
1406 };
1407
1408 if !can_be_valid_metric_name(name) {
1409 meta.add_error(Error::invalid(format!(
1410 "Metric name contains invalid characters: \"{name}\""
1411 )));
1412 removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1413 return false;
1414 }
1415
1416 let unit = measurement.unit.value().unwrap_or(&MetricUnit::None);
1418
1419 if let Some(max_name_and_unit_len) = max_name_and_unit_len {
1420 let max_name_len = max_name_and_unit_len - unit.to_string().len();
1421
1422 if name.len() > max_name_len {
1423 meta.add_error(Error::invalid(format!(
1424 "Metric name too long {}/{max_name_len}: \"{name}\"",
1425 name.len(),
1426 )));
1427 removed_measurements
1428 .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1429 return false;
1430 }
1431 }
1432
1433 if let Some(builtin_measurement) = measurements_config
1435 .builtin_measurement_keys()
1436 .find(|builtin| builtin.name() == name)
1437 {
1438 let value = measurement.value.value().unwrap_or(&FiniteF64::ZERO);
1439 if !builtin_measurement.allow_negative() && *value < 0.0 {
1441 meta.add_error(Error::invalid(format!(
1442 "Negative value for measurement {name} not allowed: {value}",
1443 )));
1444 removed_measurements
1445 .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1446 return false;
1447 }
1448 return builtin_measurement.unit() == unit;
1452 }
1453
1454 if custom_measurements_count < max_custom_measurements {
1456 custom_measurements_count += 1;
1457 return true;
1458 }
1459
1460 meta.add_error(Error::invalid(format!("Too many measurements: {name}")));
1461 removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1462
1463 false
1464 });
1465
1466 if !removed_measurements.is_empty() {
1467 meta.set_original_value(Some(removed_measurements));
1468 }
1469}
1470
1471fn get_metric_measurement_unit(measurement_name: &str) -> Option<MetricUnit> {
1476 match measurement_name {
1477 "fcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1479 "lcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1480 "fid" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1481 "fp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1482 "inp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1483 "ttfb" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1484 "ttfb.requesttime" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1485 "cls" => Some(MetricUnit::None),
1486
1487 "app_start_cold" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1489 "app_start_warm" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1490 "frames_total" => Some(MetricUnit::None),
1491 "frames_slow" => Some(MetricUnit::None),
1492 "frames_slow_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1493 "frames_frozen" => Some(MetricUnit::None),
1494 "frames_frozen_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1495 "time_to_initial_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1496 "time_to_full_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1497
1498 "stall_count" => Some(MetricUnit::None),
1500 "stall_total_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1501 "stall_longest_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1502 "stall_percentage" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1503
1504 _ => None,
1506 }
1507}
1508
1509fn normalize_app_start_measurements(measurements: &mut Measurements) {
1514 if let Some(app_start_cold_value) = measurements.remove("app.start.cold") {
1515 measurements.insert("app_start_cold".to_owned(), app_start_cold_value);
1516 }
1517 if let Some(app_start_warm_value) = measurements.remove("app.start.warm") {
1518 measurements.insert("app_start_warm".to_owned(), app_start_warm_value);
1519 }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524
1525 use std::collections::BTreeMap;
1526 use std::collections::HashMap;
1527
1528 use insta::assert_debug_snapshot;
1529 use itertools::Itertools;
1530 use relay_event_schema::protocol::{Breadcrumb, Csp, DebugMeta, DeviceContext, Values};
1531 use relay_protocol::{SerializableAnnotated, get_value};
1532 use serde_json::json;
1533
1534 use super::*;
1535 use crate::{ClientHints, MeasurementsConfig, ModelCostV2};
1536
1537 const IOS_MOBILE_EVENT: &str = r#"
1538 {
1539 "sdk": {"name": "sentry.cocoa"},
1540 "contexts": {
1541 "trace": {
1542 "op": "ui.load"
1543 }
1544 },
1545 "measurements": {
1546 "app_start_warm": {
1547 "value": 8049.345970153808,
1548 "unit": "millisecond"
1549 },
1550 "time_to_full_display": {
1551 "value": 8240.571022033691,
1552 "unit": "millisecond"
1553 },
1554 "time_to_initial_display": {
1555 "value": 8049.345970153808,
1556 "unit": "millisecond"
1557 }
1558 }
1559 }
1560 "#;
1561
1562 const ANDROID_MOBILE_EVENT: &str = r#"
1563 {
1564 "sdk": {"name": "sentry.java.android"},
1565 "contexts": {
1566 "trace": {
1567 "op": "ui.load"
1568 }
1569 },
1570 "measurements": {
1571 "app_start_cold": {
1572 "value": 22648,
1573 "unit": "millisecond"
1574 },
1575 "time_to_full_display": {
1576 "value": 22647,
1577 "unit": "millisecond"
1578 },
1579 "time_to_initial_display": {
1580 "value": 22647,
1581 "unit": "millisecond"
1582 }
1583 }
1584 }
1585 "#;
1586
1587 #[test]
1588 fn test_normalize_dist_none() {
1589 let mut dist = Annotated::default();
1590 normalize_dist(&mut dist);
1591 assert_eq!(dist.value(), None);
1592 }
1593
1594 #[test]
1595 fn test_normalize_dist_empty() {
1596 let mut dist = Annotated::new("".to_owned());
1597 normalize_dist(&mut dist);
1598 assert_eq!(dist.value(), None);
1599 }
1600
1601 #[test]
1602 fn test_normalize_dist_trim() {
1603 let mut dist = Annotated::new(" foo ".to_owned());
1604 normalize_dist(&mut dist);
1605 assert_eq!(dist.value(), Some(&"foo".to_owned()));
1606 }
1607
1608 #[test]
1609 fn test_normalize_dist_whitespace() {
1610 let mut dist = Annotated::new(" ".to_owned());
1611 normalize_dist(&mut dist);
1612 assert_eq!(dist.value(), None);
1613 }
1614
1615 #[test]
1616 fn test_normalize_platform_and_level_with_transaction_event() {
1617 let json = r#"
1618 {
1619 "type": "transaction"
1620 }
1621 "#;
1622
1623 let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1624 else {
1625 panic!("Invalid transaction json");
1626 };
1627
1628 normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1629
1630 assert_eq!(event.level.value().unwrap().to_string(), "info");
1631 assert_eq!(event.ty.value().unwrap().to_string(), "transaction");
1632 assert_eq!(event.platform.as_str().unwrap(), "other");
1633 }
1634
1635 #[test]
1636 fn test_normalize_platform_and_level_with_error_event() {
1637 let json = r#"
1638 {
1639 "type": "error",
1640 "exception": {
1641 "values": [{"type": "ValueError", "value": "Should not happen"}]
1642 }
1643 }
1644 "#;
1645
1646 let Annotated(Some(mut event), mut meta) = Annotated::<Event>::from_json(json).unwrap()
1647 else {
1648 panic!("Invalid error json");
1649 };
1650
1651 normalize_default_attributes(&mut event, &mut meta, &NormalizationConfig::default());
1652
1653 assert_eq!(event.level.value().unwrap().to_string(), "error");
1654 assert_eq!(event.ty.value().unwrap().to_string(), "error");
1655 assert_eq!(event.platform.value().unwrap().to_owned(), "other");
1656 }
1657
1658 #[test]
1659 fn test_computed_measurements() {
1660 let json = r#"
1661 {
1662 "type": "transaction",
1663 "timestamp": "2021-04-26T08:00:05+0100",
1664 "start_timestamp": "2021-04-26T08:00:00+0100",
1665 "measurements": {
1666 "frames_slow": {"value": 1},
1667 "frames_frozen": {"value": 2},
1668 "frames_total": {"value": 4},
1669 "stall_total_time": {"value": 4000, "unit": "millisecond"}
1670 }
1671 }
1672 "#;
1673
1674 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1675
1676 normalize_event_measurements(&mut event, None, None);
1677
1678 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1679 {
1680 "type": "transaction",
1681 "timestamp": 1619420405.0,
1682 "start_timestamp": 1619420400.0,
1683 "measurements": {
1684 "frames_frozen": {
1685 "value": 2.0,
1686 "unit": "none",
1687 },
1688 "frames_frozen_rate": {
1689 "value": 0.5,
1690 "unit": "ratio",
1691 },
1692 "frames_slow": {
1693 "value": 1.0,
1694 "unit": "none",
1695 },
1696 "frames_slow_rate": {
1697 "value": 0.25,
1698 "unit": "ratio",
1699 },
1700 "frames_total": {
1701 "value": 4.0,
1702 "unit": "none",
1703 },
1704 "stall_percentage": {
1705 "value": 0.8,
1706 "unit": "ratio",
1707 },
1708 "stall_total_time": {
1709 "value": 4000.0,
1710 "unit": "millisecond",
1711 },
1712 },
1713 }
1714 "###);
1715 }
1716
1717 #[test]
1718 fn test_filter_custom_measurements() {
1719 let json = r#"
1720 {
1721 "type": "transaction",
1722 "timestamp": "2021-04-26T08:00:05+0100",
1723 "start_timestamp": "2021-04-26T08:00:00+0100",
1724 "measurements": {
1725 "my_custom_measurement_1": {"value": 123},
1726 "frames_frozen": {"value": 666, "unit": "invalid_unit"},
1727 "frames_slow": {"value": 1},
1728 "my_custom_measurement_3": {"value": 456},
1729 "my_custom_measurement_2": {"value": 789}
1730 }
1731 }
1732 "#;
1733 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
1734
1735 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
1736 "builtinMeasurements": [
1737 {"name": "frames_frozen", "unit": "none"},
1738 {"name": "frames_slow", "unit": "none"}
1739 ],
1740 "maxCustomMeasurements": 2,
1741 "stray_key": "zzz"
1742 }))
1743 .unwrap();
1744
1745 let dynamic_measurement_config =
1746 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
1747
1748 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
1749
1750 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
1752 {
1753 "type": "transaction",
1754 "timestamp": 1619420405.0,
1755 "start_timestamp": 1619420400.0,
1756 "measurements": {
1757 "frames_slow": {
1758 "value": 1.0,
1759 "unit": "none",
1760 },
1761 "my_custom_measurement_1": {
1762 "value": 123.0,
1763 "unit": "none",
1764 },
1765 "my_custom_measurement_2": {
1766 "value": 789.0,
1767 "unit": "none",
1768 },
1769 },
1770 "_meta": {
1771 "measurements": {
1772 "": Meta(Some(MetaInner(
1773 err: [
1774 [
1775 "invalid_data",
1776 {
1777 "reason": "Too many measurements: my_custom_measurement_3",
1778 },
1779 ],
1780 ],
1781 val: Some({
1782 "my_custom_measurement_3": {
1783 "unit": "none",
1784 "value": 456.0,
1785 },
1786 }),
1787 ))),
1788 },
1789 },
1790 }
1791 "###);
1792 }
1793
1794 #[test]
1795 fn test_normalize_units() {
1796 let mut measurements = Annotated::<Measurements>::from_json(
1797 r#"{
1798 "fcp": {"value": 1.1},
1799 "stall_count": {"value": 3.3},
1800 "foo": {"value": 8.8}
1801 }"#,
1802 )
1803 .unwrap()
1804 .into_value()
1805 .unwrap();
1806 insta::assert_debug_snapshot!(measurements, @r###"
1807 Measurements(
1808 {
1809 "fcp": Measurement {
1810 value: 1.1,
1811 unit: ~,
1812 },
1813 "foo": Measurement {
1814 value: 8.8,
1815 unit: ~,
1816 },
1817 "stall_count": Measurement {
1818 value: 3.3,
1819 unit: ~,
1820 },
1821 },
1822 )
1823 "###);
1824 normalize_units(&mut measurements);
1825 insta::assert_debug_snapshot!(measurements, @r###"
1826 Measurements(
1827 {
1828 "fcp": Measurement {
1829 value: 1.1,
1830 unit: Duration(
1831 MilliSecond,
1832 ),
1833 },
1834 "foo": Measurement {
1835 value: 8.8,
1836 unit: None,
1837 },
1838 "stall_count": Measurement {
1839 value: 3.3,
1840 unit: None,
1841 },
1842 },
1843 )
1844 "###);
1845 }
1846
1847 #[test]
1848 fn test_normalize_security_report() {
1849 let mut event = Event {
1850 csp: Annotated::from(Csp::default()),
1851 ..Default::default()
1852 };
1853 let ipaddr = IpAddr("213.164.1.114".to_owned());
1854
1855 let client_ip = Some(&ipaddr);
1856
1857 let user_agent = RawUserAgentInfo {
1858 user_agent: Some(
1859 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0",
1860 ),
1861 client_hints: ClientHints {
1862 sec_ch_ua_platform: Some("macOS"),
1863 sec_ch_ua_platform_version: Some("13.2.0"),
1864 sec_ch_ua: Some(
1865 r#""Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110""#,
1866 ),
1867 sec_ch_ua_model: Some("some model"),
1868 },
1869 };
1870
1871 normalize_security_report(&mut event, client_ip, &user_agent);
1874
1875 let headers = event
1876 .request
1877 .value_mut()
1878 .get_or_insert_with(Request::default)
1879 .headers
1880 .value_mut()
1881 .get_or_insert_with(Headers::default);
1882
1883 assert_eq!(
1884 event.user.value().unwrap().ip_address,
1885 Annotated::from(ipaddr)
1886 );
1887 assert_eq!(
1888 headers.get_header(RawUserAgentInfo::USER_AGENT),
1889 user_agent.user_agent
1890 );
1891 assert_eq!(
1892 headers.get_header(ClientHints::SEC_CH_UA),
1893 user_agent.client_hints.sec_ch_ua,
1894 );
1895 assert_eq!(
1896 headers.get_header(ClientHints::SEC_CH_UA_MODEL),
1897 user_agent.client_hints.sec_ch_ua_model,
1898 );
1899 assert_eq!(
1900 headers.get_header(ClientHints::SEC_CH_UA_PLATFORM),
1901 user_agent.client_hints.sec_ch_ua_platform,
1902 );
1903 assert_eq!(
1904 headers.get_header(ClientHints::SEC_CH_UA_PLATFORM_VERSION),
1905 user_agent.client_hints.sec_ch_ua_platform_version,
1906 );
1907
1908 assert!(
1909 std::mem::size_of_val(&ClientHints::<&str>::default()) == 64,
1910 "If you add new fields, update the test accordingly"
1911 );
1912 }
1913
1914 #[test]
1915 fn test_no_device_class() {
1916 let mut event = Event {
1917 ..Default::default()
1918 };
1919 normalize_device_class(&mut event);
1920 let tags = &event.tags.value_mut().get_or_insert_with(Tags::default).0;
1921 assert_eq!(None, tags.get("device_class"));
1922 }
1923
1924 #[test]
1925 fn test_apple_low_device_class() {
1926 let mut event = Event {
1927 contexts: {
1928 let mut contexts = Contexts::new();
1929 contexts.add(DeviceContext {
1930 family: "iPhone".to_owned().into(),
1931 model: "iPhone8,4".to_owned().into(),
1932 ..Default::default()
1933 });
1934 Annotated::new(contexts)
1935 },
1936 ..Default::default()
1937 };
1938 normalize_device_class(&mut event);
1939 assert_debug_snapshot!(event.tags, @r###"
1940 Tags(
1941 PairList(
1942 [
1943 TagEntry(
1944 "device.class",
1945 "1",
1946 ),
1947 ],
1948 ),
1949 )
1950 "###);
1951 }
1952
1953 #[test]
1954 fn test_apple_medium_device_class() {
1955 let mut event = Event {
1956 contexts: {
1957 let mut contexts = Contexts::new();
1958 contexts.add(DeviceContext {
1959 family: "iPhone".to_owned().into(),
1960 model: "iPhone12,8".to_owned().into(),
1961 ..Default::default()
1962 });
1963 Annotated::new(contexts)
1964 },
1965 ..Default::default()
1966 };
1967 normalize_device_class(&mut event);
1968 assert_debug_snapshot!(event.tags, @r###"
1969 Tags(
1970 PairList(
1971 [
1972 TagEntry(
1973 "device.class",
1974 "2",
1975 ),
1976 ],
1977 ),
1978 )
1979 "###);
1980 }
1981
1982 #[test]
1983 fn test_android_low_device_class() {
1984 let mut event = Event {
1985 contexts: {
1986 let mut contexts = Contexts::new();
1987 contexts.add(DeviceContext {
1988 family: "android".to_owned().into(),
1989 processor_frequency: 1000.into(),
1990 processor_count: 6.into(),
1991 memory_size: (2 * 1024 * 1024 * 1024).into(),
1992 ..Default::default()
1993 });
1994 Annotated::new(contexts)
1995 },
1996 ..Default::default()
1997 };
1998 normalize_device_class(&mut event);
1999 assert_debug_snapshot!(event.tags, @r###"
2000 Tags(
2001 PairList(
2002 [
2003 TagEntry(
2004 "device.class",
2005 "1",
2006 ),
2007 ],
2008 ),
2009 )
2010 "###);
2011 }
2012
2013 #[test]
2014 fn test_android_medium_device_class() {
2015 let mut event = Event {
2016 contexts: {
2017 let mut contexts = Contexts::new();
2018 contexts.add(DeviceContext {
2019 family: "android".to_owned().into(),
2020 processor_frequency: 2000.into(),
2021 processor_count: 8.into(),
2022 memory_size: (6 * 1024 * 1024 * 1024).into(),
2023 ..Default::default()
2024 });
2025 Annotated::new(contexts)
2026 },
2027 ..Default::default()
2028 };
2029 normalize_device_class(&mut event);
2030 assert_debug_snapshot!(event.tags, @r###"
2031 Tags(
2032 PairList(
2033 [
2034 TagEntry(
2035 "device.class",
2036 "2",
2037 ),
2038 ],
2039 ),
2040 )
2041 "###);
2042 }
2043
2044 #[test]
2045 fn test_android_high_device_class() {
2046 let mut event = Event {
2047 contexts: {
2048 let mut contexts = Contexts::new();
2049 contexts.add(DeviceContext {
2050 family: "android".to_owned().into(),
2051 processor_frequency: 2500.into(),
2052 processor_count: 8.into(),
2053 memory_size: (6 * 1024 * 1024 * 1024).into(),
2054 ..Default::default()
2055 });
2056 Annotated::new(contexts)
2057 },
2058 ..Default::default()
2059 };
2060 normalize_device_class(&mut event);
2061 assert_debug_snapshot!(event.tags, @r###"
2062 Tags(
2063 PairList(
2064 [
2065 TagEntry(
2066 "device.class",
2067 "3",
2068 ),
2069 ],
2070 ),
2071 )
2072 "###);
2073 }
2074
2075 #[test]
2076 fn test_keeps_valid_measurement() {
2077 let name = "lcp";
2078 let measurement = Measurement {
2079 value: Annotated::new(420.69.try_into().unwrap()),
2080 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2081 };
2082
2083 assert!(!is_measurement_dropped(name, measurement));
2084 }
2085
2086 #[test]
2087 fn test_drops_too_long_measurement_names() {
2088 let name = "lcpppppppppppppppppppppppppppp";
2089 let measurement = Measurement {
2090 value: Annotated::new(420.69.try_into().unwrap()),
2091 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2092 };
2093
2094 assert!(is_measurement_dropped(name, measurement));
2095 }
2096
2097 #[test]
2098 fn test_drops_measurements_with_invalid_characters() {
2099 let name = "i æm frøm nørwåy";
2100 let measurement = Measurement {
2101 value: Annotated::new(420.69.try_into().unwrap()),
2102 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2103 };
2104
2105 assert!(is_measurement_dropped(name, measurement));
2106 }
2107
2108 fn is_measurement_dropped(name: &str, measurement: Measurement) -> bool {
2109 let max_name_and_unit_len = Some(30);
2110
2111 let mut measurements: BTreeMap<String, Annotated<Measurement>> = Object::new();
2112 measurements.insert(name.to_owned(), Annotated::new(measurement));
2113
2114 let mut measurements = Measurements(measurements);
2115 let mut meta = Meta::default();
2116 let measurements_config = MeasurementsConfig {
2117 max_custom_measurements: 1,
2118 ..Default::default()
2119 };
2120
2121 let dynamic_config = CombinedMeasurementsConfig::new(Some(&measurements_config), None);
2122
2123 assert_eq!(measurements.len(), 1);
2126
2127 remove_invalid_measurements(
2128 &mut measurements,
2129 &mut meta,
2130 dynamic_config,
2131 max_name_and_unit_len,
2132 );
2133
2134 measurements.is_empty()
2136 }
2137
2138 #[test]
2139 fn test_custom_measurements_not_dropped() {
2140 let mut measurements = Measurements(BTreeMap::from([(
2141 "custom_measurement".to_owned(),
2142 Annotated::new(Measurement {
2143 value: Annotated::new(42.0.try_into().unwrap()),
2144 unit: Annotated::new(MetricUnit::Duration(DurationUnit::MilliSecond)),
2145 }),
2146 )]));
2147
2148 let original = measurements.clone();
2149 remove_invalid_measurements(
2150 &mut measurements,
2151 &mut Meta::default(),
2152 CombinedMeasurementsConfig::new(None, None),
2153 Some(30),
2154 );
2155
2156 assert_eq!(original, measurements);
2157 }
2158
2159 #[test]
2160 fn test_normalize_app_start_measurements_does_not_add_measurements() {
2161 let mut measurements = Annotated::<Measurements>::from_json(r###"{}"###)
2162 .unwrap()
2163 .into_value()
2164 .unwrap();
2165 insta::assert_debug_snapshot!(measurements, @r###"
2166 Measurements(
2167 {},
2168 )
2169 "###);
2170 normalize_app_start_measurements(&mut measurements);
2171 insta::assert_debug_snapshot!(measurements, @r###"
2172 Measurements(
2173 {},
2174 )
2175 "###);
2176 }
2177
2178 #[test]
2179 fn test_normalize_app_start_cold_measurements() {
2180 let mut measurements =
2181 Annotated::<Measurements>::from_json(r#"{"app.start.cold": {"value": 1.1}}"#)
2182 .unwrap()
2183 .into_value()
2184 .unwrap();
2185 insta::assert_debug_snapshot!(measurements, @r###"
2186 Measurements(
2187 {
2188 "app.start.cold": Measurement {
2189 value: 1.1,
2190 unit: ~,
2191 },
2192 },
2193 )
2194 "###);
2195 normalize_app_start_measurements(&mut measurements);
2196 insta::assert_debug_snapshot!(measurements, @r###"
2197 Measurements(
2198 {
2199 "app_start_cold": Measurement {
2200 value: 1.1,
2201 unit: ~,
2202 },
2203 },
2204 )
2205 "###);
2206 }
2207
2208 #[test]
2209 fn test_normalize_app_start_warm_measurements() {
2210 let mut measurements =
2211 Annotated::<Measurements>::from_json(r#"{"app.start.warm": {"value": 1.1}}"#)
2212 .unwrap()
2213 .into_value()
2214 .unwrap();
2215 insta::assert_debug_snapshot!(measurements, @r###"
2216 Measurements(
2217 {
2218 "app.start.warm": Measurement {
2219 value: 1.1,
2220 unit: ~,
2221 },
2222 },
2223 )
2224 "###);
2225 normalize_app_start_measurements(&mut measurements);
2226 insta::assert_debug_snapshot!(measurements, @r###"
2227 Measurements(
2228 {
2229 "app_start_warm": Measurement {
2230 value: 1.1,
2231 unit: ~,
2232 },
2233 },
2234 )
2235 "###);
2236 }
2237
2238 #[test]
2239 fn test_ai_legacy_measurements() {
2240 let json = r#"
2241 {
2242 "spans": [
2243 {
2244 "timestamp": 1702474613.0495,
2245 "start_timestamp": 1702474613.0175,
2246 "description": "OpenAI ",
2247 "op": "ai.chat_completions.openai",
2248 "span_id": "9c01bd820a083e63",
2249 "parent_span_id": "a1e13f3f06239d69",
2250 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2251 "measurements": {
2252 "ai_prompt_tokens_used": {
2253 "value": 1000
2254 },
2255 "ai_completion_tokens_used": {
2256 "value": 2000
2257 }
2258 },
2259 "data": {
2260 "ai.model_id": "claude-2.1"
2261 }
2262 },
2263 {
2264 "timestamp": 1702474613.0495,
2265 "start_timestamp": 1702474613.0175,
2266 "description": "OpenAI ",
2267 "op": "ai.chat_completions.openai",
2268 "span_id": "ac01bd820a083e63",
2269 "parent_span_id": "a1e13f3f06239d69",
2270 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2271 "measurements": {
2272 "ai_prompt_tokens_used": {
2273 "value": 1000
2274 },
2275 "ai_completion_tokens_used": {
2276 "value": 2000
2277 }
2278 },
2279 "data": {
2280 "ai.model_id": "gpt4-21-04"
2281 }
2282 }
2283 ]
2284 }
2285 "#;
2286
2287 let mut event = Annotated::<Event>::from_json(json).unwrap();
2288
2289 normalize_event(
2290 &mut event,
2291 &NormalizationConfig {
2292 ai_model_costs: Some(&ModelCosts {
2293 version: 2,
2294 models: HashMap::from([
2295 (
2296 "claude-2.1".to_owned(),
2297 ModelCostV2 {
2298 input_per_token: 0.01,
2299 output_per_token: 0.02,
2300 output_reasoning_per_token: 0.03,
2301 input_cached_per_token: 0.0,
2302 },
2303 ),
2304 (
2305 "gpt4-21-04".to_owned(),
2306 ModelCostV2 {
2307 input_per_token: 0.02,
2308 output_per_token: 0.03,
2309 output_reasoning_per_token: 0.04,
2310 input_cached_per_token: 0.0,
2311 },
2312 ),
2313 ]),
2314 }),
2315 ..NormalizationConfig::default()
2316 },
2317 );
2318
2319 let spans = event.value().unwrap().spans.value().unwrap();
2320 assert_eq!(spans.len(), 2);
2321 assert_eq!(
2322 spans
2323 .first()
2324 .and_then(|span| span.value())
2325 .and_then(|span| span.data.value())
2326 .and_then(|data| data.gen_ai_usage_total_cost.value()),
2327 Some(&Value::F64(50.0))
2328 );
2329 assert_eq!(
2330 spans
2331 .get(1)
2332 .and_then(|span| span.value())
2333 .and_then(|span| span.data.value())
2334 .and_then(|data| data.gen_ai_usage_total_cost.value()),
2335 Some(&Value::F64(80.0))
2336 );
2337 }
2338
2339 #[test]
2340 fn test_ai_data() {
2341 let json = r#"
2342 {
2343 "spans": [
2344 {
2345 "timestamp": 1702474614.0175,
2346 "start_timestamp": 1702474613.0175,
2347 "description": "OpenAI ",
2348 "op": "gen_ai.chat_completions.openai",
2349 "span_id": "9c01bd820a083e63",
2350 "parent_span_id": "a1e13f3f06239d69",
2351 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2352 "data": {
2353 "gen_ai.usage.input_tokens": 1000,
2354 "gen_ai.usage.output_tokens": 2000,
2355 "gen_ai.usage.output_tokens.reasoning": 1000,
2356 "gen_ai.usage.input_tokens.cached": 500,
2357 "gen_ai.request.model": "claude-2.1"
2358 }
2359 },
2360 {
2361 "timestamp": 1702474614.0175,
2362 "start_timestamp": 1702474613.0175,
2363 "description": "OpenAI ",
2364 "op": "gen_ai.chat_completions.openai",
2365 "span_id": "ac01bd820a083e63",
2366 "parent_span_id": "a1e13f3f06239d69",
2367 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2368 "data": {
2369 "gen_ai.usage.input_tokens": 1000,
2370 "gen_ai.usage.output_tokens": 2000,
2371 "gen_ai.request.model": "gpt4-21-04"
2372 }
2373 },
2374 {
2375 "timestamp": 1702474614.0175,
2376 "start_timestamp": 1702474613.0175,
2377 "description": "OpenAI ",
2378 "op": "gen_ai.chat_completions.openai",
2379 "span_id": "ac01bd820a083e63",
2380 "parent_span_id": "a1e13f3f06239d69",
2381 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2382 "data": {
2383 "gen_ai.usage.input_tokens": 1000,
2384 "gen_ai.usage.output_tokens": 2000,
2385 "gen_ai.response.model": "gpt4-21-04"
2386 }
2387 }
2388 ]
2389 }
2390 "#;
2391
2392 let mut event = Annotated::<Event>::from_json(json).unwrap();
2393
2394 normalize_event(
2395 &mut event,
2396 &NormalizationConfig {
2397 ai_model_costs: Some(&ModelCosts {
2398 version: 2,
2399 models: HashMap::from([
2400 (
2401 "claude-2.1".to_owned(),
2402 ModelCostV2 {
2403 input_per_token: 0.01,
2404 output_per_token: 0.02,
2405 output_reasoning_per_token: 0.03,
2406 input_cached_per_token: 0.04,
2407 },
2408 ),
2409 (
2410 "gpt4-21-04".to_owned(),
2411 ModelCostV2 {
2412 input_per_token: 0.09,
2413 output_per_token: 0.05,
2414 output_reasoning_per_token: 0.0,
2415 input_cached_per_token: 0.0,
2416 },
2417 ),
2418 ]),
2419 }),
2420 ..NormalizationConfig::default()
2421 },
2422 );
2423
2424 let spans = event.value().unwrap().spans.value().unwrap();
2425 assert_eq!(spans.len(), 3);
2426 let first_span_data = spans
2427 .first()
2428 .and_then(|span| span.value())
2429 .and_then(|span| span.data.value());
2430 assert_eq!(
2431 first_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2432 Some(&Value::F64(75.0))
2433 );
2434 assert_eq!(
2435 first_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2436 Some(&Value::F64(2000.0))
2437 );
2438
2439 let second_span_data = spans
2440 .get(1)
2441 .and_then(|span| span.value())
2442 .and_then(|span| span.data.value());
2443 assert_eq!(
2444 second_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2445 Some(&Value::F64(190.0))
2446 );
2447 assert_eq!(
2448 second_span_data.and_then(|data| data.gen_ai_usage_total_tokens.value()),
2449 Some(&Value::F64(3000.0))
2450 );
2451 assert_eq!(
2452 second_span_data.and_then(|data| data.gen_ai_response_tokens_per_second.value()),
2453 Some(&Value::F64(2000.0))
2454 );
2455
2456 let third_span_data = spans
2458 .get(2)
2459 .and_then(|span| span.value())
2460 .and_then(|span| span.data.value());
2461 assert_eq!(
2462 third_span_data.and_then(|data| data.gen_ai_usage_total_cost.value()),
2463 Some(&Value::F64(190.0))
2464 );
2465 }
2466
2467 #[test]
2468 fn test_ai_data_with_no_tokens() {
2469 let json = r#"
2470 {
2471 "spans": [
2472 {
2473 "timestamp": 1702474613.0495,
2474 "start_timestamp": 1702474613.0175,
2475 "description": "OpenAI ",
2476 "op": "gen_ai.invoke_agent",
2477 "span_id": "9c01bd820a083e63",
2478 "parent_span_id": "a1e13f3f06239d69",
2479 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2480 "data": {
2481 "gen_ai.request.model": "claude-2.1"
2482 }
2483 }
2484 ]
2485 }
2486 "#;
2487
2488 let mut event = Annotated::<Event>::from_json(json).unwrap();
2489
2490 normalize_event(
2491 &mut event,
2492 &NormalizationConfig {
2493 ai_model_costs: Some(&ModelCosts {
2494 version: 2,
2495 models: HashMap::from([(
2496 "claude-2.1".to_owned(),
2497 ModelCostV2 {
2498 input_per_token: 0.01,
2499 output_per_token: 0.02,
2500 output_reasoning_per_token: 0.03,
2501 input_cached_per_token: 0.0,
2502 },
2503 )]),
2504 }),
2505 ..NormalizationConfig::default()
2506 },
2507 );
2508
2509 let spans = event.value().unwrap().spans.value().unwrap();
2510
2511 assert_eq!(spans.len(), 1);
2512 assert_eq!(
2514 spans
2515 .first()
2516 .and_then(|span| span.value())
2517 .and_then(|span| span.data.value())
2518 .and_then(|data| data.gen_ai_usage_total_cost.value()),
2519 None
2520 );
2521 assert_eq!(
2523 spans
2524 .first()
2525 .and_then(|span| span.value())
2526 .and_then(|span| span.data.value())
2527 .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2528 None
2529 );
2530 }
2531
2532 #[test]
2533 fn test_ai_data_with_ai_op_prefix() {
2534 let json = r#"
2535 {
2536 "spans": [
2537 {
2538 "timestamp": 1702474613.0495,
2539 "start_timestamp": 1702474613.0175,
2540 "description": "OpenAI ",
2541 "op": "ai.chat_completions.openai",
2542 "span_id": "9c01bd820a083e63",
2543 "parent_span_id": "a1e13f3f06239d69",
2544 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2545 "data": {
2546 "gen_ai.usage.input_tokens": 1000,
2547 "gen_ai.usage.output_tokens": 2000,
2548 "gen_ai.usage.output_tokens.reasoning": 1000,
2549 "gen_ai.usage.input_tokens.cached": 500,
2550 "gen_ai.request.model": "claude-2.1"
2551 }
2552 },
2553 {
2554 "timestamp": 1702474613.0495,
2555 "start_timestamp": 1702474613.0175,
2556 "description": "OpenAI ",
2557 "op": "ai.chat_completions.openai",
2558 "span_id": "ac01bd820a083e63",
2559 "parent_span_id": "a1e13f3f06239d69",
2560 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2561 "data": {
2562 "gen_ai.usage.input_tokens": 1000,
2563 "gen_ai.usage.output_tokens": 2000,
2564 "gen_ai.request.model": "gpt4-21-04"
2565 }
2566 }
2567 ]
2568 }
2569 "#;
2570
2571 let mut event = Annotated::<Event>::from_json(json).unwrap();
2572
2573 normalize_event(
2574 &mut event,
2575 &NormalizationConfig {
2576 ai_model_costs: Some(&ModelCosts {
2577 version: 2,
2578 models: HashMap::from([
2579 (
2580 "claude-2.1".to_owned(),
2581 ModelCostV2 {
2582 input_per_token: 0.01,
2583 output_per_token: 0.02,
2584 output_reasoning_per_token: 0.0,
2585 input_cached_per_token: 0.04,
2586 },
2587 ),
2588 (
2589 "gpt4-21-04".to_owned(),
2590 ModelCostV2 {
2591 input_per_token: 0.09,
2592 output_per_token: 0.05,
2593 output_reasoning_per_token: 0.06,
2594 input_cached_per_token: 0.0,
2595 },
2596 ),
2597 ]),
2598 }),
2599 ..NormalizationConfig::default()
2600 },
2601 );
2602
2603 let spans = event.value().unwrap().spans.value().unwrap();
2604 assert_eq!(spans.len(), 2);
2605 assert_eq!(
2606 spans
2607 .first()
2608 .and_then(|span| span.value())
2609 .and_then(|span| span.data.value())
2610 .and_then(|data| data.gen_ai_usage_total_cost.value()),
2611 Some(&Value::F64(65.0))
2612 );
2613 assert_eq!(
2614 spans
2615 .get(1)
2616 .and_then(|span| span.value())
2617 .and_then(|span| span.data.value())
2618 .and_then(|data| data.gen_ai_usage_total_cost.value()),
2619 Some(&Value::F64(190.0))
2620 );
2621 assert_eq!(
2622 spans
2623 .get(1)
2624 .and_then(|span| span.value())
2625 .and_then(|span| span.data.value())
2626 .and_then(|data| data.gen_ai_usage_total_tokens.value()),
2627 Some(&Value::F64(3000.0))
2628 );
2629 }
2630
2631 #[test]
2632 fn test_ai_response_tokens_per_second_no_output_tokens() {
2633 let json = r#"
2634 {
2635 "spans": [
2636 {
2637 "timestamp": 1702474614.0175,
2638 "start_timestamp": 1702474613.0175,
2639 "op": "gen_ai.chat_completions",
2640 "span_id": "9c01bd820a083e63",
2641 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2642 "data": {
2643 "gen_ai.usage.input_tokens": 500
2644 }
2645 }
2646 ]
2647 }
2648 "#;
2649
2650 let mut event = Annotated::<Event>::from_json(json).unwrap();
2651
2652 normalize_event(
2653 &mut event,
2654 &NormalizationConfig {
2655 ai_model_costs: Some(&ModelCosts {
2656 version: 2,
2657 models: HashMap::new(),
2658 }),
2659 ..NormalizationConfig::default()
2660 },
2661 );
2662
2663 let span_data = get_value!(event.spans[0].data!);
2664
2665 assert!(
2667 span_data
2668 .gen_ai_response_tokens_per_second
2669 .value()
2670 .is_none()
2671 );
2672 }
2673
2674 #[test]
2675 fn test_ai_response_tokens_per_second_zero_duration() {
2676 let json = r#"
2677 {
2678 "spans": [
2679 {
2680 "timestamp": 1702474613.0175,
2681 "start_timestamp": 1702474613.0175,
2682 "op": "gen_ai.chat_completions",
2683 "span_id": "9c01bd820a083e63",
2684 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2685 "data": {
2686 "gen_ai.usage.output_tokens": 1000
2687 }
2688 }
2689 ]
2690 }
2691 "#;
2692
2693 let mut event = Annotated::<Event>::from_json(json).unwrap();
2694
2695 normalize_event(
2696 &mut event,
2697 &NormalizationConfig {
2698 ai_model_costs: Some(&ModelCosts {
2699 version: 2,
2700 models: HashMap::new(),
2701 }),
2702 ..NormalizationConfig::default()
2703 },
2704 );
2705
2706 let span_data = get_value!(event.spans[0].data!);
2707
2708 assert!(
2710 span_data
2711 .gen_ai_response_tokens_per_second
2712 .value()
2713 .is_none()
2714 );
2715 }
2716
2717 #[test]
2718 fn test_apple_high_device_class() {
2719 let mut event = Event {
2720 contexts: {
2721 let mut contexts = Contexts::new();
2722 contexts.add(DeviceContext {
2723 family: "iPhone".to_owned().into(),
2724 model: "iPhone15,3".to_owned().into(),
2725 ..Default::default()
2726 });
2727 Annotated::new(contexts)
2728 },
2729 ..Default::default()
2730 };
2731 normalize_device_class(&mut event);
2732 assert_debug_snapshot!(event.tags, @r###"
2733 Tags(
2734 PairList(
2735 [
2736 TagEntry(
2737 "device.class",
2738 "3",
2739 ),
2740 ],
2741 ),
2742 )
2743 "###);
2744 }
2745
2746 #[test]
2747 fn test_filter_mobile_outliers() {
2748 let mut measurements =
2749 Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2750 .unwrap()
2751 .into_value()
2752 .unwrap();
2753 assert_eq!(measurements.len(), 1);
2754 filter_mobile_outliers(&mut measurements);
2755 assert_eq!(measurements.len(), 0);
2756 }
2757
2758 #[test]
2759 fn test_computed_performance_score() {
2760 let json = r#"
2761 {
2762 "type": "transaction",
2763 "timestamp": "2021-04-26T08:00:05+0100",
2764 "start_timestamp": "2021-04-26T08:00:00+0100",
2765 "measurements": {
2766 "fid": {"value": 213, "unit": "millisecond"},
2767 "fcp": {"value": 1237, "unit": "millisecond"},
2768 "lcp": {"value": 6596, "unit": "millisecond"},
2769 "cls": {"value": 0.11}
2770 },
2771 "contexts": {
2772 "browser": {
2773 "name": "Chrome",
2774 "version": "120.1.1",
2775 "type": "browser"
2776 }
2777 }
2778 }
2779 "#;
2780
2781 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2782
2783 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2784 "profiles": [
2785 {
2786 "name": "Desktop",
2787 "scoreComponents": [
2788 {
2789 "measurement": "fcp",
2790 "weight": 0.15,
2791 "p10": 900,
2792 "p50": 1600
2793 },
2794 {
2795 "measurement": "lcp",
2796 "weight": 0.30,
2797 "p10": 1200,
2798 "p50": 2400
2799 },
2800 {
2801 "measurement": "fid",
2802 "weight": 0.30,
2803 "p10": 100,
2804 "p50": 300
2805 },
2806 {
2807 "measurement": "cls",
2808 "weight": 0.25,
2809 "p10": 0.1,
2810 "p50": 0.25
2811 },
2812 {
2813 "measurement": "ttfb",
2814 "weight": 0.0,
2815 "p10": 0.2,
2816 "p50": 0.4
2817 },
2818 ],
2819 "condition": {
2820 "op":"eq",
2821 "name": "event.contexts.browser.name",
2822 "value": "Chrome"
2823 }
2824 }
2825 ]
2826 }))
2827 .unwrap();
2828
2829 normalize_performance_score(&mut event, Some(&performance_score));
2830
2831 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2832 {
2833 "type": "transaction",
2834 "timestamp": 1619420405.0,
2835 "start_timestamp": 1619420400.0,
2836 "contexts": {
2837 "browser": {
2838 "name": "Chrome",
2839 "version": "120.1.1",
2840 "type": "browser",
2841 },
2842 },
2843 "measurements": {
2844 "cls": {
2845 "value": 0.11,
2846 },
2847 "fcp": {
2848 "value": 1237.0,
2849 "unit": "millisecond",
2850 },
2851 "fid": {
2852 "value": 213.0,
2853 "unit": "millisecond",
2854 },
2855 "lcp": {
2856 "value": 6596.0,
2857 "unit": "millisecond",
2858 },
2859 "score.cls": {
2860 "value": 0.21864170607444863,
2861 "unit": "ratio",
2862 },
2863 "score.fcp": {
2864 "value": 0.10750855443790831,
2865 "unit": "ratio",
2866 },
2867 "score.fid": {
2868 "value": 0.19657361348282545,
2869 "unit": "ratio",
2870 },
2871 "score.lcp": {
2872 "value": 0.009238896571386584,
2873 "unit": "ratio",
2874 },
2875 "score.ratio.cls": {
2876 "value": 0.8745668242977945,
2877 "unit": "ratio",
2878 },
2879 "score.ratio.fcp": {
2880 "value": 0.7167236962527221,
2881 "unit": "ratio",
2882 },
2883 "score.ratio.fid": {
2884 "value": 0.6552453782760849,
2885 "unit": "ratio",
2886 },
2887 "score.ratio.lcp": {
2888 "value": 0.03079632190462195,
2889 "unit": "ratio",
2890 },
2891 "score.total": {
2892 "value": 0.531962770566569,
2893 "unit": "ratio",
2894 },
2895 "score.weight.cls": {
2896 "value": 0.25,
2897 "unit": "ratio",
2898 },
2899 "score.weight.fcp": {
2900 "value": 0.15,
2901 "unit": "ratio",
2902 },
2903 "score.weight.fid": {
2904 "value": 0.3,
2905 "unit": "ratio",
2906 },
2907 "score.weight.lcp": {
2908 "value": 0.3,
2909 "unit": "ratio",
2910 },
2911 "score.weight.ttfb": {
2912 "value": 0.0,
2913 "unit": "ratio",
2914 },
2915 },
2916 }
2917 "###);
2918 }
2919
2920 #[test]
2923 fn test_computed_performance_score_with_under_normalized_weights() {
2924 let json = r#"
2925 {
2926 "type": "transaction",
2927 "timestamp": "2021-04-26T08:00:05+0100",
2928 "start_timestamp": "2021-04-26T08:00:00+0100",
2929 "measurements": {
2930 "fid": {"value": 213, "unit": "millisecond"},
2931 "fcp": {"value": 1237, "unit": "millisecond"},
2932 "lcp": {"value": 6596, "unit": "millisecond"},
2933 "cls": {"value": 0.11}
2934 },
2935 "contexts": {
2936 "browser": {
2937 "name": "Chrome",
2938 "version": "120.1.1",
2939 "type": "browser"
2940 }
2941 }
2942 }
2943 "#;
2944
2945 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2946
2947 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2948 "profiles": [
2949 {
2950 "name": "Desktop",
2951 "scoreComponents": [
2952 {
2953 "measurement": "fcp",
2954 "weight": 0.03,
2955 "p10": 900,
2956 "p50": 1600
2957 },
2958 {
2959 "measurement": "lcp",
2960 "weight": 0.06,
2961 "p10": 1200,
2962 "p50": 2400
2963 },
2964 {
2965 "measurement": "fid",
2966 "weight": 0.06,
2967 "p10": 100,
2968 "p50": 300
2969 },
2970 {
2971 "measurement": "cls",
2972 "weight": 0.05,
2973 "p10": 0.1,
2974 "p50": 0.25
2975 },
2976 {
2977 "measurement": "ttfb",
2978 "weight": 0.0,
2979 "p10": 0.2,
2980 "p50": 0.4
2981 },
2982 ],
2983 "condition": {
2984 "op":"eq",
2985 "name": "event.contexts.browser.name",
2986 "value": "Chrome"
2987 }
2988 }
2989 ]
2990 }))
2991 .unwrap();
2992
2993 normalize_performance_score(&mut event, Some(&performance_score));
2994
2995 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2996 {
2997 "type": "transaction",
2998 "timestamp": 1619420405.0,
2999 "start_timestamp": 1619420400.0,
3000 "contexts": {
3001 "browser": {
3002 "name": "Chrome",
3003 "version": "120.1.1",
3004 "type": "browser",
3005 },
3006 },
3007 "measurements": {
3008 "cls": {
3009 "value": 0.11,
3010 },
3011 "fcp": {
3012 "value": 1237.0,
3013 "unit": "millisecond",
3014 },
3015 "fid": {
3016 "value": 213.0,
3017 "unit": "millisecond",
3018 },
3019 "lcp": {
3020 "value": 6596.0,
3021 "unit": "millisecond",
3022 },
3023 "score.cls": {
3024 "value": 0.21864170607444863,
3025 "unit": "ratio",
3026 },
3027 "score.fcp": {
3028 "value": 0.10750855443790831,
3029 "unit": "ratio",
3030 },
3031 "score.fid": {
3032 "value": 0.19657361348282545,
3033 "unit": "ratio",
3034 },
3035 "score.lcp": {
3036 "value": 0.009238896571386584,
3037 "unit": "ratio",
3038 },
3039 "score.ratio.cls": {
3040 "value": 0.8745668242977945,
3041 "unit": "ratio",
3042 },
3043 "score.ratio.fcp": {
3044 "value": 0.7167236962527221,
3045 "unit": "ratio",
3046 },
3047 "score.ratio.fid": {
3048 "value": 0.6552453782760849,
3049 "unit": "ratio",
3050 },
3051 "score.ratio.lcp": {
3052 "value": 0.03079632190462195,
3053 "unit": "ratio",
3054 },
3055 "score.total": {
3056 "value": 0.531962770566569,
3057 "unit": "ratio",
3058 },
3059 "score.weight.cls": {
3060 "value": 0.25,
3061 "unit": "ratio",
3062 },
3063 "score.weight.fcp": {
3064 "value": 0.15,
3065 "unit": "ratio",
3066 },
3067 "score.weight.fid": {
3068 "value": 0.3,
3069 "unit": "ratio",
3070 },
3071 "score.weight.lcp": {
3072 "value": 0.3,
3073 "unit": "ratio",
3074 },
3075 "score.weight.ttfb": {
3076 "value": 0.0,
3077 "unit": "ratio",
3078 },
3079 },
3080 }
3081 "###);
3082 }
3083
3084 #[test]
3087 fn test_computed_performance_score_with_over_normalized_weights() {
3088 let json = r#"
3089 {
3090 "type": "transaction",
3091 "timestamp": "2021-04-26T08:00:05+0100",
3092 "start_timestamp": "2021-04-26T08:00:00+0100",
3093 "measurements": {
3094 "fid": {"value": 213, "unit": "millisecond"},
3095 "fcp": {"value": 1237, "unit": "millisecond"},
3096 "lcp": {"value": 6596, "unit": "millisecond"},
3097 "cls": {"value": 0.11}
3098 },
3099 "contexts": {
3100 "browser": {
3101 "name": "Chrome",
3102 "version": "120.1.1",
3103 "type": "browser"
3104 }
3105 }
3106 }
3107 "#;
3108
3109 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3110
3111 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3112 "profiles": [
3113 {
3114 "name": "Desktop",
3115 "scoreComponents": [
3116 {
3117 "measurement": "fcp",
3118 "weight": 0.30,
3119 "p10": 900,
3120 "p50": 1600
3121 },
3122 {
3123 "measurement": "lcp",
3124 "weight": 0.60,
3125 "p10": 1200,
3126 "p50": 2400
3127 },
3128 {
3129 "measurement": "fid",
3130 "weight": 0.60,
3131 "p10": 100,
3132 "p50": 300
3133 },
3134 {
3135 "measurement": "cls",
3136 "weight": 0.50,
3137 "p10": 0.1,
3138 "p50": 0.25
3139 },
3140 {
3141 "measurement": "ttfb",
3142 "weight": 0.0,
3143 "p10": 0.2,
3144 "p50": 0.4
3145 },
3146 ],
3147 "condition": {
3148 "op":"eq",
3149 "name": "event.contexts.browser.name",
3150 "value": "Chrome"
3151 }
3152 }
3153 ]
3154 }))
3155 .unwrap();
3156
3157 normalize_performance_score(&mut event, Some(&performance_score));
3158
3159 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3160 {
3161 "type": "transaction",
3162 "timestamp": 1619420405.0,
3163 "start_timestamp": 1619420400.0,
3164 "contexts": {
3165 "browser": {
3166 "name": "Chrome",
3167 "version": "120.1.1",
3168 "type": "browser",
3169 },
3170 },
3171 "measurements": {
3172 "cls": {
3173 "value": 0.11,
3174 },
3175 "fcp": {
3176 "value": 1237.0,
3177 "unit": "millisecond",
3178 },
3179 "fid": {
3180 "value": 213.0,
3181 "unit": "millisecond",
3182 },
3183 "lcp": {
3184 "value": 6596.0,
3185 "unit": "millisecond",
3186 },
3187 "score.cls": {
3188 "value": 0.21864170607444863,
3189 "unit": "ratio",
3190 },
3191 "score.fcp": {
3192 "value": 0.10750855443790831,
3193 "unit": "ratio",
3194 },
3195 "score.fid": {
3196 "value": 0.19657361348282545,
3197 "unit": "ratio",
3198 },
3199 "score.lcp": {
3200 "value": 0.009238896571386584,
3201 "unit": "ratio",
3202 },
3203 "score.ratio.cls": {
3204 "value": 0.8745668242977945,
3205 "unit": "ratio",
3206 },
3207 "score.ratio.fcp": {
3208 "value": 0.7167236962527221,
3209 "unit": "ratio",
3210 },
3211 "score.ratio.fid": {
3212 "value": 0.6552453782760849,
3213 "unit": "ratio",
3214 },
3215 "score.ratio.lcp": {
3216 "value": 0.03079632190462195,
3217 "unit": "ratio",
3218 },
3219 "score.total": {
3220 "value": 0.531962770566569,
3221 "unit": "ratio",
3222 },
3223 "score.weight.cls": {
3224 "value": 0.25,
3225 "unit": "ratio",
3226 },
3227 "score.weight.fcp": {
3228 "value": 0.15,
3229 "unit": "ratio",
3230 },
3231 "score.weight.fid": {
3232 "value": 0.3,
3233 "unit": "ratio",
3234 },
3235 "score.weight.lcp": {
3236 "value": 0.3,
3237 "unit": "ratio",
3238 },
3239 "score.weight.ttfb": {
3240 "value": 0.0,
3241 "unit": "ratio",
3242 },
3243 },
3244 }
3245 "###);
3246 }
3247
3248 #[test]
3249 fn test_computed_performance_score_missing_measurement() {
3250 let json = r#"
3251 {
3252 "type": "transaction",
3253 "timestamp": "2021-04-26T08:00:05+0100",
3254 "start_timestamp": "2021-04-26T08:00:00+0100",
3255 "measurements": {
3256 "a": {"value": 213, "unit": "millisecond"}
3257 },
3258 "contexts": {
3259 "browser": {
3260 "name": "Chrome",
3261 "version": "120.1.1",
3262 "type": "browser"
3263 }
3264 }
3265 }
3266 "#;
3267
3268 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3269
3270 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3271 "profiles": [
3272 {
3273 "name": "Desktop",
3274 "scoreComponents": [
3275 {
3276 "measurement": "a",
3277 "weight": 0.15,
3278 "p10": 900,
3279 "p50": 1600
3280 },
3281 {
3282 "measurement": "b",
3283 "weight": 0.30,
3284 "p10": 1200,
3285 "p50": 2400
3286 },
3287 ],
3288 "condition": {
3289 "op":"eq",
3290 "name": "event.contexts.browser.name",
3291 "value": "Chrome"
3292 }
3293 }
3294 ]
3295 }))
3296 .unwrap();
3297
3298 normalize_performance_score(&mut event, Some(&performance_score));
3299
3300 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3301 {
3302 "type": "transaction",
3303 "timestamp": 1619420405.0,
3304 "start_timestamp": 1619420400.0,
3305 "contexts": {
3306 "browser": {
3307 "name": "Chrome",
3308 "version": "120.1.1",
3309 "type": "browser",
3310 },
3311 },
3312 "measurements": {
3313 "a": {
3314 "value": 213.0,
3315 "unit": "millisecond",
3316 },
3317 },
3318 }
3319 "###);
3320 }
3321
3322 #[test]
3323 fn test_computed_performance_score_optional_measurement() {
3324 let json = r#"
3325 {
3326 "type": "transaction",
3327 "timestamp": "2021-04-26T08:00:05+0100",
3328 "start_timestamp": "2021-04-26T08:00:00+0100",
3329 "measurements": {
3330 "a": {"value": 213, "unit": "millisecond"},
3331 "b": {"value": 213, "unit": "millisecond"}
3332 },
3333 "contexts": {
3334 "browser": {
3335 "name": "Chrome",
3336 "version": "120.1.1",
3337 "type": "browser"
3338 }
3339 }
3340 }
3341 "#;
3342
3343 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3344
3345 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3346 "profiles": [
3347 {
3348 "name": "Desktop",
3349 "scoreComponents": [
3350 {
3351 "measurement": "a",
3352 "weight": 0.15,
3353 "p10": 900,
3354 "p50": 1600,
3355 },
3356 {
3357 "measurement": "b",
3358 "weight": 0.30,
3359 "p10": 1200,
3360 "p50": 2400,
3361 "optional": true
3362 },
3363 {
3364 "measurement": "c",
3365 "weight": 0.55,
3366 "p10": 1200,
3367 "p50": 2400,
3368 "optional": true
3369 },
3370 ],
3371 "condition": {
3372 "op":"eq",
3373 "name": "event.contexts.browser.name",
3374 "value": "Chrome"
3375 }
3376 }
3377 ]
3378 }))
3379 .unwrap();
3380
3381 normalize_performance_score(&mut event, Some(&performance_score));
3382
3383 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3384 {
3385 "type": "transaction",
3386 "timestamp": 1619420405.0,
3387 "start_timestamp": 1619420400.0,
3388 "contexts": {
3389 "browser": {
3390 "name": "Chrome",
3391 "version": "120.1.1",
3392 "type": "browser",
3393 },
3394 },
3395 "measurements": {
3396 "a": {
3397 "value": 213.0,
3398 "unit": "millisecond",
3399 },
3400 "b": {
3401 "value": 213.0,
3402 "unit": "millisecond",
3403 },
3404 "score.a": {
3405 "value": 0.33333215313291975,
3406 "unit": "ratio",
3407 },
3408 "score.b": {
3409 "value": 0.66666415149198,
3410 "unit": "ratio",
3411 },
3412 "score.ratio.a": {
3413 "value": 0.9999964593987591,
3414 "unit": "ratio",
3415 },
3416 "score.ratio.b": {
3417 "value": 0.9999962272379699,
3418 "unit": "ratio",
3419 },
3420 "score.total": {
3421 "value": 0.9999963046248997,
3422 "unit": "ratio",
3423 },
3424 "score.weight.a": {
3425 "value": 0.33333333333333337,
3426 "unit": "ratio",
3427 },
3428 "score.weight.b": {
3429 "value": 0.6666666666666667,
3430 "unit": "ratio",
3431 },
3432 "score.weight.c": {
3433 "value": 0.0,
3434 "unit": "ratio",
3435 },
3436 },
3437 }
3438 "###);
3439 }
3440
3441 #[test]
3442 fn test_computed_performance_score_weight_0() {
3443 let json = r#"
3444 {
3445 "type": "transaction",
3446 "timestamp": "2021-04-26T08:00:05+0100",
3447 "start_timestamp": "2021-04-26T08:00:00+0100",
3448 "measurements": {
3449 "cls": {"value": 0.11}
3450 }
3451 }
3452 "#;
3453
3454 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3455
3456 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3457 "profiles": [
3458 {
3459 "name": "Desktop",
3460 "scoreComponents": [
3461 {
3462 "measurement": "cls",
3463 "weight": 0,
3464 "p10": 0.1,
3465 "p50": 0.25
3466 },
3467 ],
3468 "condition": {
3469 "op":"and",
3470 "inner": []
3471 }
3472 }
3473 ]
3474 }))
3475 .unwrap();
3476
3477 normalize_performance_score(&mut event, Some(&performance_score));
3478
3479 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3480 {
3481 "type": "transaction",
3482 "timestamp": 1619420405.0,
3483 "start_timestamp": 1619420400.0,
3484 "measurements": {
3485 "cls": {
3486 "value": 0.11,
3487 },
3488 },
3489 }
3490 "###);
3491 }
3492
3493 #[test]
3494 fn test_computed_performance_score_negative_value() {
3495 let json = r#"
3496 {
3497 "type": "transaction",
3498 "timestamp": "2021-04-26T08:00:05+0100",
3499 "start_timestamp": "2021-04-26T08:00:00+0100",
3500 "measurements": {
3501 "ttfb": {"value": -100, "unit": "millisecond"}
3502 }
3503 }
3504 "#;
3505
3506 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3507
3508 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3509 "profiles": [
3510 {
3511 "name": "Desktop",
3512 "scoreComponents": [
3513 {
3514 "measurement": "ttfb",
3515 "weight": 1.0,
3516 "p10": 100.0,
3517 "p50": 250.0
3518 },
3519 ],
3520 "condition": {
3521 "op":"and",
3522 "inner": []
3523 }
3524 }
3525 ]
3526 }))
3527 .unwrap();
3528
3529 normalize_performance_score(&mut event, Some(&performance_score));
3530
3531 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3532 {
3533 "type": "transaction",
3534 "timestamp": 1619420405.0,
3535 "start_timestamp": 1619420400.0,
3536 "measurements": {
3537 "score.ratio.ttfb": {
3538 "value": 1.0,
3539 "unit": "ratio",
3540 },
3541 "score.total": {
3542 "value": 1.0,
3543 "unit": "ratio",
3544 },
3545 "score.ttfb": {
3546 "value": 1.0,
3547 "unit": "ratio",
3548 },
3549 "score.weight.ttfb": {
3550 "value": 1.0,
3551 "unit": "ratio",
3552 },
3553 "ttfb": {
3554 "value": -100.0,
3555 "unit": "millisecond",
3556 },
3557 },
3558 }
3559 "###);
3560 }
3561
3562 #[test]
3563 fn test_filter_negative_web_vital_measurements() {
3564 let json = r#"
3565 {
3566 "type": "transaction",
3567 "timestamp": "2021-04-26T08:00:05+0100",
3568 "start_timestamp": "2021-04-26T08:00:00+0100",
3569 "measurements": {
3570 "ttfb": {"value": -100, "unit": "millisecond"}
3571 }
3572 }
3573 "#;
3574 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3575
3576 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3578 "builtinMeasurements": [
3579 {"name": "ttfb", "unit": "millisecond"},
3580 ],
3581 }))
3582 .unwrap();
3583
3584 let dynamic_measurement_config =
3585 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3586
3587 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3588
3589 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3590 {
3591 "type": "transaction",
3592 "timestamp": 1619420405.0,
3593 "start_timestamp": 1619420400.0,
3594 "measurements": {},
3595 "_meta": {
3596 "measurements": {
3597 "": Meta(Some(MetaInner(
3598 err: [
3599 [
3600 "invalid_data",
3601 {
3602 "reason": "Negative value for measurement ttfb not allowed: -100",
3603 },
3604 ],
3605 ],
3606 val: Some({
3607 "ttfb": {
3608 "unit": "millisecond",
3609 "value": -100.0,
3610 },
3611 }),
3612 ))),
3613 },
3614 },
3615 }
3616 "###);
3617 }
3618
3619 #[test]
3620 fn test_computed_performance_score_multiple_profiles() {
3621 let json = r#"
3622 {
3623 "type": "transaction",
3624 "timestamp": "2021-04-26T08:00:05+0100",
3625 "start_timestamp": "2021-04-26T08:00:00+0100",
3626 "measurements": {
3627 "cls": {"value": 0.11},
3628 "inp": {"value": 120.0}
3629 }
3630 }
3631 "#;
3632
3633 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3634
3635 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3636 "profiles": [
3637 {
3638 "name": "Desktop",
3639 "scoreComponents": [
3640 {
3641 "measurement": "cls",
3642 "weight": 0,
3643 "p10": 0.1,
3644 "p50": 0.25
3645 },
3646 ],
3647 "condition": {
3648 "op":"and",
3649 "inner": []
3650 }
3651 },
3652 {
3653 "name": "Desktop",
3654 "scoreComponents": [
3655 {
3656 "measurement": "inp",
3657 "weight": 1.0,
3658 "p10": 0.1,
3659 "p50": 0.25
3660 },
3661 ],
3662 "condition": {
3663 "op":"and",
3664 "inner": []
3665 }
3666 }
3667 ]
3668 }))
3669 .unwrap();
3670
3671 normalize_performance_score(&mut event, Some(&performance_score));
3672
3673 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3674 {
3675 "type": "transaction",
3676 "timestamp": 1619420405.0,
3677 "start_timestamp": 1619420400.0,
3678 "measurements": {
3679 "cls": {
3680 "value": 0.11,
3681 },
3682 "inp": {
3683 "value": 120.0,
3684 },
3685 "score.inp": {
3686 "value": 0.0,
3687 "unit": "ratio",
3688 },
3689 "score.ratio.inp": {
3690 "value": 0.0,
3691 "unit": "ratio",
3692 },
3693 "score.total": {
3694 "value": 0.0,
3695 "unit": "ratio",
3696 },
3697 "score.weight.inp": {
3698 "value": 1.0,
3699 "unit": "ratio",
3700 },
3701 },
3702 }
3703 "###);
3704 }
3705
3706 #[test]
3707 fn test_compute_performance_score_for_mobile_ios_profile() {
3708 let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3709 .unwrap()
3710 .0
3711 .unwrap();
3712
3713 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3714 "profiles": [
3715 {
3716 "name": "Mobile",
3717 "scoreComponents": [
3718 {
3719 "measurement": "time_to_initial_display",
3720 "weight": 0.25,
3721 "p10": 1800.0,
3722 "p50": 3000.0,
3723 "optional": true
3724 },
3725 {
3726 "measurement": "time_to_full_display",
3727 "weight": 0.25,
3728 "p10": 2500.0,
3729 "p50": 4000.0,
3730 "optional": true
3731 },
3732 {
3733 "measurement": "app_start_warm",
3734 "weight": 0.25,
3735 "p10": 200.0,
3736 "p50": 500.0,
3737 "optional": true
3738 },
3739 {
3740 "measurement": "app_start_cold",
3741 "weight": 0.25,
3742 "p10": 200.0,
3743 "p50": 500.0,
3744 "optional": true
3745 }
3746 ],
3747 "condition": {
3748 "op": "and",
3749 "inner": [
3750 {
3751 "op": "or",
3752 "inner": [
3753 {
3754 "op": "eq",
3755 "name": "event.sdk.name",
3756 "value": "sentry.cocoa"
3757 },
3758 {
3759 "op": "eq",
3760 "name": "event.sdk.name",
3761 "value": "sentry.java.android"
3762 }
3763 ]
3764 },
3765 {
3766 "op": "eq",
3767 "name": "event.contexts.trace.op",
3768 "value": "ui.load"
3769 }
3770 ]
3771 }
3772 }
3773 ]
3774 }))
3775 .unwrap();
3776
3777 normalize_performance_score(&mut event, Some(&performance_score));
3778
3779 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3780 }
3781
3782 #[test]
3783 fn test_compute_performance_score_for_mobile_android_profile() {
3784 let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3785 .unwrap()
3786 .0
3787 .unwrap();
3788
3789 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3790 "profiles": [
3791 {
3792 "name": "Mobile",
3793 "scoreComponents": [
3794 {
3795 "measurement": "time_to_initial_display",
3796 "weight": 0.25,
3797 "p10": 1800.0,
3798 "p50": 3000.0,
3799 "optional": true
3800 },
3801 {
3802 "measurement": "time_to_full_display",
3803 "weight": 0.25,
3804 "p10": 2500.0,
3805 "p50": 4000.0,
3806 "optional": true
3807 },
3808 {
3809 "measurement": "app_start_warm",
3810 "weight": 0.25,
3811 "p10": 200.0,
3812 "p50": 500.0,
3813 "optional": true
3814 },
3815 {
3816 "measurement": "app_start_cold",
3817 "weight": 0.25,
3818 "p10": 200.0,
3819 "p50": 500.0,
3820 "optional": true
3821 }
3822 ],
3823 "condition": {
3824 "op": "and",
3825 "inner": [
3826 {
3827 "op": "or",
3828 "inner": [
3829 {
3830 "op": "eq",
3831 "name": "event.sdk.name",
3832 "value": "sentry.cocoa"
3833 },
3834 {
3835 "op": "eq",
3836 "name": "event.sdk.name",
3837 "value": "sentry.java.android"
3838 }
3839 ]
3840 },
3841 {
3842 "op": "eq",
3843 "name": "event.contexts.trace.op",
3844 "value": "ui.load"
3845 }
3846 ]
3847 }
3848 }
3849 ]
3850 }))
3851 .unwrap();
3852
3853 normalize_performance_score(&mut event, Some(&performance_score));
3854
3855 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3856 }
3857
3858 #[test]
3859 fn test_computes_performance_score_and_tags_with_profile_version() {
3860 let json = r#"
3861 {
3862 "type": "transaction",
3863 "timestamp": "2021-04-26T08:00:05+0100",
3864 "start_timestamp": "2021-04-26T08:00:00+0100",
3865 "measurements": {
3866 "inp": {"value": 120.0}
3867 }
3868 }
3869 "#;
3870
3871 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3872
3873 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3874 "profiles": [
3875 {
3876 "name": "Desktop",
3877 "scoreComponents": [
3878 {
3879 "measurement": "inp",
3880 "weight": 1.0,
3881 "p10": 0.1,
3882 "p50": 0.25
3883 },
3884 ],
3885 "condition": {
3886 "op":"and",
3887 "inner": []
3888 },
3889 "version": "beta"
3890 }
3891 ]
3892 }))
3893 .unwrap();
3894
3895 normalize(
3896 &mut event,
3897 &mut Meta::default(),
3898 &NormalizationConfig {
3899 performance_score: Some(&performance_score),
3900 ..Default::default()
3901 },
3902 );
3903
3904 insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3905 {
3906 "performance_score": {
3907 "score_profile_version": "beta",
3908 "type": "performancescore",
3909 },
3910 }
3911 "###);
3912 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3913 {
3914 "inp": {
3915 "value": 120.0,
3916 "unit": "millisecond",
3917 },
3918 "score.inp": {
3919 "value": 0.0,
3920 "unit": "ratio",
3921 },
3922 "score.ratio.inp": {
3923 "value": 0.0,
3924 "unit": "ratio",
3925 },
3926 "score.total": {
3927 "value": 0.0,
3928 "unit": "ratio",
3929 },
3930 "score.weight.inp": {
3931 "value": 1.0,
3932 "unit": "ratio",
3933 },
3934 }
3935 "###);
3936 }
3937
3938 #[test]
3939 fn test_computes_standalone_cls_performance_score() {
3940 let json = r#"
3941 {
3942 "type": "transaction",
3943 "timestamp": "2021-04-26T08:00:05+0100",
3944 "start_timestamp": "2021-04-26T08:00:00+0100",
3945 "measurements": {
3946 "cls": {"value": 0.5}
3947 }
3948 }
3949 "#;
3950
3951 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3952
3953 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3954 "profiles": [
3955 {
3956 "name": "Default",
3957 "scoreComponents": [
3958 {
3959 "measurement": "fcp",
3960 "weight": 0.15,
3961 "p10": 900.0,
3962 "p50": 1600.0,
3963 "optional": true,
3964 },
3965 {
3966 "measurement": "lcp",
3967 "weight": 0.30,
3968 "p10": 1200.0,
3969 "p50": 2400.0,
3970 "optional": true,
3971 },
3972 {
3973 "measurement": "cls",
3974 "weight": 0.15,
3975 "p10": 0.1,
3976 "p50": 0.25,
3977 "optional": true,
3978 },
3979 {
3980 "measurement": "ttfb",
3981 "weight": 0.10,
3982 "p10": 200.0,
3983 "p50": 400.0,
3984 "optional": true,
3985 },
3986 ],
3987 "condition": {
3988 "op": "and",
3989 "inner": [],
3990 },
3991 }
3992 ]
3993 }))
3994 .unwrap();
3995
3996 normalize(
3997 &mut event,
3998 &mut Meta::default(),
3999 &NormalizationConfig {
4000 performance_score: Some(&performance_score),
4001 ..Default::default()
4002 },
4003 );
4004
4005 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4006 {
4007 "cls": {
4008 "value": 0.5,
4009 "unit": "none",
4010 },
4011 "score.cls": {
4012 "value": 0.16615877613713903,
4013 "unit": "ratio",
4014 },
4015 "score.ratio.cls": {
4016 "value": 0.16615877613713903,
4017 "unit": "ratio",
4018 },
4019 "score.total": {
4020 "value": 0.16615877613713903,
4021 "unit": "ratio",
4022 },
4023 "score.weight.cls": {
4024 "value": 1.0,
4025 "unit": "ratio",
4026 },
4027 "score.weight.fcp": {
4028 "value": 0.0,
4029 "unit": "ratio",
4030 },
4031 "score.weight.lcp": {
4032 "value": 0.0,
4033 "unit": "ratio",
4034 },
4035 "score.weight.ttfb": {
4036 "value": 0.0,
4037 "unit": "ratio",
4038 },
4039 }
4040 "###);
4041 }
4042
4043 #[test]
4044 fn test_computes_standalone_lcp_performance_score() {
4045 let json = r#"
4046 {
4047 "type": "transaction",
4048 "timestamp": "2021-04-26T08:00:05+0100",
4049 "start_timestamp": "2021-04-26T08:00:00+0100",
4050 "measurements": {
4051 "lcp": {"value": 1200.0}
4052 }
4053 }
4054 "#;
4055
4056 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4057
4058 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4059 "profiles": [
4060 {
4061 "name": "Default",
4062 "scoreComponents": [
4063 {
4064 "measurement": "fcp",
4065 "weight": 0.15,
4066 "p10": 900.0,
4067 "p50": 1600.0,
4068 "optional": true,
4069 },
4070 {
4071 "measurement": "lcp",
4072 "weight": 0.30,
4073 "p10": 1200.0,
4074 "p50": 2400.0,
4075 "optional": true,
4076 },
4077 {
4078 "measurement": "cls",
4079 "weight": 0.15,
4080 "p10": 0.1,
4081 "p50": 0.25,
4082 "optional": true,
4083 },
4084 {
4085 "measurement": "ttfb",
4086 "weight": 0.10,
4087 "p10": 200.0,
4088 "p50": 400.0,
4089 "optional": true,
4090 },
4091 ],
4092 "condition": {
4093 "op": "and",
4094 "inner": [],
4095 },
4096 }
4097 ]
4098 }))
4099 .unwrap();
4100
4101 normalize(
4102 &mut event,
4103 &mut Meta::default(),
4104 &NormalizationConfig {
4105 performance_score: Some(&performance_score),
4106 ..Default::default()
4107 },
4108 );
4109
4110 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4111 {
4112 "lcp": {
4113 "value": 1200.0,
4114 "unit": "millisecond",
4115 },
4116 "score.lcp": {
4117 "value": 0.8999999314038525,
4118 "unit": "ratio",
4119 },
4120 "score.ratio.lcp": {
4121 "value": 0.8999999314038525,
4122 "unit": "ratio",
4123 },
4124 "score.total": {
4125 "value": 0.8999999314038525,
4126 "unit": "ratio",
4127 },
4128 "score.weight.cls": {
4129 "value": 0.0,
4130 "unit": "ratio",
4131 },
4132 "score.weight.fcp": {
4133 "value": 0.0,
4134 "unit": "ratio",
4135 },
4136 "score.weight.lcp": {
4137 "value": 1.0,
4138 "unit": "ratio",
4139 },
4140 "score.weight.ttfb": {
4141 "value": 0.0,
4142 "unit": "ratio",
4143 },
4144 }
4145 "###);
4146 }
4147
4148 #[test]
4149 fn test_computed_performance_score_uses_first_matching_profile() {
4150 let json = r#"
4151 {
4152 "type": "transaction",
4153 "timestamp": "2021-04-26T08:00:05+0100",
4154 "start_timestamp": "2021-04-26T08:00:00+0100",
4155 "measurements": {
4156 "a": {"value": 213, "unit": "millisecond"},
4157 "b": {"value": 213, "unit": "millisecond"}
4158 },
4159 "contexts": {
4160 "browser": {
4161 "name": "Chrome",
4162 "version": "120.1.1",
4163 "type": "browser"
4164 }
4165 }
4166 }
4167 "#;
4168
4169 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4170
4171 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4172 "profiles": [
4173 {
4174 "name": "Mobile",
4175 "scoreComponents": [
4176 {
4177 "measurement": "a",
4178 "weight": 0.15,
4179 "p10": 100,
4180 "p50": 200,
4181 },
4182 {
4183 "measurement": "b",
4184 "weight": 0.30,
4185 "p10": 100,
4186 "p50": 200,
4187 "optional": true
4188 },
4189 {
4190 "measurement": "c",
4191 "weight": 0.55,
4192 "p10": 100,
4193 "p50": 200,
4194 "optional": true
4195 },
4196 ],
4197 "condition": {
4198 "op":"eq",
4199 "name": "event.contexts.browser.name",
4200 "value": "Chrome Mobile"
4201 }
4202 },
4203 {
4204 "name": "Desktop",
4205 "scoreComponents": [
4206 {
4207 "measurement": "a",
4208 "weight": 0.15,
4209 "p10": 900,
4210 "p50": 1600,
4211 },
4212 {
4213 "measurement": "b",
4214 "weight": 0.30,
4215 "p10": 1200,
4216 "p50": 2400,
4217 "optional": true
4218 },
4219 {
4220 "measurement": "c",
4221 "weight": 0.55,
4222 "p10": 1200,
4223 "p50": 2400,
4224 "optional": true
4225 },
4226 ],
4227 "condition": {
4228 "op":"eq",
4229 "name": "event.contexts.browser.name",
4230 "value": "Chrome"
4231 }
4232 },
4233 {
4234 "name": "Default",
4235 "scoreComponents": [
4236 {
4237 "measurement": "a",
4238 "weight": 0.15,
4239 "p10": 100,
4240 "p50": 200,
4241 },
4242 {
4243 "measurement": "b",
4244 "weight": 0.30,
4245 "p10": 100,
4246 "p50": 200,
4247 "optional": true
4248 },
4249 {
4250 "measurement": "c",
4251 "weight": 0.55,
4252 "p10": 100,
4253 "p50": 200,
4254 "optional": true
4255 },
4256 ],
4257 "condition": {
4258 "op": "and",
4259 "inner": [],
4260 }
4261 }
4262 ]
4263 }))
4264 .unwrap();
4265
4266 normalize_performance_score(&mut event, Some(&performance_score));
4267
4268 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4269 {
4270 "type": "transaction",
4271 "timestamp": 1619420405.0,
4272 "start_timestamp": 1619420400.0,
4273 "contexts": {
4274 "browser": {
4275 "name": "Chrome",
4276 "version": "120.1.1",
4277 "type": "browser",
4278 },
4279 },
4280 "measurements": {
4281 "a": {
4282 "value": 213.0,
4283 "unit": "millisecond",
4284 },
4285 "b": {
4286 "value": 213.0,
4287 "unit": "millisecond",
4288 },
4289 "score.a": {
4290 "value": 0.33333215313291975,
4291 "unit": "ratio",
4292 },
4293 "score.b": {
4294 "value": 0.66666415149198,
4295 "unit": "ratio",
4296 },
4297 "score.ratio.a": {
4298 "value": 0.9999964593987591,
4299 "unit": "ratio",
4300 },
4301 "score.ratio.b": {
4302 "value": 0.9999962272379699,
4303 "unit": "ratio",
4304 },
4305 "score.total": {
4306 "value": 0.9999963046248997,
4307 "unit": "ratio",
4308 },
4309 "score.weight.a": {
4310 "value": 0.33333333333333337,
4311 "unit": "ratio",
4312 },
4313 "score.weight.b": {
4314 "value": 0.6666666666666667,
4315 "unit": "ratio",
4316 },
4317 "score.weight.c": {
4318 "value": 0.0,
4319 "unit": "ratio",
4320 },
4321 },
4322 }
4323 "###);
4324 }
4325
4326 #[test]
4327 fn test_computed_performance_score_falls_back_to_default_profile() {
4328 let json = r#"
4329 {
4330 "type": "transaction",
4331 "timestamp": "2021-04-26T08:00:05+0100",
4332 "start_timestamp": "2021-04-26T08:00:00+0100",
4333 "measurements": {
4334 "a": {"value": 213, "unit": "millisecond"},
4335 "b": {"value": 213, "unit": "millisecond"}
4336 },
4337 "contexts": {}
4338 }
4339 "#;
4340
4341 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4342
4343 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4344 "profiles": [
4345 {
4346 "name": "Mobile",
4347 "scoreComponents": [
4348 {
4349 "measurement": "a",
4350 "weight": 0.15,
4351 "p10": 900,
4352 "p50": 1600,
4353 "optional": true
4354 },
4355 {
4356 "measurement": "b",
4357 "weight": 0.30,
4358 "p10": 1200,
4359 "p50": 2400,
4360 "optional": true
4361 },
4362 {
4363 "measurement": "c",
4364 "weight": 0.55,
4365 "p10": 1200,
4366 "p50": 2400,
4367 "optional": true
4368 },
4369 ],
4370 "condition": {
4371 "op":"eq",
4372 "name": "event.contexts.browser.name",
4373 "value": "Chrome Mobile"
4374 }
4375 },
4376 {
4377 "name": "Desktop",
4378 "scoreComponents": [
4379 {
4380 "measurement": "a",
4381 "weight": 0.15,
4382 "p10": 900,
4383 "p50": 1600,
4384 "optional": true
4385 },
4386 {
4387 "measurement": "b",
4388 "weight": 0.30,
4389 "p10": 1200,
4390 "p50": 2400,
4391 "optional": true
4392 },
4393 {
4394 "measurement": "c",
4395 "weight": 0.55,
4396 "p10": 1200,
4397 "p50": 2400,
4398 "optional": true
4399 },
4400 ],
4401 "condition": {
4402 "op":"eq",
4403 "name": "event.contexts.browser.name",
4404 "value": "Chrome"
4405 }
4406 },
4407 {
4408 "name": "Default",
4409 "scoreComponents": [
4410 {
4411 "measurement": "a",
4412 "weight": 0.15,
4413 "p10": 100,
4414 "p50": 200,
4415 "optional": true
4416 },
4417 {
4418 "measurement": "b",
4419 "weight": 0.30,
4420 "p10": 100,
4421 "p50": 200,
4422 "optional": true
4423 },
4424 {
4425 "measurement": "c",
4426 "weight": 0.55,
4427 "p10": 100,
4428 "p50": 200,
4429 "optional": true
4430 },
4431 ],
4432 "condition": {
4433 "op": "and",
4434 "inner": [],
4435 }
4436 }
4437 ]
4438 }))
4439 .unwrap();
4440
4441 normalize_performance_score(&mut event, Some(&performance_score));
4442
4443 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4444 {
4445 "type": "transaction",
4446 "timestamp": 1619420405.0,
4447 "start_timestamp": 1619420400.0,
4448 "contexts": {},
4449 "measurements": {
4450 "a": {
4451 "value": 213.0,
4452 "unit": "millisecond",
4453 },
4454 "b": {
4455 "value": 213.0,
4456 "unit": "millisecond",
4457 },
4458 "score.a": {
4459 "value": 0.15121816827413334,
4460 "unit": "ratio",
4461 },
4462 "score.b": {
4463 "value": 0.3024363365482667,
4464 "unit": "ratio",
4465 },
4466 "score.ratio.a": {
4467 "value": 0.45365450482239994,
4468 "unit": "ratio",
4469 },
4470 "score.ratio.b": {
4471 "value": 0.45365450482239994,
4472 "unit": "ratio",
4473 },
4474 "score.total": {
4475 "value": 0.4536545048224,
4476 "unit": "ratio",
4477 },
4478 "score.weight.a": {
4479 "value": 0.33333333333333337,
4480 "unit": "ratio",
4481 },
4482 "score.weight.b": {
4483 "value": 0.6666666666666667,
4484 "unit": "ratio",
4485 },
4486 "score.weight.c": {
4487 "value": 0.0,
4488 "unit": "ratio",
4489 },
4490 },
4491 }
4492 "###);
4493 }
4494
4495 #[test]
4496 fn test_normalization_removes_reprocessing_context() {
4497 let json = r#"{
4498 "contexts": {
4499 "reprocessing": {}
4500 }
4501 }"#;
4502 let mut event = Annotated::<Event>::from_json(json).unwrap();
4503 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4504 normalize_event(&mut event, &NormalizationConfig::default());
4505 assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4506 }
4507
4508 #[test]
4509 fn test_renormalization_does_not_remove_reprocessing_context() {
4510 let json = r#"{
4511 "contexts": {
4512 "reprocessing": {}
4513 }
4514 }"#;
4515 let mut event = Annotated::<Event>::from_json(json).unwrap();
4516 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4517 normalize_event(
4518 &mut event,
4519 &NormalizationConfig {
4520 is_renormalize: true,
4521 ..Default::default()
4522 },
4523 );
4524 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4525 }
4526
4527 #[test]
4528 fn test_normalize_user() {
4529 let json = r#"{
4530 "user": {
4531 "id": "123456",
4532 "username": "john",
4533 "other": "value"
4534 }
4535 }"#;
4536 let mut event = Annotated::<Event>::from_json(json).unwrap();
4537 normalize_user(event.value_mut().as_mut().unwrap());
4538
4539 let user = event.value().unwrap().user.value().unwrap();
4540 assert_eq!(user.data, {
4541 let mut map = Object::new();
4542 map.insert(
4543 "other".to_owned(),
4544 Annotated::new(Value::String("value".to_owned())),
4545 );
4546 Annotated::new(map)
4547 });
4548 assert_eq!(user.other, Object::new());
4549 assert_eq!(user.username, Annotated::new("john".to_owned().into()));
4550 assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
4551 }
4552
4553 #[test]
4554 fn test_handle_types_in_spaced_exception_values() {
4555 let mut exception = Annotated::new(Exception {
4556 value: Annotated::new("ValueError: unauthorized".to_owned().into()),
4557 ..Exception::default()
4558 });
4559 normalize_exception(&mut exception);
4560
4561 let exception = exception.value().unwrap();
4562 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4563 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4564 }
4565
4566 #[test]
4567 fn test_handle_types_in_non_spaced_excepton_values() {
4568 let mut exception = Annotated::new(Exception {
4569 value: Annotated::new("ValueError:unauthorized".to_owned().into()),
4570 ..Exception::default()
4571 });
4572 normalize_exception(&mut exception);
4573
4574 let exception = exception.value().unwrap();
4575 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4576 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4577 }
4578
4579 #[test]
4580 fn test_rejects_empty_exception_fields() {
4581 let mut exception = Annotated::new(Exception {
4582 value: Annotated::new("".to_owned().into()),
4583 ty: Annotated::new("".to_owned()),
4584 ..Default::default()
4585 });
4586
4587 normalize_exception(&mut exception);
4588
4589 assert!(exception.value().is_none());
4590 assert!(exception.meta().has_errors());
4591 }
4592
4593 #[test]
4594 fn test_json_value() {
4595 let mut exception = Annotated::new(Exception {
4596 value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
4597 ..Exception::default()
4598 });
4599
4600 normalize_exception(&mut exception);
4601
4602 let exception = exception.value().unwrap();
4603
4604 assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4606 assert_eq!(exception.ty.value(), None);
4607 }
4608
4609 #[test]
4610 fn test_exception_invalid() {
4611 let mut exception = Annotated::new(Exception::default());
4612
4613 normalize_exception(&mut exception);
4614
4615 let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4616 error.insert("attribute", "type or value");
4617 });
4618 assert_eq!(
4619 exception.meta().iter_errors().collect_tuple(),
4620 Some((&expected,))
4621 );
4622 }
4623
4624 #[test]
4625 fn test_normalize_exception() {
4626 let mut event = Annotated::new(Event {
4627 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4628 ty: Annotated::empty(),
4630 value: Annotated::empty(),
4631 ..Default::default()
4632 })])),
4633 ..Default::default()
4634 });
4635
4636 normalize_event(&mut event, &NormalizationConfig::default());
4637
4638 let exception = event
4639 .value()
4640 .unwrap()
4641 .exceptions
4642 .value()
4643 .unwrap()
4644 .values
4645 .value()
4646 .unwrap()
4647 .first()
4648 .unwrap();
4649
4650 assert_debug_snapshot!(exception.meta(), @r###"
4651 Meta {
4652 remarks: [],
4653 errors: [
4654 Error {
4655 kind: MissingAttribute,
4656 data: {
4657 "attribute": String(
4658 "type or value",
4659 ),
4660 },
4661 },
4662 ],
4663 original_length: None,
4664 original_value: Some(
4665 Object(
4666 {
4667 "mechanism": ~,
4668 "module": ~,
4669 "raw_stacktrace": ~,
4670 "stacktrace": ~,
4671 "thread_id": ~,
4672 "type": ~,
4673 "value": ~,
4674 },
4675 ),
4676 ),
4677 }
4678 "###);
4679 }
4680
4681 #[test]
4682 fn test_normalize_breadcrumbs() {
4683 let mut event = Event {
4684 breadcrumbs: Annotated::new(Values {
4685 values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4686 ..Default::default()
4687 }),
4688 ..Default::default()
4689 };
4690 normalize_breadcrumbs(&mut event);
4691
4692 let breadcrumb = event
4693 .breadcrumbs
4694 .value()
4695 .unwrap()
4696 .values
4697 .value()
4698 .unwrap()
4699 .first()
4700 .unwrap()
4701 .value()
4702 .unwrap();
4703 assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4704 assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4705 }
4706
4707 #[test]
4708 fn test_other_debug_images_have_meta_errors() {
4709 let mut event = Event {
4710 debug_meta: Annotated::new(DebugMeta {
4711 images: Annotated::new(vec![Annotated::new(
4712 DebugImage::Other(BTreeMap::default()),
4713 )]),
4714 ..Default::default()
4715 }),
4716 ..Default::default()
4717 };
4718 normalize_debug_meta(&mut event);
4719
4720 let debug_image_meta = event
4721 .debug_meta
4722 .value()
4723 .unwrap()
4724 .images
4725 .value()
4726 .unwrap()
4727 .first()
4728 .unwrap()
4729 .meta();
4730 assert_debug_snapshot!(debug_image_meta, @r###"
4731 Meta {
4732 remarks: [],
4733 errors: [
4734 Error {
4735 kind: InvalidData,
4736 data: {
4737 "reason": String(
4738 "unsupported debug image type",
4739 ),
4740 },
4741 },
4742 ],
4743 original_length: None,
4744 original_value: Some(
4745 Object(
4746 {},
4747 ),
4748 ),
4749 }
4750 "###);
4751 }
4752
4753 #[test]
4754 fn test_skip_span_normalization_when_configured() {
4755 let json = r#"{
4756 "type": "transaction",
4757 "start_timestamp": 1,
4758 "timestamp": 2,
4759 "contexts": {
4760 "trace": {
4761 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4762 "span_id": "aaaaaaaaaaaaaaaa"
4763 }
4764 },
4765 "spans": [
4766 {
4767 "op": "db",
4768 "description": "SELECT * FROM table;",
4769 "start_timestamp": 1,
4770 "timestamp": 2,
4771 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4772 "span_id": "bbbbbbbbbbbbbbbb",
4773 "parent_span_id": "aaaaaaaaaaaaaaaa"
4774 }
4775 ]
4776 }"#;
4777
4778 let mut event = Annotated::<Event>::from_json(json).unwrap();
4779 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4780 normalize_event(
4781 &mut event,
4782 &NormalizationConfig {
4783 is_renormalize: true,
4784 ..Default::default()
4785 },
4786 );
4787 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4788 normalize_event(
4789 &mut event,
4790 &NormalizationConfig {
4791 is_renormalize: false,
4792 ..Default::default()
4793 },
4794 );
4795 assert!(get_value!(event.spans[0].exclusive_time).is_some());
4796 }
4797
4798 #[test]
4799 fn test_normalize_trace_context_tags_extracts_lcp_info() {
4800 let json = r#"{
4801 "type": "transaction",
4802 "start_timestamp": 1,
4803 "timestamp": 2,
4804 "contexts": {
4805 "trace": {
4806 "data": {
4807 "lcp.element": "body > div#app > div > h1#header",
4808 "lcp.size": 24827,
4809 "lcp.id": "header",
4810 "lcp.url": "http://example.com/image.jpg"
4811 }
4812 }
4813 },
4814 "measurements": {
4815 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4816 }
4817 }"#;
4818 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4819 normalize_trace_context_tags(&mut event);
4820 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4821 {
4822 "type": "transaction",
4823 "timestamp": 2.0,
4824 "start_timestamp": 1.0,
4825 "contexts": {
4826 "trace": {
4827 "data": {
4828 "lcp.element": "body > div#app > div > h1#header",
4829 "lcp.size": 24827,
4830 "lcp.id": "header",
4831 "lcp.url": "http://example.com/image.jpg",
4832 },
4833 "type": "trace",
4834 },
4835 },
4836 "tags": [
4837 [
4838 "lcp.element",
4839 "body > div#app > div > h1#header",
4840 ],
4841 [
4842 "lcp.size",
4843 "24827",
4844 ],
4845 [
4846 "lcp.id",
4847 "header",
4848 ],
4849 [
4850 "lcp.url",
4851 "http://example.com/image.jpg",
4852 ],
4853 ],
4854 "measurements": {
4855 "lcp": {
4856 "value": 146.20000000298023,
4857 "unit": "millisecond",
4858 },
4859 },
4860 }
4861 "###);
4862 }
4863
4864 #[test]
4865 fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4866 let json = r#"{
4867 "type": "transaction",
4868 "start_timestamp": 1,
4869 "timestamp": 2,
4870 "contexts": {
4871 "trace": {
4872 "data": {
4873 "lcp.element": "body > div#app > div > h1#id",
4874 "lcp.size": 33333,
4875 "lcp.id": "id",
4876 "lcp.url": "http://example.com/another-image.jpg"
4877 }
4878 }
4879 },
4880 "tags": {
4881 "lcp.element": "body > div#app > div > h1#header",
4882 "lcp.size": 24827,
4883 "lcp.id": "header",
4884 "lcp.url": "http://example.com/image.jpg"
4885 },
4886 "measurements": {
4887 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4888 }
4889 }"#;
4890 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4891 normalize_trace_context_tags(&mut event);
4892 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4893 {
4894 "type": "transaction",
4895 "timestamp": 2.0,
4896 "start_timestamp": 1.0,
4897 "contexts": {
4898 "trace": {
4899 "data": {
4900 "lcp.element": "body > div#app > div > h1#id",
4901 "lcp.size": 33333,
4902 "lcp.id": "id",
4903 "lcp.url": "http://example.com/another-image.jpg",
4904 },
4905 "type": "trace",
4906 },
4907 },
4908 "tags": [
4909 [
4910 "lcp.element",
4911 "body > div#app > div > h1#header",
4912 ],
4913 [
4914 "lcp.id",
4915 "header",
4916 ],
4917 [
4918 "lcp.size",
4919 "24827",
4920 ],
4921 [
4922 "lcp.url",
4923 "http://example.com/image.jpg",
4924 ],
4925 ],
4926 "measurements": {
4927 "lcp": {
4928 "value": 146.20000000298023,
4929 "unit": "millisecond",
4930 },
4931 },
4932 }
4933 "###);
4934 }
4935}