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_event_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::new().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_event_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 && let Some(replay_id) = replay_id
366 {
367 contexts.add(ReplayContext {
368 replay_id: Annotated::new(EventId(replay_id)),
369 other: Object::default(),
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 && let Some(ip) = inferred_ip {
476 let user = user.get_or_insert_with(User::default);
477 user.ip_address.set_value(Some(ip.to_owned()));
478 }
479
480 if infer_ip == AutoInferSetting::Legacy {
484 if let Some(http_ip) = remote_addr_ip {
485 let user = user.get_or_insert_with(User::default);
486 user.ip_address.value_mut().get_or_insert(http_ip);
487 } else if let Some(client_ip) = inferred_ip {
488 let user = user.get_or_insert_with(User::default);
489 if user.ip_address.value().is_none() {
491 let scrubbed_before = user
493 .ip_address
494 .meta()
495 .iter_remarks()
496 .any(|r| r.ty == RemarkType::Removed);
497 if !scrubbed_before {
498 if let Some("javascript") | Some("cocoa") | Some("objc") = platform {
500 user.ip_address = Annotated::new(client_ip.to_owned());
501 }
502 }
503 }
504 }
505 }
506}
507
508pub fn normalize_user_geoinfo(
510 geoip_lookup: &GeoIpLookup,
511 user: &mut Annotated<User>,
512 ip_addr: Option<&IpAddr>,
513) {
514 let user = user.value_mut().get_or_insert_with(User::default);
515 if user.geo.value().is_some() {
517 return;
518 }
519 if let Some(ip_address) = user
520 .ip_address
521 .value()
522 .filter(|ip| !ip.is_auto())
523 .or(ip_addr)
524 .and_then(|ip| ip.as_str().parse().ok())
525 && let Some(geo) = geoip_lookup.lookup(ip_address)
526 {
527 user.geo.set_value(Some(geo));
528 }
529}
530
531fn normalize_user(event: &mut Event) {
532 let Annotated(Some(user), _) = &mut event.user else {
533 return;
534 };
535
536 if !user.other.is_empty() {
537 let data = user.data.value_mut().get_or_insert_with(Object::new);
538 data.extend(std::mem::take(&mut user.other));
539 }
540
541 let event_user_tag = get_event_user_tag(user);
544 user.sentry_user.set_value(event_user_tag);
545}
546
547fn normalize_logentry(logentry: &mut Annotated<LogEntry>, _meta: &mut Meta) {
548 let _ = processor::apply(logentry, |logentry, meta| {
549 crate::logentry::normalize_logentry(logentry, meta)
550 });
551}
552
553fn normalize_debug_meta(event: &mut Event) {
555 let Annotated(Some(debug_meta), _) = &mut event.debug_meta else {
556 return;
557 };
558 let Annotated(Some(debug_images), _) = &mut debug_meta.images else {
559 return;
560 };
561
562 for annotated_image in debug_images {
563 let _ = processor::apply(annotated_image, |image, meta| match image {
564 DebugImage::Other(_) => {
565 meta.add_error(Error::invalid("unsupported debug image type"));
566 Err(ProcessingAction::DeleteValueSoft)
567 }
568 _ => Ok(()),
569 });
570 }
571}
572
573fn normalize_breadcrumbs(event: &mut Event) {
574 let Annotated(Some(breadcrumbs), _) = &mut event.breadcrumbs else {
575 return;
576 };
577 let Some(breadcrumbs) = breadcrumbs.values.value_mut() else {
578 return;
579 };
580
581 for annotated_breadcrumb in breadcrumbs {
582 let Annotated(Some(breadcrumb), _) = annotated_breadcrumb else {
583 continue;
584 };
585
586 if breadcrumb.ty.value().is_empty() {
587 breadcrumb.ty.set_value(Some("default".to_owned()));
588 }
589 if breadcrumb.level.value().is_none() {
590 breadcrumb.level.set_value(Some(Level::Info));
591 }
592 }
593}
594
595fn normalize_release_dist(event: &mut Event) {
597 normalize_dist(&mut event.dist);
598}
599
600fn normalize_dist(distribution: &mut Annotated<String>) {
601 let _ = processor::apply(distribution, |dist, meta| {
602 let trimmed = dist.trim();
603 if trimmed.is_empty() {
604 return Err(ProcessingAction::DeleteValueHard);
605 } else if bytecount::num_chars(trimmed.as_bytes()) > MaxChars::Distribution.limit() {
606 meta.add_error(Error::new(ErrorKind::ValueTooLong));
607 return Err(ProcessingAction::DeleteValueSoft);
608 } else if trimmed != dist {
609 *dist = trimmed.to_owned();
610 }
611 Ok(())
612 });
613}
614
615struct DedupCache(SmallVec<[u64; 16]>);
616
617impl DedupCache {
618 pub fn new() -> Self {
619 Self(SmallVec::default())
620 }
621
622 pub fn probe<H: Hash>(&mut self, element: H) -> bool {
623 let mut hasher = DefaultHasher::new();
624 element.hash(&mut hasher);
625 let hash = hasher.finish();
626
627 if self.0.contains(&hash) {
628 false
629 } else {
630 self.0.push(hash);
631 true
632 }
633 }
634}
635
636fn normalize_event_tags(event: &mut Event) {
638 let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
639 let environment = &mut event.environment;
640 if environment.is_empty() {
641 *environment = Annotated::empty();
642 }
643
644 if let Some(tag) = tags.remove("environment").and_then(Annotated::into_value) {
646 environment.get_or_insert_with(|| tag);
647 }
648
649 let mut tag_cache = DedupCache::new();
652 tags.retain(|entry| {
653 match entry.value() {
654 Some(tag) => match tag.key() {
655 Some("release") | Some("dist") | Some("user") | Some("filename")
656 | Some("function") => false,
657 name => tag_cache.probe(name),
658 },
659 None => true,
661 }
662 });
663
664 for tag in tags.iter_mut() {
665 let _ = processor::apply(tag, |tag, _| {
666 if let Some(key) = tag.key()
667 && key.is_empty()
668 {
669 tag.0 = Annotated::from_error(Error::nonempty(), None);
670 }
671
672 if let Some(value) = tag.value()
673 && value.is_empty()
674 {
675 tag.1 = Annotated::from_error(Error::nonempty(), None);
676 }
677
678 Ok(())
679 });
680 }
681
682 let server_name = std::mem::take(&mut event.server_name);
683 if server_name.value().is_some() {
684 let tag_name = "server_name".to_owned();
685 tags.insert(tag_name, server_name);
686 }
687
688 let site = std::mem::take(&mut event.site);
689 if site.value().is_some() {
690 let tag_name = "site".to_owned();
691 tags.insert(tag_name, site);
692 }
693}
694
695fn normalize_device_class(event: &mut Event) {
698 let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
699 let tag_name = "device.class".to_owned();
700 tags.remove("device.class");
702 if let Some(contexts) = event.contexts.value()
703 && let Some(device_class) = DeviceClass::from_contexts(contexts)
704 {
705 tags.insert(tag_name, Annotated::new(device_class.to_string()));
706 }
707}
708
709fn normalize_stacktraces(event: &mut Event) {
714 normalize_event_stacktrace(event);
715 normalize_exception_stacktraces(event);
716 normalize_thread_stacktraces(event);
717}
718
719fn normalize_event_stacktrace(event: &mut Event) {
721 let Annotated(Some(stacktrace), meta) = &mut event.stacktrace else {
722 return;
723 };
724 stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
725}
726
727fn normalize_exception_stacktraces(event: &mut Event) {
731 let Some(event_exception) = event.exceptions.value_mut() else {
732 return;
733 };
734 let Some(exceptions) = event_exception.values.value_mut() else {
735 return;
736 };
737 for annotated_exception in exceptions {
738 let Some(exception) = annotated_exception.value_mut() else {
739 continue;
740 };
741 if let Annotated(Some(stacktrace), meta) = &mut exception.stacktrace {
742 stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
743 }
744 }
745}
746
747fn normalize_thread_stacktraces(event: &mut Event) {
751 let Some(event_threads) = event.threads.value_mut() else {
752 return;
753 };
754 let Some(threads) = event_threads.values.value_mut() else {
755 return;
756 };
757 for annotated_thread in threads {
758 let Some(thread) = annotated_thread.value_mut() else {
759 continue;
760 };
761 if let Annotated(Some(stacktrace), meta) = &mut thread.stacktrace {
762 stacktrace::normalize_stacktrace(&mut stacktrace.0, meta);
763 }
764 }
765}
766
767fn normalize_exceptions(event: &mut Event) {
768 let os_hint = mechanism::OsHint::from_event(event);
769
770 if let Some(exception_values) = event.exceptions.value_mut()
771 && let Some(exceptions) = exception_values.values.value_mut()
772 {
773 if exceptions.len() == 1
774 && event.stacktrace.value().is_some()
775 && let Some(exception) = exceptions.get_mut(0)
776 && let Some(exception) = exception.value_mut()
777 {
778 mem::swap(&mut exception.stacktrace, &mut event.stacktrace);
779 event.stacktrace = Annotated::empty();
780 }
781
782 for exception in exceptions {
790 normalize_exception(exception);
791 if let Some(exception) = exception.value_mut()
792 && let Some(mechanism) = exception.mechanism.value_mut()
793 {
794 mechanism::normalize_mechanism(mechanism, os_hint);
795 }
796 }
797 }
798}
799
800fn normalize_exception(exception: &mut Annotated<Exception>) {
801 static TYPE_VALUE_RE: OnceLock<Regex> = OnceLock::new();
802 let regex = TYPE_VALUE_RE.get_or_init(|| Regex::new(r"^(\w+):(.*)$").unwrap());
803
804 let _ = processor::apply(exception, |exception, meta| {
805 if exception.ty.value().is_empty()
806 && let Some(value_str) = exception.value.value_mut()
807 {
808 let new_values = regex
809 .captures(value_str)
810 .map(|cap| (cap[1].to_string(), cap[2].trim().to_owned().into()));
811
812 if let Some((new_type, new_value)) = new_values {
813 exception.ty.set_value(Some(new_type));
814 *value_str = new_value;
815 }
816 }
817
818 if exception.ty.value().is_empty() && exception.value.value().is_empty() {
819 meta.add_error(Error::with(ErrorKind::MissingAttribute, |error| {
820 error.insert("attribute", "type or value");
821 }));
822 return Err(ProcessingAction::DeleteValueSoft);
823 }
824
825 Ok(())
826 });
827}
828
829fn normalize_user_agent(_event: &mut Event, normalize_user_agent: Option<bool>) {
830 if normalize_user_agent.unwrap_or(false) {
831 user_agent::normalize_user_agent(_event);
832 }
833}
834
835fn normalize_event_measurements(
837 event: &mut Event,
838 measurements_config: Option<CombinedMeasurementsConfig>,
839 max_mri_len: Option<usize>,
840) {
841 if event.ty.value() != Some(&EventType::Transaction) {
842 event.measurements = Annotated::empty();
844 } else if let Annotated(Some(ref mut measurements), ref mut meta) = event.measurements {
845 normalize_measurements(
846 measurements,
847 meta,
848 measurements_config,
849 max_mri_len,
850 event.start_timestamp.0,
851 event.timestamp.0,
852 );
853 }
854}
855
856pub fn normalize_measurements(
858 measurements: &mut Measurements,
859 meta: &mut Meta,
860 measurements_config: Option<CombinedMeasurementsConfig>,
861 max_mri_len: Option<usize>,
862 start_timestamp: Option<Timestamp>,
863 end_timestamp: Option<Timestamp>,
864) {
865 normalize_mobile_measurements(measurements);
866 normalize_units(measurements);
867
868 let duration_millis = start_timestamp.zip(end_timestamp).and_then(|(start, end)| {
869 FiniteF64::new(relay_common::time::chrono_to_positive_millis(end - start))
870 });
871
872 compute_measurements(duration_millis, measurements);
873 if let Some(measurements_config) = measurements_config {
874 remove_invalid_measurements(measurements, meta, measurements_config, max_mri_len);
875 }
876}
877
878pub trait MutMeasurements {
879 fn measurements(&mut self) -> &mut Annotated<Measurements>;
880}
881
882pub fn normalize_performance_score(
887 event: &mut (impl Getter + MutMeasurements),
888 performance_score: Option<&PerformanceScoreConfig>,
889) -> Option<String> {
890 let mut version = None;
891 let Some(performance_score) = performance_score else {
892 return version;
893 };
894 for profile in &performance_score.profiles {
895 if let Some(condition) = &profile.condition {
896 if !condition.matches(event) {
897 continue;
898 }
899 if let Some(measurements) = event.measurements().value_mut() {
900 let mut should_add_total = false;
901 if profile.score_components.iter().any(|c| {
902 !measurements.contains_key(c.measurement.as_str())
903 && c.weight.abs() >= f64::EPSILON
904 && !c.optional
905 }) {
906 continue;
910 }
911 let mut score_total = FiniteF64::ZERO;
912 let mut weight_total = FiniteF64::ZERO;
913 for component in &profile.score_components {
914 if component.optional
916 && !measurements.contains_key(component.measurement.as_str())
917 {
918 continue;
919 }
920 weight_total += component.weight;
921 }
922 if weight_total.abs() < FiniteF64::EPSILON {
923 continue;
926 }
927 for component in &profile.score_components {
928 let mut normalized_component_weight = FiniteF64::ZERO;
930
931 if let Some(value) = measurements.get_value(component.measurement.as_str()) {
932 normalized_component_weight = component.weight.saturating_div(weight_total);
933 let cdf = utils::calculate_cdf_score(
934 value.to_f64().max(0.0), component.p10.to_f64(),
936 component.p50.to_f64(),
937 );
938
939 let cdf = Annotated::try_from(cdf);
940
941 measurements.insert(
942 format!("score.ratio.{}", component.measurement),
943 Measurement {
944 value: cdf.clone(),
945 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
946 }
947 .into(),
948 );
949
950 let component_score =
951 cdf.and_then(|cdf| match cdf * normalized_component_weight {
952 Some(v) => Annotated::new(v),
953 None => Annotated::from_error(TryFromFloatError, None),
954 });
955
956 if let Some(component_score) = component_score.value() {
957 score_total += *component_score;
958 should_add_total = true;
959 }
960
961 measurements.insert(
962 format!("score.{}", component.measurement),
963 Measurement {
964 value: component_score,
965 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
966 }
967 .into(),
968 );
969 }
970
971 measurements.insert(
972 format!("score.weight.{}", component.measurement),
973 Measurement {
974 value: normalized_component_weight.into(),
975 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
976 }
977 .into(),
978 );
979 }
980 if should_add_total {
981 version.clone_from(&profile.version);
982 measurements.insert(
983 "score.total".to_owned(),
984 Measurement {
985 value: score_total.into(),
986 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
987 }
988 .into(),
989 );
990 }
991 }
992 break; }
994 }
995 version
996}
997
998fn normalize_trace_context_tags(event: &mut Event) {
1000 let tags = &mut event.tags.value_mut().get_or_insert_with(Tags::default).0;
1001 if let Some(contexts) = event.contexts.value()
1002 && let Some(trace_context) = contexts.get::<TraceContext>()
1003 && let Some(data) = trace_context.data.value()
1004 {
1005 if let Some(lcp_element) = data.lcp_element.value()
1006 && !tags.contains("lcp.element")
1007 {
1008 let tag_name = "lcp.element".to_owned();
1009 tags.insert(tag_name, Annotated::new(lcp_element.clone()));
1010 }
1011 if let Some(lcp_size) = data.lcp_size.value()
1012 && !tags.contains("lcp.size")
1013 {
1014 let tag_name = "lcp.size".to_owned();
1015 tags.insert(tag_name, Annotated::new(lcp_size.to_string()));
1016 }
1017 if let Some(lcp_id) = data.lcp_id.value() {
1018 let tag_name = "lcp.id".to_owned();
1019 if !tags.contains("lcp.id") {
1020 tags.insert(tag_name, Annotated::new(lcp_id.clone()));
1021 }
1022 }
1023 if let Some(lcp_url) = data.lcp_url.value() {
1024 let tag_name = "lcp.url".to_owned();
1025 if !tags.contains("lcp.url") {
1026 tags.insert(tag_name, Annotated::new(lcp_url.clone()));
1027 }
1028 }
1029 }
1030}
1031
1032impl MutMeasurements for Event {
1033 fn measurements(&mut self) -> &mut Annotated<Measurements> {
1034 &mut self.measurements
1035 }
1036}
1037
1038impl MutMeasurements for Span {
1039 fn measurements(&mut self) -> &mut Annotated<Measurements> {
1040 &mut self.measurements
1041 }
1042}
1043
1044fn compute_measurements(
1054 transaction_duration_ms: Option<FiniteF64>,
1055 measurements: &mut Measurements,
1056) {
1057 if let Some(frames_total) = measurements.get_value("frames_total")
1058 && frames_total > 0.0
1059 {
1060 if let Some(frames_frozen) = measurements.get_value("frames_frozen") {
1061 let frames_frozen_rate = Measurement {
1062 value: (frames_frozen / frames_total).into(),
1063 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
1064 };
1065 measurements.insert("frames_frozen_rate".to_owned(), frames_frozen_rate.into());
1066 }
1067 if let Some(frames_slow) = measurements.get_value("frames_slow") {
1068 let frames_slow_rate = Measurement {
1069 value: (frames_slow / frames_total).into(),
1070 unit: MetricUnit::Fraction(FractionUnit::Ratio).into(),
1071 };
1072 measurements.insert("frames_slow_rate".to_owned(), frames_slow_rate.into());
1073 }
1074 }
1075
1076 if let Some(transaction_duration_ms) = transaction_duration_ms
1078 && transaction_duration_ms > 0.0
1079 && let Some(stall_total_time) = measurements
1080 .get("stall_total_time")
1081 .and_then(Annotated::value)
1082 && matches!(
1083 stall_total_time.unit.value(),
1084 Some(&MetricUnit::Duration(DurationUnit::MilliSecond) | &MetricUnit::None) | None
1086 )
1087 && let Some(stall_total_time) = stall_total_time.value.0
1088 {
1089 let stall_percentage = Measurement {
1090 value: (stall_total_time / transaction_duration_ms).into(),
1091 unit: (MetricUnit::Fraction(FractionUnit::Ratio)).into(),
1092 };
1093 measurements.insert("stall_percentage".to_owned(), stall_percentage.into());
1094 }
1095}
1096
1097fn normalize_breakdowns(event: &mut Event, breakdowns_config: Option<&BreakdownsConfig>) {
1099 match breakdowns_config {
1100 None => {}
1101 Some(config) => breakdowns::normalize_breakdowns(event, config),
1102 }
1103}
1104
1105fn normalize_default_attributes(event: &mut Event, meta: &mut Meta, config: &NormalizationConfig) {
1106 let event_type = infer_event_type(event);
1107 event.ty = Annotated::from(event_type);
1108 event.project = Annotated::from(config.project_id);
1109 event.key_id = Annotated::from(config.key_id.clone());
1110 event.version = Annotated::from(config.protocol_version.clone());
1111 event.grouping_config = config
1112 .grouping_config
1113 .clone()
1114 .map_or(Annotated::empty(), |x| {
1115 FromValue::from_value(Annotated::<Value>::from(x))
1116 });
1117
1118 let _ = relay_event_schema::processor::apply(&mut event.platform, |platform, _| {
1119 if is_valid_platform(platform) {
1120 Ok(())
1121 } else {
1122 Err(ProcessingAction::DeleteValueSoft)
1123 }
1124 });
1125
1126 event.errors.get_or_insert_with(Vec::new);
1128 event.id.get_or_insert_with(EventId::new);
1129 event.platform.get_or_insert_with(|| "other".to_owned());
1130 event.logger.get_or_insert_with(String::new);
1131 event.extra.get_or_insert_with(Object::new);
1132 event.level.get_or_insert_with(|| match event_type {
1133 EventType::Transaction => Level::Info,
1134 _ => Level::Error,
1135 });
1136 if event.client_sdk.value().is_none() {
1137 event.client_sdk.set_value(get_sdk_info(config));
1138 }
1139
1140 if event.platform.as_str() == Some("java")
1141 && let Some(event_logger) = event.logger.value_mut().take()
1142 {
1143 let shortened = shorten_logger(event_logger, meta);
1144 event.logger.set_value(Some(shortened));
1145 }
1146}
1147
1148pub fn is_valid_platform(platform: &str) -> bool {
1152 VALID_PLATFORMS.contains(&platform)
1153}
1154
1155fn infer_event_type(event: &Event) -> EventType {
1157 if event.ty.value() == Some(&EventType::Transaction) {
1161 return EventType::Transaction;
1162 }
1163 if event.ty.value() == Some(&EventType::UserReportV2) {
1164 return EventType::UserReportV2;
1165 }
1166
1167 let has_exceptions = event
1169 .exceptions
1170 .value()
1171 .and_then(|exceptions| exceptions.values.value())
1172 .filter(|values| !values.is_empty())
1173 .is_some();
1174
1175 if has_exceptions {
1176 EventType::Error
1177 } else if event.csp.value().is_some() {
1178 EventType::Csp
1179 } else if event.hpkp.value().is_some() {
1180 EventType::Hpkp
1181 } else if event.expectct.value().is_some() {
1182 EventType::ExpectCt
1183 } else if event.expectstaple.value().is_some() {
1184 EventType::ExpectStaple
1185 } else if event.context::<NelContext>().is_some() {
1186 EventType::Nel
1187 } else {
1188 EventType::Default
1189 }
1190}
1191
1192fn get_sdk_info(config: &NormalizationConfig) -> Option<ClientSdkInfo> {
1194 config.client.as_ref().and_then(|client| {
1195 client
1196 .splitn(2, '/')
1197 .collect_tuple()
1198 .or_else(|| client.splitn(2, ' ').collect_tuple())
1199 .map(|(name, version)| ClientSdkInfo {
1200 name: Annotated::new(name.to_owned()),
1201 version: Annotated::new(version.to_owned()),
1202 ..Default::default()
1203 })
1204 })
1205}
1206
1207fn shorten_logger(logger: String, meta: &mut Meta) -> String {
1219 let original_len = bytecount::num_chars(logger.as_bytes());
1220 let trimmed = logger.trim();
1221 let logger_len = bytecount::num_chars(trimmed.as_bytes());
1222 if logger_len <= MaxChars::Logger.limit() {
1223 if trimmed == logger {
1224 return logger;
1225 } else {
1226 if trimmed.is_empty() {
1227 meta.add_remark(Remark {
1228 ty: RemarkType::Removed,
1229 rule_id: "@logger:remove".to_owned(),
1230 range: Some((0, original_len)),
1231 });
1232 } else {
1233 meta.add_remark(Remark {
1234 ty: RemarkType::Substituted,
1235 rule_id: "@logger:trim".to_owned(),
1236 range: None,
1237 });
1238 }
1239 meta.set_original_length(Some(original_len));
1240 return trimmed.to_owned();
1241 };
1242 }
1243
1244 let mut tokens = trimmed.split("").collect_vec();
1245 tokens.pop();
1247 tokens.reverse(); tokens.pop();
1249
1250 let word_cut = remove_logger_extra_chars(&mut tokens);
1251 if word_cut {
1252 remove_logger_word(&mut tokens);
1253 }
1254
1255 tokens.reverse();
1256 meta.add_remark(Remark {
1257 ty: RemarkType::Substituted,
1258 rule_id: "@logger:replace".to_owned(),
1259 range: Some((0, logger_len - tokens.len())),
1260 });
1261 meta.set_original_length(Some(original_len));
1262
1263 format!("*{}", tokens.join(""))
1264}
1265
1266fn remove_logger_extra_chars(tokens: &mut Vec<&str>) -> bool {
1272 let mut remove_chars = tokens.len() - MaxChars::Logger.limit() + 1;
1274 let mut word_cut = false;
1275 while remove_chars > 0 {
1276 if let Some(c) = tokens.pop() {
1277 if !word_cut && c != "." {
1278 word_cut = true;
1279 } else if word_cut && c == "." {
1280 word_cut = false;
1281 }
1282 }
1283 remove_chars -= 1;
1284 }
1285 word_cut
1286}
1287
1288fn remove_logger_word(tokens: &mut Vec<&str>) {
1291 let mut delimiter_found = false;
1292 for token in tokens.iter() {
1293 if *token == "." {
1294 delimiter_found = true;
1295 break;
1296 }
1297 }
1298 if !delimiter_found {
1299 return;
1300 }
1301 while let Some(i) = tokens.last() {
1302 if *i == "." {
1303 break;
1304 }
1305 tokens.pop();
1306 }
1307}
1308
1309fn normalize_contexts(contexts: &mut Annotated<Contexts>) {
1311 let _ = processor::apply(contexts, |contexts, _meta| {
1312 contexts.0.remove("reprocessing");
1316
1317 for annotated in &mut contexts.0.values_mut() {
1318 if let Some(ContextInner(Context::Trace(context))) = annotated.value_mut() {
1319 context.status.get_or_insert_with(|| SpanStatus::Unknown);
1320 }
1321 if let Some(context_inner) = annotated.value_mut() {
1322 crate::normalize::contexts::normalize_context(&mut context_inner.0);
1323 }
1324 }
1325
1326 Ok(())
1327 });
1328}
1329
1330fn filter_mobile_outliers(measurements: &mut Measurements) {
1334 for key in [
1335 "app_start_cold",
1336 "app_start_warm",
1337 "time_to_initial_display",
1338 "time_to_full_display",
1339 ] {
1340 if let Some(value) = measurements.get_value(key)
1341 && value > MAX_DURATION_MOBILE_MS
1342 {
1343 measurements.remove(key);
1344 }
1345 }
1346}
1347
1348fn normalize_mobile_measurements(measurements: &mut Measurements) {
1349 normalize_app_start_measurements(measurements);
1350 filter_mobile_outliers(measurements);
1351}
1352
1353fn normalize_units(measurements: &mut Measurements) {
1354 for (name, measurement) in measurements.iter_mut() {
1355 let measurement = match measurement.value_mut() {
1356 Some(m) => m,
1357 None => continue,
1358 };
1359
1360 let stated_unit = measurement.unit.value().copied();
1361 let default_unit = get_metric_measurement_unit(name);
1362 measurement
1363 .unit
1364 .set_value(Some(stated_unit.or(default_unit).unwrap_or_default()))
1365 }
1366}
1367
1368fn remove_invalid_measurements(
1377 measurements: &mut Measurements,
1378 meta: &mut Meta,
1379 measurements_config: CombinedMeasurementsConfig,
1380 max_name_and_unit_len: Option<usize>,
1381) {
1382 let max_custom_measurements = measurements_config
1384 .max_custom_measurements()
1385 .unwrap_or(usize::MAX);
1386
1387 let mut custom_measurements_count = 0;
1388 let mut removed_measurements = Object::new();
1389
1390 measurements.retain(|name, value| {
1391 let measurement = match value.value_mut() {
1392 Some(m) => m,
1393 None => return false,
1394 };
1395
1396 if !can_be_valid_metric_name(name) {
1397 meta.add_error(Error::invalid(format!(
1398 "Metric name contains invalid characters: \"{name}\""
1399 )));
1400 removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1401 return false;
1402 }
1403
1404 let unit = measurement.unit.value().unwrap_or(&MetricUnit::None);
1406
1407 if let Some(max_name_and_unit_len) = max_name_and_unit_len {
1408 let max_name_len = max_name_and_unit_len - unit.to_string().len();
1409
1410 if name.len() > max_name_len {
1411 meta.add_error(Error::invalid(format!(
1412 "Metric name too long {}/{max_name_len}: \"{name}\"",
1413 name.len(),
1414 )));
1415 removed_measurements
1416 .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1417 return false;
1418 }
1419 }
1420
1421 if let Some(builtin_measurement) = measurements_config
1423 .builtin_measurement_keys()
1424 .find(|builtin| builtin.name() == name)
1425 {
1426 let value = measurement.value.value().unwrap_or(&FiniteF64::ZERO);
1427 if !builtin_measurement.allow_negative() && *value < 0.0 {
1429 meta.add_error(Error::invalid(format!(
1430 "Negative value for measurement {name} not allowed: {value}",
1431 )));
1432 removed_measurements
1433 .insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1434 return false;
1435 }
1436 return builtin_measurement.unit() == unit;
1440 }
1441
1442 if custom_measurements_count < max_custom_measurements {
1444 custom_measurements_count += 1;
1445 return true;
1446 }
1447
1448 meta.add_error(Error::invalid(format!("Too many measurements: {name}")));
1449 removed_measurements.insert(name.clone(), Annotated::new(std::mem::take(measurement)));
1450
1451 false
1452 });
1453
1454 if !removed_measurements.is_empty() {
1455 meta.set_original_value(Some(removed_measurements));
1456 }
1457}
1458
1459fn get_metric_measurement_unit(measurement_name: &str) -> Option<MetricUnit> {
1464 match measurement_name {
1465 "fcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1467 "lcp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1468 "fid" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1469 "fp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1470 "inp" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1471 "ttfb" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1472 "ttfb.requesttime" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1473 "cls" => Some(MetricUnit::None),
1474
1475 "app_start_cold" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1477 "app_start_warm" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1478 "frames_total" => Some(MetricUnit::None),
1479 "frames_slow" => Some(MetricUnit::None),
1480 "frames_slow_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1481 "frames_frozen" => Some(MetricUnit::None),
1482 "frames_frozen_rate" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1483 "time_to_initial_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1484 "time_to_full_display" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1485
1486 "stall_count" => Some(MetricUnit::None),
1488 "stall_total_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1489 "stall_longest_time" => Some(MetricUnit::Duration(DurationUnit::MilliSecond)),
1490 "stall_percentage" => Some(MetricUnit::Fraction(FractionUnit::Ratio)),
1491
1492 _ => None,
1494 }
1495}
1496
1497fn normalize_app_start_measurements(measurements: &mut Measurements) {
1502 if let Some(app_start_cold_value) = measurements.remove("app.start.cold") {
1503 measurements.insert("app_start_cold".to_owned(), app_start_cold_value);
1504 }
1505 if let Some(app_start_warm_value) = measurements.remove("app.start.warm") {
1506 measurements.insert("app_start_warm".to_owned(), app_start_warm_value);
1507 }
1508}
1509
1510#[cfg(test)]
1511mod tests {
1512
1513 use relay_event_schema::protocol::SpanData;
1514 use relay_pattern::Pattern;
1515 use relay_protocol::assert_annotated_snapshot;
1516 use std::collections::BTreeMap;
1517 use std::collections::HashMap;
1518
1519 use insta::assert_debug_snapshot;
1520 use itertools::Itertools;
1521 use relay_event_schema::protocol::{Breadcrumb, Csp, DebugMeta, DeviceContext, Values};
1522 use relay_protocol::{SerializableAnnotated, get_value};
1523 use serde_json::json;
1524
1525 use super::*;
1526 use crate::{ClientHints, MeasurementsConfig, ModelCostV2};
1527
1528 const IOS_MOBILE_EVENT: &str = r#"
1529 {
1530 "sdk": {"name": "sentry.cocoa"},
1531 "contexts": {
1532 "trace": {
1533 "op": "ui.load"
1534 }
1535 },
1536 "measurements": {
1537 "app_start_warm": {
1538 "value": 8049.345970153808,
1539 "unit": "millisecond"
1540 },
1541 "time_to_full_display": {
1542 "value": 8240.571022033691,
1543 "unit": "millisecond"
1544 },
1545 "time_to_initial_display": {
1546 "value": 8049.345970153808,
1547 "unit": "millisecond"
1548 }
1549 }
1550 }
1551 "#;
1552
1553 const ANDROID_MOBILE_EVENT: &str = r#"
1554 {
1555 "sdk": {"name": "sentry.java.android"},
1556 "contexts": {
1557 "trace": {
1558 "op": "ui.load"
1559 }
1560 },
1561 "measurements": {
1562 "app_start_cold": {
1563 "value": 22648,
1564 "unit": "millisecond"
1565 },
1566 "time_to_full_display": {
1567 "value": 22647,
1568 "unit": "millisecond"
1569 },
1570 "time_to_initial_display": {
1571 "value": 22647,
1572 "unit": "millisecond"
1573 }
1574 }
1575 }
1576 "#;
1577
1578 fn collect_span_data<const N: usize>(event: Annotated<Event>) -> [Annotated<SpanData>; N] {
1579 get_value!(event.spans!)
1580 .iter()
1581 .map(|span| Annotated::new(get_value!(span.data!).clone()))
1582 .collect::<Vec<_>>()
1583 .try_into()
1584 .unwrap()
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 Pattern::new("claude-2.1").unwrap(),
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 input_cache_write_per_token: 0.0,
2303 },
2304 ),
2305 (
2306 Pattern::new("gpt4-21-04").unwrap(),
2307 ModelCostV2 {
2308 input_per_token: 0.02,
2309 output_per_token: 0.03,
2310 output_reasoning_per_token: 0.04,
2311 input_cached_per_token: 0.0,
2312 input_cache_write_per_token: 0.0,
2313 },
2314 ),
2315 ]),
2316 }),
2317 ..NormalizationConfig::default()
2318 },
2319 );
2320
2321 let [span1, span2] = collect_span_data(event);
2322
2323 assert_annotated_snapshot!(span1, @r#"
2324 {
2325 "gen_ai.usage.total_tokens": 3000.0,
2326 "gen_ai.usage.input_tokens": 1000.0,
2327 "gen_ai.usage.output_tokens": 2000.0,
2328 "gen_ai.request.model": "claude-2.1",
2329 "gen_ai.cost.total_tokens": 50.0,
2330 "gen_ai.cost.input_tokens": 10.0,
2331 "gen_ai.cost.output_tokens": 40.0,
2332 "gen_ai.response.tokens_per_second": 62500.0,
2333 "gen_ai.operation.type": "ai_client"
2334 }
2335 "#);
2336 assert_annotated_snapshot!(span2, @r#"
2337 {
2338 "gen_ai.usage.total_tokens": 3000.0,
2339 "gen_ai.usage.input_tokens": 1000.0,
2340 "gen_ai.usage.output_tokens": 2000.0,
2341 "gen_ai.request.model": "gpt4-21-04",
2342 "gen_ai.cost.total_tokens": 80.0,
2343 "gen_ai.cost.input_tokens": 20.0,
2344 "gen_ai.cost.output_tokens": 60.0,
2345 "gen_ai.response.tokens_per_second": 62500.0,
2346 "gen_ai.operation.type": "ai_client"
2347 }
2348 "#);
2349 }
2350
2351 #[test]
2352 fn test_ai_data() {
2353 let json = r#"
2354 {
2355 "spans": [
2356 {
2357 "timestamp": 1702474614.0175,
2358 "start_timestamp": 1702474613.0175,
2359 "description": "OpenAI ",
2360 "op": "gen_ai.chat_completions.openai",
2361 "span_id": "9c01bd820a083e63",
2362 "parent_span_id": "a1e13f3f06239d69",
2363 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2364 "data": {
2365 "gen_ai.usage.input_tokens": 1000,
2366 "gen_ai.usage.output_tokens": 2000,
2367 "gen_ai.usage.output_tokens.reasoning": 1000,
2368 "gen_ai.usage.input_tokens.cached": 500,
2369 "gen_ai.request.model": "claude-2.1"
2370 }
2371 },
2372 {
2373 "timestamp": 1702474614.0175,
2374 "start_timestamp": 1702474613.0175,
2375 "description": "OpenAI ",
2376 "op": "gen_ai.chat_completions.openai",
2377 "span_id": "ac01bd820a083e63",
2378 "parent_span_id": "a1e13f3f06239d69",
2379 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2380 "data": {
2381 "gen_ai.usage.input_tokens": 1000,
2382 "gen_ai.usage.output_tokens": 2000,
2383 "gen_ai.request.model": "gpt4-21-04"
2384 }
2385 },
2386 {
2387 "timestamp": 1702474614.0175,
2388 "start_timestamp": 1702474613.0175,
2389 "description": "OpenAI ",
2390 "op": "gen_ai.chat_completions.openai",
2391 "span_id": "ac01bd820a083e63",
2392 "parent_span_id": "a1e13f3f06239d69",
2393 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2394 "data": {
2395 "gen_ai.usage.input_tokens": 1000,
2396 "gen_ai.usage.output_tokens": 2000,
2397 "gen_ai.response.model": "gpt4-21-04"
2398 }
2399 }
2400 ]
2401 }
2402 "#;
2403
2404 let mut event = Annotated::<Event>::from_json(json).unwrap();
2405
2406 normalize_event(
2407 &mut event,
2408 &NormalizationConfig {
2409 ai_model_costs: Some(&ModelCosts {
2410 version: 2,
2411 models: HashMap::from([
2412 (
2413 Pattern::new("claude-2.1").unwrap(),
2414 ModelCostV2 {
2415 input_per_token: 0.01,
2416 output_per_token: 0.02,
2417 output_reasoning_per_token: 0.03,
2418 input_cached_per_token: 0.04,
2419 input_cache_write_per_token: 0.0,
2420 },
2421 ),
2422 (
2423 Pattern::new("gpt4-21-04").unwrap(),
2424 ModelCostV2 {
2425 input_per_token: 0.09,
2426 output_per_token: 0.05,
2427 output_reasoning_per_token: 0.0,
2428 input_cached_per_token: 0.0,
2429 input_cache_write_per_token: 0.0,
2430 },
2431 ),
2432 ]),
2433 }),
2434 ..NormalizationConfig::default()
2435 },
2436 );
2437
2438 let [span1, span2, span3] = collect_span_data(event);
2439
2440 assert_annotated_snapshot!(span1, @r#"
2441 {
2442 "gen_ai.usage.total_tokens": 3000.0,
2443 "gen_ai.usage.input_tokens": 1000,
2444 "gen_ai.usage.input_tokens.cached": 500,
2445 "gen_ai.usage.output_tokens": 2000,
2446 "gen_ai.usage.output_tokens.reasoning": 1000,
2447 "gen_ai.request.model": "claude-2.1",
2448 "gen_ai.cost.total_tokens": 75.0,
2449 "gen_ai.cost.input_tokens": 25.0,
2450 "gen_ai.cost.output_tokens": 50.0,
2451 "gen_ai.response.tokens_per_second": 2000.0,
2452 "gen_ai.operation.type": "ai_client"
2453 }
2454 "#);
2455 assert_annotated_snapshot!(span2, @r#"
2456 {
2457 "gen_ai.usage.total_tokens": 3000.0,
2458 "gen_ai.usage.input_tokens": 1000,
2459 "gen_ai.usage.output_tokens": 2000,
2460 "gen_ai.request.model": "gpt4-21-04",
2461 "gen_ai.cost.total_tokens": 190.0,
2462 "gen_ai.cost.input_tokens": 90.0,
2463 "gen_ai.cost.output_tokens": 100.0,
2464 "gen_ai.response.tokens_per_second": 2000.0,
2465 "gen_ai.operation.type": "ai_client"
2466 }
2467 "#);
2468 assert_annotated_snapshot!(span3, @r#"
2469 {
2470 "gen_ai.usage.total_tokens": 3000.0,
2471 "gen_ai.usage.input_tokens": 1000,
2472 "gen_ai.usage.output_tokens": 2000,
2473 "gen_ai.response.model": "gpt4-21-04",
2474 "gen_ai.cost.total_tokens": 190.0,
2475 "gen_ai.cost.input_tokens": 90.0,
2476 "gen_ai.cost.output_tokens": 100.0,
2477 "gen_ai.response.tokens_per_second": 2000.0,
2478 "gen_ai.operation.type": "ai_client"
2479 }
2480 "#);
2481 }
2482
2483 #[test]
2484 fn test_ai_data_with_no_tokens() {
2485 let json = r#"
2486 {
2487 "spans": [
2488 {
2489 "timestamp": 1702474613.0495,
2490 "start_timestamp": 1702474613.0175,
2491 "description": "OpenAI ",
2492 "op": "gen_ai.invoke_agent",
2493 "span_id": "9c01bd820a083e63",
2494 "parent_span_id": "a1e13f3f06239d69",
2495 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2496 "data": {
2497 "gen_ai.request.model": "claude-2.1"
2498 }
2499 }
2500 ]
2501 }
2502 "#;
2503
2504 let mut event = Annotated::<Event>::from_json(json).unwrap();
2505
2506 normalize_event(
2507 &mut event,
2508 &NormalizationConfig {
2509 ai_model_costs: Some(&ModelCosts {
2510 version: 2,
2511 models: HashMap::from([(
2512 Pattern::new("claude-2.1").unwrap(),
2513 ModelCostV2 {
2514 input_per_token: 0.01,
2515 output_per_token: 0.02,
2516 output_reasoning_per_token: 0.03,
2517 input_cached_per_token: 0.0,
2518 input_cache_write_per_token: 0.0,
2519 },
2520 )]),
2521 }),
2522 ..NormalizationConfig::default()
2523 },
2524 );
2525
2526 let [span] = collect_span_data(event);
2527
2528 assert_annotated_snapshot!(span, @r#"
2529 {
2530 "gen_ai.request.model": "claude-2.1",
2531 "gen_ai.operation.type": "agent"
2532 }
2533 "#);
2534 }
2535
2536 #[test]
2537 fn test_ai_data_with_ai_op_prefix() {
2538 let json = r#"
2539 {
2540 "spans": [
2541 {
2542 "timestamp": 1702474613.0495,
2543 "start_timestamp": 1702474613.0175,
2544 "description": "OpenAI ",
2545 "op": "ai.chat_completions.openai",
2546 "span_id": "9c01bd820a083e63",
2547 "parent_span_id": "a1e13f3f06239d69",
2548 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2549 "data": {
2550 "gen_ai.usage.input_tokens": 1000,
2551 "gen_ai.usage.output_tokens": 2000,
2552 "gen_ai.usage.output_tokens.reasoning": 1000,
2553 "gen_ai.usage.input_tokens.cached": 500,
2554 "gen_ai.request.model": "claude-2.1"
2555 }
2556 },
2557 {
2558 "timestamp": 1702474613.0495,
2559 "start_timestamp": 1702474613.0175,
2560 "description": "OpenAI ",
2561 "op": "ai.chat_completions.openai",
2562 "span_id": "ac01bd820a083e63",
2563 "parent_span_id": "a1e13f3f06239d69",
2564 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2565 "data": {
2566 "gen_ai.usage.input_tokens": 1000,
2567 "gen_ai.usage.output_tokens": 2000,
2568 "gen_ai.request.model": "gpt4-21-04"
2569 }
2570 }
2571 ]
2572 }
2573 "#;
2574
2575 let mut event = Annotated::<Event>::from_json(json).unwrap();
2576
2577 normalize_event(
2578 &mut event,
2579 &NormalizationConfig {
2580 ai_model_costs: Some(&ModelCosts {
2581 version: 2,
2582 models: HashMap::from([
2583 (
2584 Pattern::new("claude-2.1").unwrap(),
2585 ModelCostV2 {
2586 input_per_token: 0.01,
2587 output_per_token: 0.02,
2588 output_reasoning_per_token: 0.0,
2589 input_cached_per_token: 0.04,
2590 input_cache_write_per_token: 0.0,
2591 },
2592 ),
2593 (
2594 Pattern::new("gpt4-21-04").unwrap(),
2595 ModelCostV2 {
2596 input_per_token: 0.09,
2597 output_per_token: 0.05,
2598 output_reasoning_per_token: 0.06,
2599 input_cached_per_token: 0.0,
2600 input_cache_write_per_token: 0.0,
2601 },
2602 ),
2603 ]),
2604 }),
2605 ..NormalizationConfig::default()
2606 },
2607 );
2608
2609 let [span1, span2] = collect_span_data(event);
2610
2611 assert_annotated_snapshot!(span1, @r#"
2612 {
2613 "gen_ai.usage.total_tokens": 3000.0,
2614 "gen_ai.usage.input_tokens": 1000,
2615 "gen_ai.usage.input_tokens.cached": 500,
2616 "gen_ai.usage.output_tokens": 2000,
2617 "gen_ai.usage.output_tokens.reasoning": 1000,
2618 "gen_ai.request.model": "claude-2.1",
2619 "gen_ai.cost.total_tokens": 65.0,
2620 "gen_ai.cost.input_tokens": 25.0,
2621 "gen_ai.cost.output_tokens": 40.0,
2622 "gen_ai.response.tokens_per_second": 62500.0,
2623 "gen_ai.operation.type": "ai_client"
2624 }
2625 "#);
2626 assert_annotated_snapshot!(span2, @r#"
2627 {
2628 "gen_ai.usage.total_tokens": 3000.0,
2629 "gen_ai.usage.input_tokens": 1000,
2630 "gen_ai.usage.output_tokens": 2000,
2631 "gen_ai.request.model": "gpt4-21-04",
2632 "gen_ai.cost.total_tokens": 190.0,
2633 "gen_ai.cost.input_tokens": 90.0,
2634 "gen_ai.cost.output_tokens": 100.0,
2635 "gen_ai.response.tokens_per_second": 62500.0,
2636 "gen_ai.operation.type": "ai_client"
2637 }
2638 "#);
2639 }
2640
2641 #[test]
2642 fn test_ai_response_tokens_per_second_no_output_tokens() {
2643 let json = r#"
2644 {
2645 "spans": [
2646 {
2647 "timestamp": 1702474614.0175,
2648 "start_timestamp": 1702474613.0175,
2649 "op": "gen_ai.chat_completions",
2650 "span_id": "9c01bd820a083e63",
2651 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2652 "data": {
2653 "gen_ai.usage.input_tokens": 500
2654 }
2655 }
2656 ]
2657 }
2658 "#;
2659
2660 let mut event = Annotated::<Event>::from_json(json).unwrap();
2661
2662 normalize_event(
2663 &mut event,
2664 &NormalizationConfig {
2665 ai_model_costs: Some(&ModelCosts {
2666 version: 2,
2667 models: HashMap::new(),
2668 }),
2669 ..NormalizationConfig::default()
2670 },
2671 );
2672
2673 let [span] = collect_span_data(event);
2674
2675 assert_annotated_snapshot!(span, @r#"
2677 {
2678 "gen_ai.usage.total_tokens": 500.0,
2679 "gen_ai.usage.input_tokens": 500,
2680 "gen_ai.operation.type": "ai_client"
2681 }
2682 "#);
2683 }
2684
2685 #[test]
2686 fn test_ai_response_tokens_per_second_zero_duration() {
2687 let json = r#"
2688 {
2689 "spans": [
2690 {
2691 "timestamp": 1702474613.0175,
2692 "start_timestamp": 1702474613.0175,
2693 "op": "gen_ai.chat_completions",
2694 "span_id": "9c01bd820a083e63",
2695 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2696 "data": {
2697 "gen_ai.usage.output_tokens": 1000
2698 }
2699 }
2700 ]
2701 }
2702 "#;
2703
2704 let mut event = Annotated::<Event>::from_json(json).unwrap();
2705
2706 normalize_event(
2707 &mut event,
2708 &NormalizationConfig {
2709 ai_model_costs: Some(&ModelCosts {
2710 version: 2,
2711 models: HashMap::new(),
2712 }),
2713 ..NormalizationConfig::default()
2714 },
2715 );
2716
2717 let [span] = collect_span_data(event);
2718
2719 assert_annotated_snapshot!(span, @r#"
2721 {
2722 "gen_ai.usage.total_tokens": 1000.0,
2723 "gen_ai.usage.output_tokens": 1000,
2724 "gen_ai.operation.type": "ai_client"
2725 }
2726 "#);
2727 }
2728
2729 #[test]
2730 fn test_ai_operation_type_mapping() {
2731 let json = r#"
2732 {
2733 "type": "transaction",
2734 "transaction": "test-transaction",
2735 "spans": [
2736 {
2737 "op": "gen_ai.chat",
2738 "description": "AI chat completion",
2739 "data": {}
2740 },
2741 {
2742 "op": "gen_ai.handoff",
2743 "description": "AI agent handoff",
2744 "data": {}
2745 },
2746 {
2747 "op": "gen_ai.unknown",
2748 "description": "Unknown AI operation",
2749 "data": {}
2750 }
2751 ]
2752 }
2753 "#;
2754
2755 let mut event = Annotated::<Event>::from_json(json).unwrap();
2756
2757 normalize_event(&mut event, &NormalizationConfig::default());
2758
2759 let [span1, span2, span3] = collect_span_data(event);
2760
2761 assert_annotated_snapshot!(span1, @r#"
2762 {
2763 "gen_ai.operation.type": "ai_client"
2764 }
2765 "#);
2766 assert_annotated_snapshot!(span2, @r#"
2767 {
2768 "gen_ai.operation.type": "handoff"
2769 }
2770 "#);
2771 assert_annotated_snapshot!(span3, @r#"
2772 {
2773 "gen_ai.operation.type": "ai_client"
2774 }
2775 "#);
2776 }
2777
2778 #[test]
2779 fn test_apple_high_device_class() {
2780 let mut event = Event {
2781 contexts: {
2782 let mut contexts = Contexts::new();
2783 contexts.add(DeviceContext {
2784 family: "iPhone".to_owned().into(),
2785 model: "iPhone15,3".to_owned().into(),
2786 ..Default::default()
2787 });
2788 Annotated::new(contexts)
2789 },
2790 ..Default::default()
2791 };
2792 normalize_device_class(&mut event);
2793 assert_debug_snapshot!(event.tags, @r###"
2794 Tags(
2795 PairList(
2796 [
2797 TagEntry(
2798 "device.class",
2799 "3",
2800 ),
2801 ],
2802 ),
2803 )
2804 "###);
2805 }
2806
2807 #[test]
2808 fn test_filter_mobile_outliers() {
2809 let mut measurements =
2810 Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2811 .unwrap()
2812 .into_value()
2813 .unwrap();
2814 assert_eq!(measurements.len(), 1);
2815 filter_mobile_outliers(&mut measurements);
2816 assert_eq!(measurements.len(), 0);
2817 }
2818
2819 #[test]
2820 fn test_computed_performance_score() {
2821 let json = r#"
2822 {
2823 "type": "transaction",
2824 "timestamp": "2021-04-26T08:00:05+0100",
2825 "start_timestamp": "2021-04-26T08:00:00+0100",
2826 "measurements": {
2827 "fid": {"value": 213, "unit": "millisecond"},
2828 "fcp": {"value": 1237, "unit": "millisecond"},
2829 "lcp": {"value": 6596, "unit": "millisecond"},
2830 "cls": {"value": 0.11}
2831 },
2832 "contexts": {
2833 "browser": {
2834 "name": "Chrome",
2835 "version": "120.1.1",
2836 "type": "browser"
2837 }
2838 }
2839 }
2840 "#;
2841
2842 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2843
2844 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2845 "profiles": [
2846 {
2847 "name": "Desktop",
2848 "scoreComponents": [
2849 {
2850 "measurement": "fcp",
2851 "weight": 0.15,
2852 "p10": 900,
2853 "p50": 1600
2854 },
2855 {
2856 "measurement": "lcp",
2857 "weight": 0.30,
2858 "p10": 1200,
2859 "p50": 2400
2860 },
2861 {
2862 "measurement": "fid",
2863 "weight": 0.30,
2864 "p10": 100,
2865 "p50": 300
2866 },
2867 {
2868 "measurement": "cls",
2869 "weight": 0.25,
2870 "p10": 0.1,
2871 "p50": 0.25
2872 },
2873 {
2874 "measurement": "ttfb",
2875 "weight": 0.0,
2876 "p10": 0.2,
2877 "p50": 0.4
2878 },
2879 ],
2880 "condition": {
2881 "op":"eq",
2882 "name": "event.contexts.browser.name",
2883 "value": "Chrome"
2884 }
2885 }
2886 ]
2887 }))
2888 .unwrap();
2889
2890 normalize_performance_score(&mut event, Some(&performance_score));
2891
2892 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2893 {
2894 "type": "transaction",
2895 "timestamp": 1619420405.0,
2896 "start_timestamp": 1619420400.0,
2897 "contexts": {
2898 "browser": {
2899 "name": "Chrome",
2900 "version": "120.1.1",
2901 "type": "browser",
2902 },
2903 },
2904 "measurements": {
2905 "cls": {
2906 "value": 0.11,
2907 },
2908 "fcp": {
2909 "value": 1237.0,
2910 "unit": "millisecond",
2911 },
2912 "fid": {
2913 "value": 213.0,
2914 "unit": "millisecond",
2915 },
2916 "lcp": {
2917 "value": 6596.0,
2918 "unit": "millisecond",
2919 },
2920 "score.cls": {
2921 "value": 0.21864170607444863,
2922 "unit": "ratio",
2923 },
2924 "score.fcp": {
2925 "value": 0.10750855443790831,
2926 "unit": "ratio",
2927 },
2928 "score.fid": {
2929 "value": 0.19657361348282545,
2930 "unit": "ratio",
2931 },
2932 "score.lcp": {
2933 "value": 0.009238896571386584,
2934 "unit": "ratio",
2935 },
2936 "score.ratio.cls": {
2937 "value": 0.8745668242977945,
2938 "unit": "ratio",
2939 },
2940 "score.ratio.fcp": {
2941 "value": 0.7167236962527221,
2942 "unit": "ratio",
2943 },
2944 "score.ratio.fid": {
2945 "value": 0.6552453782760849,
2946 "unit": "ratio",
2947 },
2948 "score.ratio.lcp": {
2949 "value": 0.03079632190462195,
2950 "unit": "ratio",
2951 },
2952 "score.total": {
2953 "value": 0.531962770566569,
2954 "unit": "ratio",
2955 },
2956 "score.weight.cls": {
2957 "value": 0.25,
2958 "unit": "ratio",
2959 },
2960 "score.weight.fcp": {
2961 "value": 0.15,
2962 "unit": "ratio",
2963 },
2964 "score.weight.fid": {
2965 "value": 0.3,
2966 "unit": "ratio",
2967 },
2968 "score.weight.lcp": {
2969 "value": 0.3,
2970 "unit": "ratio",
2971 },
2972 "score.weight.ttfb": {
2973 "value": 0.0,
2974 "unit": "ratio",
2975 },
2976 },
2977 }
2978 "###);
2979 }
2980
2981 #[test]
2984 fn test_computed_performance_score_with_under_normalized_weights() {
2985 let json = r#"
2986 {
2987 "type": "transaction",
2988 "timestamp": "2021-04-26T08:00:05+0100",
2989 "start_timestamp": "2021-04-26T08:00:00+0100",
2990 "measurements": {
2991 "fid": {"value": 213, "unit": "millisecond"},
2992 "fcp": {"value": 1237, "unit": "millisecond"},
2993 "lcp": {"value": 6596, "unit": "millisecond"},
2994 "cls": {"value": 0.11}
2995 },
2996 "contexts": {
2997 "browser": {
2998 "name": "Chrome",
2999 "version": "120.1.1",
3000 "type": "browser"
3001 }
3002 }
3003 }
3004 "#;
3005
3006 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3007
3008 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3009 "profiles": [
3010 {
3011 "name": "Desktop",
3012 "scoreComponents": [
3013 {
3014 "measurement": "fcp",
3015 "weight": 0.03,
3016 "p10": 900,
3017 "p50": 1600
3018 },
3019 {
3020 "measurement": "lcp",
3021 "weight": 0.06,
3022 "p10": 1200,
3023 "p50": 2400
3024 },
3025 {
3026 "measurement": "fid",
3027 "weight": 0.06,
3028 "p10": 100,
3029 "p50": 300
3030 },
3031 {
3032 "measurement": "cls",
3033 "weight": 0.05,
3034 "p10": 0.1,
3035 "p50": 0.25
3036 },
3037 {
3038 "measurement": "ttfb",
3039 "weight": 0.0,
3040 "p10": 0.2,
3041 "p50": 0.4
3042 },
3043 ],
3044 "condition": {
3045 "op":"eq",
3046 "name": "event.contexts.browser.name",
3047 "value": "Chrome"
3048 }
3049 }
3050 ]
3051 }))
3052 .unwrap();
3053
3054 normalize_performance_score(&mut event, Some(&performance_score));
3055
3056 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3057 {
3058 "type": "transaction",
3059 "timestamp": 1619420405.0,
3060 "start_timestamp": 1619420400.0,
3061 "contexts": {
3062 "browser": {
3063 "name": "Chrome",
3064 "version": "120.1.1",
3065 "type": "browser",
3066 },
3067 },
3068 "measurements": {
3069 "cls": {
3070 "value": 0.11,
3071 },
3072 "fcp": {
3073 "value": 1237.0,
3074 "unit": "millisecond",
3075 },
3076 "fid": {
3077 "value": 213.0,
3078 "unit": "millisecond",
3079 },
3080 "lcp": {
3081 "value": 6596.0,
3082 "unit": "millisecond",
3083 },
3084 "score.cls": {
3085 "value": 0.21864170607444863,
3086 "unit": "ratio",
3087 },
3088 "score.fcp": {
3089 "value": 0.10750855443790831,
3090 "unit": "ratio",
3091 },
3092 "score.fid": {
3093 "value": 0.19657361348282545,
3094 "unit": "ratio",
3095 },
3096 "score.lcp": {
3097 "value": 0.009238896571386584,
3098 "unit": "ratio",
3099 },
3100 "score.ratio.cls": {
3101 "value": 0.8745668242977945,
3102 "unit": "ratio",
3103 },
3104 "score.ratio.fcp": {
3105 "value": 0.7167236962527221,
3106 "unit": "ratio",
3107 },
3108 "score.ratio.fid": {
3109 "value": 0.6552453782760849,
3110 "unit": "ratio",
3111 },
3112 "score.ratio.lcp": {
3113 "value": 0.03079632190462195,
3114 "unit": "ratio",
3115 },
3116 "score.total": {
3117 "value": 0.531962770566569,
3118 "unit": "ratio",
3119 },
3120 "score.weight.cls": {
3121 "value": 0.25,
3122 "unit": "ratio",
3123 },
3124 "score.weight.fcp": {
3125 "value": 0.15,
3126 "unit": "ratio",
3127 },
3128 "score.weight.fid": {
3129 "value": 0.3,
3130 "unit": "ratio",
3131 },
3132 "score.weight.lcp": {
3133 "value": 0.3,
3134 "unit": "ratio",
3135 },
3136 "score.weight.ttfb": {
3137 "value": 0.0,
3138 "unit": "ratio",
3139 },
3140 },
3141 }
3142 "###);
3143 }
3144
3145 #[test]
3148 fn test_computed_performance_score_with_over_normalized_weights() {
3149 let json = r#"
3150 {
3151 "type": "transaction",
3152 "timestamp": "2021-04-26T08:00:05+0100",
3153 "start_timestamp": "2021-04-26T08:00:00+0100",
3154 "measurements": {
3155 "fid": {"value": 213, "unit": "millisecond"},
3156 "fcp": {"value": 1237, "unit": "millisecond"},
3157 "lcp": {"value": 6596, "unit": "millisecond"},
3158 "cls": {"value": 0.11}
3159 },
3160 "contexts": {
3161 "browser": {
3162 "name": "Chrome",
3163 "version": "120.1.1",
3164 "type": "browser"
3165 }
3166 }
3167 }
3168 "#;
3169
3170 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3171
3172 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3173 "profiles": [
3174 {
3175 "name": "Desktop",
3176 "scoreComponents": [
3177 {
3178 "measurement": "fcp",
3179 "weight": 0.30,
3180 "p10": 900,
3181 "p50": 1600
3182 },
3183 {
3184 "measurement": "lcp",
3185 "weight": 0.60,
3186 "p10": 1200,
3187 "p50": 2400
3188 },
3189 {
3190 "measurement": "fid",
3191 "weight": 0.60,
3192 "p10": 100,
3193 "p50": 300
3194 },
3195 {
3196 "measurement": "cls",
3197 "weight": 0.50,
3198 "p10": 0.1,
3199 "p50": 0.25
3200 },
3201 {
3202 "measurement": "ttfb",
3203 "weight": 0.0,
3204 "p10": 0.2,
3205 "p50": 0.4
3206 },
3207 ],
3208 "condition": {
3209 "op":"eq",
3210 "name": "event.contexts.browser.name",
3211 "value": "Chrome"
3212 }
3213 }
3214 ]
3215 }))
3216 .unwrap();
3217
3218 normalize_performance_score(&mut event, Some(&performance_score));
3219
3220 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3221 {
3222 "type": "transaction",
3223 "timestamp": 1619420405.0,
3224 "start_timestamp": 1619420400.0,
3225 "contexts": {
3226 "browser": {
3227 "name": "Chrome",
3228 "version": "120.1.1",
3229 "type": "browser",
3230 },
3231 },
3232 "measurements": {
3233 "cls": {
3234 "value": 0.11,
3235 },
3236 "fcp": {
3237 "value": 1237.0,
3238 "unit": "millisecond",
3239 },
3240 "fid": {
3241 "value": 213.0,
3242 "unit": "millisecond",
3243 },
3244 "lcp": {
3245 "value": 6596.0,
3246 "unit": "millisecond",
3247 },
3248 "score.cls": {
3249 "value": 0.21864170607444863,
3250 "unit": "ratio",
3251 },
3252 "score.fcp": {
3253 "value": 0.10750855443790831,
3254 "unit": "ratio",
3255 },
3256 "score.fid": {
3257 "value": 0.19657361348282545,
3258 "unit": "ratio",
3259 },
3260 "score.lcp": {
3261 "value": 0.009238896571386584,
3262 "unit": "ratio",
3263 },
3264 "score.ratio.cls": {
3265 "value": 0.8745668242977945,
3266 "unit": "ratio",
3267 },
3268 "score.ratio.fcp": {
3269 "value": 0.7167236962527221,
3270 "unit": "ratio",
3271 },
3272 "score.ratio.fid": {
3273 "value": 0.6552453782760849,
3274 "unit": "ratio",
3275 },
3276 "score.ratio.lcp": {
3277 "value": 0.03079632190462195,
3278 "unit": "ratio",
3279 },
3280 "score.total": {
3281 "value": 0.531962770566569,
3282 "unit": "ratio",
3283 },
3284 "score.weight.cls": {
3285 "value": 0.25,
3286 "unit": "ratio",
3287 },
3288 "score.weight.fcp": {
3289 "value": 0.15,
3290 "unit": "ratio",
3291 },
3292 "score.weight.fid": {
3293 "value": 0.3,
3294 "unit": "ratio",
3295 },
3296 "score.weight.lcp": {
3297 "value": 0.3,
3298 "unit": "ratio",
3299 },
3300 "score.weight.ttfb": {
3301 "value": 0.0,
3302 "unit": "ratio",
3303 },
3304 },
3305 }
3306 "###);
3307 }
3308
3309 #[test]
3310 fn test_computed_performance_score_missing_measurement() {
3311 let json = r#"
3312 {
3313 "type": "transaction",
3314 "timestamp": "2021-04-26T08:00:05+0100",
3315 "start_timestamp": "2021-04-26T08:00:00+0100",
3316 "measurements": {
3317 "a": {"value": 213, "unit": "millisecond"}
3318 },
3319 "contexts": {
3320 "browser": {
3321 "name": "Chrome",
3322 "version": "120.1.1",
3323 "type": "browser"
3324 }
3325 }
3326 }
3327 "#;
3328
3329 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3330
3331 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3332 "profiles": [
3333 {
3334 "name": "Desktop",
3335 "scoreComponents": [
3336 {
3337 "measurement": "a",
3338 "weight": 0.15,
3339 "p10": 900,
3340 "p50": 1600
3341 },
3342 {
3343 "measurement": "b",
3344 "weight": 0.30,
3345 "p10": 1200,
3346 "p50": 2400
3347 },
3348 ],
3349 "condition": {
3350 "op":"eq",
3351 "name": "event.contexts.browser.name",
3352 "value": "Chrome"
3353 }
3354 }
3355 ]
3356 }))
3357 .unwrap();
3358
3359 normalize_performance_score(&mut event, Some(&performance_score));
3360
3361 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3362 {
3363 "type": "transaction",
3364 "timestamp": 1619420405.0,
3365 "start_timestamp": 1619420400.0,
3366 "contexts": {
3367 "browser": {
3368 "name": "Chrome",
3369 "version": "120.1.1",
3370 "type": "browser",
3371 },
3372 },
3373 "measurements": {
3374 "a": {
3375 "value": 213.0,
3376 "unit": "millisecond",
3377 },
3378 },
3379 }
3380 "###);
3381 }
3382
3383 #[test]
3384 fn test_computed_performance_score_optional_measurement() {
3385 let json = r#"
3386 {
3387 "type": "transaction",
3388 "timestamp": "2021-04-26T08:00:05+0100",
3389 "start_timestamp": "2021-04-26T08:00:00+0100",
3390 "measurements": {
3391 "a": {"value": 213, "unit": "millisecond"},
3392 "b": {"value": 213, "unit": "millisecond"}
3393 },
3394 "contexts": {
3395 "browser": {
3396 "name": "Chrome",
3397 "version": "120.1.1",
3398 "type": "browser"
3399 }
3400 }
3401 }
3402 "#;
3403
3404 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3405
3406 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3407 "profiles": [
3408 {
3409 "name": "Desktop",
3410 "scoreComponents": [
3411 {
3412 "measurement": "a",
3413 "weight": 0.15,
3414 "p10": 900,
3415 "p50": 1600,
3416 },
3417 {
3418 "measurement": "b",
3419 "weight": 0.30,
3420 "p10": 1200,
3421 "p50": 2400,
3422 "optional": true
3423 },
3424 {
3425 "measurement": "c",
3426 "weight": 0.55,
3427 "p10": 1200,
3428 "p50": 2400,
3429 "optional": true
3430 },
3431 ],
3432 "condition": {
3433 "op":"eq",
3434 "name": "event.contexts.browser.name",
3435 "value": "Chrome"
3436 }
3437 }
3438 ]
3439 }))
3440 .unwrap();
3441
3442 normalize_performance_score(&mut event, Some(&performance_score));
3443
3444 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3445 {
3446 "type": "transaction",
3447 "timestamp": 1619420405.0,
3448 "start_timestamp": 1619420400.0,
3449 "contexts": {
3450 "browser": {
3451 "name": "Chrome",
3452 "version": "120.1.1",
3453 "type": "browser",
3454 },
3455 },
3456 "measurements": {
3457 "a": {
3458 "value": 213.0,
3459 "unit": "millisecond",
3460 },
3461 "b": {
3462 "value": 213.0,
3463 "unit": "millisecond",
3464 },
3465 "score.a": {
3466 "value": 0.33333215313291975,
3467 "unit": "ratio",
3468 },
3469 "score.b": {
3470 "value": 0.66666415149198,
3471 "unit": "ratio",
3472 },
3473 "score.ratio.a": {
3474 "value": 0.9999964593987591,
3475 "unit": "ratio",
3476 },
3477 "score.ratio.b": {
3478 "value": 0.9999962272379699,
3479 "unit": "ratio",
3480 },
3481 "score.total": {
3482 "value": 0.9999963046248997,
3483 "unit": "ratio",
3484 },
3485 "score.weight.a": {
3486 "value": 0.33333333333333337,
3487 "unit": "ratio",
3488 },
3489 "score.weight.b": {
3490 "value": 0.6666666666666667,
3491 "unit": "ratio",
3492 },
3493 "score.weight.c": {
3494 "value": 0.0,
3495 "unit": "ratio",
3496 },
3497 },
3498 }
3499 "###);
3500 }
3501
3502 #[test]
3503 fn test_computed_performance_score_weight_0() {
3504 let json = r#"
3505 {
3506 "type": "transaction",
3507 "timestamp": "2021-04-26T08:00:05+0100",
3508 "start_timestamp": "2021-04-26T08:00:00+0100",
3509 "measurements": {
3510 "cls": {"value": 0.11}
3511 }
3512 }
3513 "#;
3514
3515 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3516
3517 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3518 "profiles": [
3519 {
3520 "name": "Desktop",
3521 "scoreComponents": [
3522 {
3523 "measurement": "cls",
3524 "weight": 0,
3525 "p10": 0.1,
3526 "p50": 0.25
3527 },
3528 ],
3529 "condition": {
3530 "op":"and",
3531 "inner": []
3532 }
3533 }
3534 ]
3535 }))
3536 .unwrap();
3537
3538 normalize_performance_score(&mut event, Some(&performance_score));
3539
3540 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3541 {
3542 "type": "transaction",
3543 "timestamp": 1619420405.0,
3544 "start_timestamp": 1619420400.0,
3545 "measurements": {
3546 "cls": {
3547 "value": 0.11,
3548 },
3549 },
3550 }
3551 "###);
3552 }
3553
3554 #[test]
3555 fn test_computed_performance_score_negative_value() {
3556 let json = r#"
3557 {
3558 "type": "transaction",
3559 "timestamp": "2021-04-26T08:00:05+0100",
3560 "start_timestamp": "2021-04-26T08:00:00+0100",
3561 "measurements": {
3562 "ttfb": {"value": -100, "unit": "millisecond"}
3563 }
3564 }
3565 "#;
3566
3567 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3568
3569 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3570 "profiles": [
3571 {
3572 "name": "Desktop",
3573 "scoreComponents": [
3574 {
3575 "measurement": "ttfb",
3576 "weight": 1.0,
3577 "p10": 100.0,
3578 "p50": 250.0
3579 },
3580 ],
3581 "condition": {
3582 "op":"and",
3583 "inner": []
3584 }
3585 }
3586 ]
3587 }))
3588 .unwrap();
3589
3590 normalize_performance_score(&mut event, Some(&performance_score));
3591
3592 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3593 {
3594 "type": "transaction",
3595 "timestamp": 1619420405.0,
3596 "start_timestamp": 1619420400.0,
3597 "measurements": {
3598 "score.ratio.ttfb": {
3599 "value": 1.0,
3600 "unit": "ratio",
3601 },
3602 "score.total": {
3603 "value": 1.0,
3604 "unit": "ratio",
3605 },
3606 "score.ttfb": {
3607 "value": 1.0,
3608 "unit": "ratio",
3609 },
3610 "score.weight.ttfb": {
3611 "value": 1.0,
3612 "unit": "ratio",
3613 },
3614 "ttfb": {
3615 "value": -100.0,
3616 "unit": "millisecond",
3617 },
3618 },
3619 }
3620 "###);
3621 }
3622
3623 #[test]
3624 fn test_filter_negative_web_vital_measurements() {
3625 let json = r#"
3626 {
3627 "type": "transaction",
3628 "timestamp": "2021-04-26T08:00:05+0100",
3629 "start_timestamp": "2021-04-26T08:00:00+0100",
3630 "measurements": {
3631 "ttfb": {"value": -100, "unit": "millisecond"}
3632 }
3633 }
3634 "#;
3635 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3636
3637 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3639 "builtinMeasurements": [
3640 {"name": "ttfb", "unit": "millisecond"},
3641 ],
3642 }))
3643 .unwrap();
3644
3645 let dynamic_measurement_config =
3646 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3647
3648 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3649
3650 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3651 {
3652 "type": "transaction",
3653 "timestamp": 1619420405.0,
3654 "start_timestamp": 1619420400.0,
3655 "measurements": {},
3656 "_meta": {
3657 "measurements": {
3658 "": Meta(Some(MetaInner(
3659 err: [
3660 [
3661 "invalid_data",
3662 {
3663 "reason": "Negative value for measurement ttfb not allowed: -100",
3664 },
3665 ],
3666 ],
3667 val: Some({
3668 "ttfb": {
3669 "unit": "millisecond",
3670 "value": -100.0,
3671 },
3672 }),
3673 ))),
3674 },
3675 },
3676 }
3677 "###);
3678 }
3679
3680 #[test]
3681 fn test_computed_performance_score_multiple_profiles() {
3682 let json = r#"
3683 {
3684 "type": "transaction",
3685 "timestamp": "2021-04-26T08:00:05+0100",
3686 "start_timestamp": "2021-04-26T08:00:00+0100",
3687 "measurements": {
3688 "cls": {"value": 0.11},
3689 "inp": {"value": 120.0}
3690 }
3691 }
3692 "#;
3693
3694 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3695
3696 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3697 "profiles": [
3698 {
3699 "name": "Desktop",
3700 "scoreComponents": [
3701 {
3702 "measurement": "cls",
3703 "weight": 0,
3704 "p10": 0.1,
3705 "p50": 0.25
3706 },
3707 ],
3708 "condition": {
3709 "op":"and",
3710 "inner": []
3711 }
3712 },
3713 {
3714 "name": "Desktop",
3715 "scoreComponents": [
3716 {
3717 "measurement": "inp",
3718 "weight": 1.0,
3719 "p10": 0.1,
3720 "p50": 0.25
3721 },
3722 ],
3723 "condition": {
3724 "op":"and",
3725 "inner": []
3726 }
3727 }
3728 ]
3729 }))
3730 .unwrap();
3731
3732 normalize_performance_score(&mut event, Some(&performance_score));
3733
3734 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3735 {
3736 "type": "transaction",
3737 "timestamp": 1619420405.0,
3738 "start_timestamp": 1619420400.0,
3739 "measurements": {
3740 "cls": {
3741 "value": 0.11,
3742 },
3743 "inp": {
3744 "value": 120.0,
3745 },
3746 "score.inp": {
3747 "value": 0.0,
3748 "unit": "ratio",
3749 },
3750 "score.ratio.inp": {
3751 "value": 0.0,
3752 "unit": "ratio",
3753 },
3754 "score.total": {
3755 "value": 0.0,
3756 "unit": "ratio",
3757 },
3758 "score.weight.inp": {
3759 "value": 1.0,
3760 "unit": "ratio",
3761 },
3762 },
3763 }
3764 "###);
3765 }
3766
3767 #[test]
3768 fn test_compute_performance_score_for_mobile_ios_profile() {
3769 let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3770 .unwrap()
3771 .0
3772 .unwrap();
3773
3774 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3775 "profiles": [
3776 {
3777 "name": "Mobile",
3778 "scoreComponents": [
3779 {
3780 "measurement": "time_to_initial_display",
3781 "weight": 0.25,
3782 "p10": 1800.0,
3783 "p50": 3000.0,
3784 "optional": true
3785 },
3786 {
3787 "measurement": "time_to_full_display",
3788 "weight": 0.25,
3789 "p10": 2500.0,
3790 "p50": 4000.0,
3791 "optional": true
3792 },
3793 {
3794 "measurement": "app_start_warm",
3795 "weight": 0.25,
3796 "p10": 200.0,
3797 "p50": 500.0,
3798 "optional": true
3799 },
3800 {
3801 "measurement": "app_start_cold",
3802 "weight": 0.25,
3803 "p10": 200.0,
3804 "p50": 500.0,
3805 "optional": true
3806 }
3807 ],
3808 "condition": {
3809 "op": "and",
3810 "inner": [
3811 {
3812 "op": "or",
3813 "inner": [
3814 {
3815 "op": "eq",
3816 "name": "event.sdk.name",
3817 "value": "sentry.cocoa"
3818 },
3819 {
3820 "op": "eq",
3821 "name": "event.sdk.name",
3822 "value": "sentry.java.android"
3823 }
3824 ]
3825 },
3826 {
3827 "op": "eq",
3828 "name": "event.contexts.trace.op",
3829 "value": "ui.load"
3830 }
3831 ]
3832 }
3833 }
3834 ]
3835 }))
3836 .unwrap();
3837
3838 normalize_performance_score(&mut event, Some(&performance_score));
3839
3840 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3841 }
3842
3843 #[test]
3844 fn test_compute_performance_score_for_mobile_android_profile() {
3845 let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3846 .unwrap()
3847 .0
3848 .unwrap();
3849
3850 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3851 "profiles": [
3852 {
3853 "name": "Mobile",
3854 "scoreComponents": [
3855 {
3856 "measurement": "time_to_initial_display",
3857 "weight": 0.25,
3858 "p10": 1800.0,
3859 "p50": 3000.0,
3860 "optional": true
3861 },
3862 {
3863 "measurement": "time_to_full_display",
3864 "weight": 0.25,
3865 "p10": 2500.0,
3866 "p50": 4000.0,
3867 "optional": true
3868 },
3869 {
3870 "measurement": "app_start_warm",
3871 "weight": 0.25,
3872 "p10": 200.0,
3873 "p50": 500.0,
3874 "optional": true
3875 },
3876 {
3877 "measurement": "app_start_cold",
3878 "weight": 0.25,
3879 "p10": 200.0,
3880 "p50": 500.0,
3881 "optional": true
3882 }
3883 ],
3884 "condition": {
3885 "op": "and",
3886 "inner": [
3887 {
3888 "op": "or",
3889 "inner": [
3890 {
3891 "op": "eq",
3892 "name": "event.sdk.name",
3893 "value": "sentry.cocoa"
3894 },
3895 {
3896 "op": "eq",
3897 "name": "event.sdk.name",
3898 "value": "sentry.java.android"
3899 }
3900 ]
3901 },
3902 {
3903 "op": "eq",
3904 "name": "event.contexts.trace.op",
3905 "value": "ui.load"
3906 }
3907 ]
3908 }
3909 }
3910 ]
3911 }))
3912 .unwrap();
3913
3914 normalize_performance_score(&mut event, Some(&performance_score));
3915
3916 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3917 }
3918
3919 #[test]
3920 fn test_computes_performance_score_and_tags_with_profile_version() {
3921 let json = r#"
3922 {
3923 "type": "transaction",
3924 "timestamp": "2021-04-26T08:00:05+0100",
3925 "start_timestamp": "2021-04-26T08:00:00+0100",
3926 "measurements": {
3927 "inp": {"value": 120.0}
3928 }
3929 }
3930 "#;
3931
3932 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3933
3934 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3935 "profiles": [
3936 {
3937 "name": "Desktop",
3938 "scoreComponents": [
3939 {
3940 "measurement": "inp",
3941 "weight": 1.0,
3942 "p10": 0.1,
3943 "p50": 0.25
3944 },
3945 ],
3946 "condition": {
3947 "op":"and",
3948 "inner": []
3949 },
3950 "version": "beta"
3951 }
3952 ]
3953 }))
3954 .unwrap();
3955
3956 normalize(
3957 &mut event,
3958 &mut Meta::default(),
3959 &NormalizationConfig {
3960 performance_score: Some(&performance_score),
3961 ..Default::default()
3962 },
3963 );
3964
3965 insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3966 {
3967 "performance_score": {
3968 "score_profile_version": "beta",
3969 "type": "performancescore",
3970 },
3971 }
3972 "###);
3973 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3974 {
3975 "inp": {
3976 "value": 120.0,
3977 "unit": "millisecond",
3978 },
3979 "score.inp": {
3980 "value": 0.0,
3981 "unit": "ratio",
3982 },
3983 "score.ratio.inp": {
3984 "value": 0.0,
3985 "unit": "ratio",
3986 },
3987 "score.total": {
3988 "value": 0.0,
3989 "unit": "ratio",
3990 },
3991 "score.weight.inp": {
3992 "value": 1.0,
3993 "unit": "ratio",
3994 },
3995 }
3996 "###);
3997 }
3998
3999 #[test]
4000 fn test_computes_standalone_cls_performance_score() {
4001 let json = r#"
4002 {
4003 "type": "transaction",
4004 "timestamp": "2021-04-26T08:00:05+0100",
4005 "start_timestamp": "2021-04-26T08:00:00+0100",
4006 "measurements": {
4007 "cls": {"value": 0.5}
4008 }
4009 }
4010 "#;
4011
4012 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4013
4014 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4015 "profiles": [
4016 {
4017 "name": "Default",
4018 "scoreComponents": [
4019 {
4020 "measurement": "fcp",
4021 "weight": 0.15,
4022 "p10": 900.0,
4023 "p50": 1600.0,
4024 "optional": true,
4025 },
4026 {
4027 "measurement": "lcp",
4028 "weight": 0.30,
4029 "p10": 1200.0,
4030 "p50": 2400.0,
4031 "optional": true,
4032 },
4033 {
4034 "measurement": "cls",
4035 "weight": 0.15,
4036 "p10": 0.1,
4037 "p50": 0.25,
4038 "optional": true,
4039 },
4040 {
4041 "measurement": "ttfb",
4042 "weight": 0.10,
4043 "p10": 200.0,
4044 "p50": 400.0,
4045 "optional": true,
4046 },
4047 ],
4048 "condition": {
4049 "op": "and",
4050 "inner": [],
4051 },
4052 }
4053 ]
4054 }))
4055 .unwrap();
4056
4057 normalize(
4058 &mut event,
4059 &mut Meta::default(),
4060 &NormalizationConfig {
4061 performance_score: Some(&performance_score),
4062 ..Default::default()
4063 },
4064 );
4065
4066 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4067 {
4068 "cls": {
4069 "value": 0.5,
4070 "unit": "none",
4071 },
4072 "score.cls": {
4073 "value": 0.16615877613713903,
4074 "unit": "ratio",
4075 },
4076 "score.ratio.cls": {
4077 "value": 0.16615877613713903,
4078 "unit": "ratio",
4079 },
4080 "score.total": {
4081 "value": 0.16615877613713903,
4082 "unit": "ratio",
4083 },
4084 "score.weight.cls": {
4085 "value": 1.0,
4086 "unit": "ratio",
4087 },
4088 "score.weight.fcp": {
4089 "value": 0.0,
4090 "unit": "ratio",
4091 },
4092 "score.weight.lcp": {
4093 "value": 0.0,
4094 "unit": "ratio",
4095 },
4096 "score.weight.ttfb": {
4097 "value": 0.0,
4098 "unit": "ratio",
4099 },
4100 }
4101 "###);
4102 }
4103
4104 #[test]
4105 fn test_computes_standalone_lcp_performance_score() {
4106 let json = r#"
4107 {
4108 "type": "transaction",
4109 "timestamp": "2021-04-26T08:00:05+0100",
4110 "start_timestamp": "2021-04-26T08:00:00+0100",
4111 "measurements": {
4112 "lcp": {"value": 1200.0}
4113 }
4114 }
4115 "#;
4116
4117 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4118
4119 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4120 "profiles": [
4121 {
4122 "name": "Default",
4123 "scoreComponents": [
4124 {
4125 "measurement": "fcp",
4126 "weight": 0.15,
4127 "p10": 900.0,
4128 "p50": 1600.0,
4129 "optional": true,
4130 },
4131 {
4132 "measurement": "lcp",
4133 "weight": 0.30,
4134 "p10": 1200.0,
4135 "p50": 2400.0,
4136 "optional": true,
4137 },
4138 {
4139 "measurement": "cls",
4140 "weight": 0.15,
4141 "p10": 0.1,
4142 "p50": 0.25,
4143 "optional": true,
4144 },
4145 {
4146 "measurement": "ttfb",
4147 "weight": 0.10,
4148 "p10": 200.0,
4149 "p50": 400.0,
4150 "optional": true,
4151 },
4152 ],
4153 "condition": {
4154 "op": "and",
4155 "inner": [],
4156 },
4157 }
4158 ]
4159 }))
4160 .unwrap();
4161
4162 normalize(
4163 &mut event,
4164 &mut Meta::default(),
4165 &NormalizationConfig {
4166 performance_score: Some(&performance_score),
4167 ..Default::default()
4168 },
4169 );
4170
4171 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4172 {
4173 "lcp": {
4174 "value": 1200.0,
4175 "unit": "millisecond",
4176 },
4177 "score.lcp": {
4178 "value": 0.8999999314038525,
4179 "unit": "ratio",
4180 },
4181 "score.ratio.lcp": {
4182 "value": 0.8999999314038525,
4183 "unit": "ratio",
4184 },
4185 "score.total": {
4186 "value": 0.8999999314038525,
4187 "unit": "ratio",
4188 },
4189 "score.weight.cls": {
4190 "value": 0.0,
4191 "unit": "ratio",
4192 },
4193 "score.weight.fcp": {
4194 "value": 0.0,
4195 "unit": "ratio",
4196 },
4197 "score.weight.lcp": {
4198 "value": 1.0,
4199 "unit": "ratio",
4200 },
4201 "score.weight.ttfb": {
4202 "value": 0.0,
4203 "unit": "ratio",
4204 },
4205 }
4206 "###);
4207 }
4208
4209 #[test]
4210 fn test_computed_performance_score_uses_first_matching_profile() {
4211 let json = r#"
4212 {
4213 "type": "transaction",
4214 "timestamp": "2021-04-26T08:00:05+0100",
4215 "start_timestamp": "2021-04-26T08:00:00+0100",
4216 "measurements": {
4217 "a": {"value": 213, "unit": "millisecond"},
4218 "b": {"value": 213, "unit": "millisecond"}
4219 },
4220 "contexts": {
4221 "browser": {
4222 "name": "Chrome",
4223 "version": "120.1.1",
4224 "type": "browser"
4225 }
4226 }
4227 }
4228 "#;
4229
4230 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4231
4232 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4233 "profiles": [
4234 {
4235 "name": "Mobile",
4236 "scoreComponents": [
4237 {
4238 "measurement": "a",
4239 "weight": 0.15,
4240 "p10": 100,
4241 "p50": 200,
4242 },
4243 {
4244 "measurement": "b",
4245 "weight": 0.30,
4246 "p10": 100,
4247 "p50": 200,
4248 "optional": true
4249 },
4250 {
4251 "measurement": "c",
4252 "weight": 0.55,
4253 "p10": 100,
4254 "p50": 200,
4255 "optional": true
4256 },
4257 ],
4258 "condition": {
4259 "op":"eq",
4260 "name": "event.contexts.browser.name",
4261 "value": "Chrome Mobile"
4262 }
4263 },
4264 {
4265 "name": "Desktop",
4266 "scoreComponents": [
4267 {
4268 "measurement": "a",
4269 "weight": 0.15,
4270 "p10": 900,
4271 "p50": 1600,
4272 },
4273 {
4274 "measurement": "b",
4275 "weight": 0.30,
4276 "p10": 1200,
4277 "p50": 2400,
4278 "optional": true
4279 },
4280 {
4281 "measurement": "c",
4282 "weight": 0.55,
4283 "p10": 1200,
4284 "p50": 2400,
4285 "optional": true
4286 },
4287 ],
4288 "condition": {
4289 "op":"eq",
4290 "name": "event.contexts.browser.name",
4291 "value": "Chrome"
4292 }
4293 },
4294 {
4295 "name": "Default",
4296 "scoreComponents": [
4297 {
4298 "measurement": "a",
4299 "weight": 0.15,
4300 "p10": 100,
4301 "p50": 200,
4302 },
4303 {
4304 "measurement": "b",
4305 "weight": 0.30,
4306 "p10": 100,
4307 "p50": 200,
4308 "optional": true
4309 },
4310 {
4311 "measurement": "c",
4312 "weight": 0.55,
4313 "p10": 100,
4314 "p50": 200,
4315 "optional": true
4316 },
4317 ],
4318 "condition": {
4319 "op": "and",
4320 "inner": [],
4321 }
4322 }
4323 ]
4324 }))
4325 .unwrap();
4326
4327 normalize_performance_score(&mut event, Some(&performance_score));
4328
4329 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4330 {
4331 "type": "transaction",
4332 "timestamp": 1619420405.0,
4333 "start_timestamp": 1619420400.0,
4334 "contexts": {
4335 "browser": {
4336 "name": "Chrome",
4337 "version": "120.1.1",
4338 "type": "browser",
4339 },
4340 },
4341 "measurements": {
4342 "a": {
4343 "value": 213.0,
4344 "unit": "millisecond",
4345 },
4346 "b": {
4347 "value": 213.0,
4348 "unit": "millisecond",
4349 },
4350 "score.a": {
4351 "value": 0.33333215313291975,
4352 "unit": "ratio",
4353 },
4354 "score.b": {
4355 "value": 0.66666415149198,
4356 "unit": "ratio",
4357 },
4358 "score.ratio.a": {
4359 "value": 0.9999964593987591,
4360 "unit": "ratio",
4361 },
4362 "score.ratio.b": {
4363 "value": 0.9999962272379699,
4364 "unit": "ratio",
4365 },
4366 "score.total": {
4367 "value": 0.9999963046248997,
4368 "unit": "ratio",
4369 },
4370 "score.weight.a": {
4371 "value": 0.33333333333333337,
4372 "unit": "ratio",
4373 },
4374 "score.weight.b": {
4375 "value": 0.6666666666666667,
4376 "unit": "ratio",
4377 },
4378 "score.weight.c": {
4379 "value": 0.0,
4380 "unit": "ratio",
4381 },
4382 },
4383 }
4384 "###);
4385 }
4386
4387 #[test]
4388 fn test_computed_performance_score_falls_back_to_default_profile() {
4389 let json = r#"
4390 {
4391 "type": "transaction",
4392 "timestamp": "2021-04-26T08:00:05+0100",
4393 "start_timestamp": "2021-04-26T08:00:00+0100",
4394 "measurements": {
4395 "a": {"value": 213, "unit": "millisecond"},
4396 "b": {"value": 213, "unit": "millisecond"}
4397 },
4398 "contexts": {}
4399 }
4400 "#;
4401
4402 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4403
4404 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4405 "profiles": [
4406 {
4407 "name": "Mobile",
4408 "scoreComponents": [
4409 {
4410 "measurement": "a",
4411 "weight": 0.15,
4412 "p10": 900,
4413 "p50": 1600,
4414 "optional": true
4415 },
4416 {
4417 "measurement": "b",
4418 "weight": 0.30,
4419 "p10": 1200,
4420 "p50": 2400,
4421 "optional": true
4422 },
4423 {
4424 "measurement": "c",
4425 "weight": 0.55,
4426 "p10": 1200,
4427 "p50": 2400,
4428 "optional": true
4429 },
4430 ],
4431 "condition": {
4432 "op":"eq",
4433 "name": "event.contexts.browser.name",
4434 "value": "Chrome Mobile"
4435 }
4436 },
4437 {
4438 "name": "Desktop",
4439 "scoreComponents": [
4440 {
4441 "measurement": "a",
4442 "weight": 0.15,
4443 "p10": 900,
4444 "p50": 1600,
4445 "optional": true
4446 },
4447 {
4448 "measurement": "b",
4449 "weight": 0.30,
4450 "p10": 1200,
4451 "p50": 2400,
4452 "optional": true
4453 },
4454 {
4455 "measurement": "c",
4456 "weight": 0.55,
4457 "p10": 1200,
4458 "p50": 2400,
4459 "optional": true
4460 },
4461 ],
4462 "condition": {
4463 "op":"eq",
4464 "name": "event.contexts.browser.name",
4465 "value": "Chrome"
4466 }
4467 },
4468 {
4469 "name": "Default",
4470 "scoreComponents": [
4471 {
4472 "measurement": "a",
4473 "weight": 0.15,
4474 "p10": 100,
4475 "p50": 200,
4476 "optional": true
4477 },
4478 {
4479 "measurement": "b",
4480 "weight": 0.30,
4481 "p10": 100,
4482 "p50": 200,
4483 "optional": true
4484 },
4485 {
4486 "measurement": "c",
4487 "weight": 0.55,
4488 "p10": 100,
4489 "p50": 200,
4490 "optional": true
4491 },
4492 ],
4493 "condition": {
4494 "op": "and",
4495 "inner": [],
4496 }
4497 }
4498 ]
4499 }))
4500 .unwrap();
4501
4502 normalize_performance_score(&mut event, Some(&performance_score));
4503
4504 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4505 {
4506 "type": "transaction",
4507 "timestamp": 1619420405.0,
4508 "start_timestamp": 1619420400.0,
4509 "contexts": {},
4510 "measurements": {
4511 "a": {
4512 "value": 213.0,
4513 "unit": "millisecond",
4514 },
4515 "b": {
4516 "value": 213.0,
4517 "unit": "millisecond",
4518 },
4519 "score.a": {
4520 "value": 0.15121816827413334,
4521 "unit": "ratio",
4522 },
4523 "score.b": {
4524 "value": 0.3024363365482667,
4525 "unit": "ratio",
4526 },
4527 "score.ratio.a": {
4528 "value": 0.45365450482239994,
4529 "unit": "ratio",
4530 },
4531 "score.ratio.b": {
4532 "value": 0.45365450482239994,
4533 "unit": "ratio",
4534 },
4535 "score.total": {
4536 "value": 0.4536545048224,
4537 "unit": "ratio",
4538 },
4539 "score.weight.a": {
4540 "value": 0.33333333333333337,
4541 "unit": "ratio",
4542 },
4543 "score.weight.b": {
4544 "value": 0.6666666666666667,
4545 "unit": "ratio",
4546 },
4547 "score.weight.c": {
4548 "value": 0.0,
4549 "unit": "ratio",
4550 },
4551 },
4552 }
4553 "###);
4554 }
4555
4556 #[test]
4557 fn test_normalization_removes_reprocessing_context() {
4558 let json = r#"{
4559 "contexts": {
4560 "reprocessing": {}
4561 }
4562 }"#;
4563 let mut event = Annotated::<Event>::from_json(json).unwrap();
4564 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4565 normalize_event(&mut event, &NormalizationConfig::default());
4566 assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4567 }
4568
4569 #[test]
4570 fn test_renormalization_does_not_remove_reprocessing_context() {
4571 let json = r#"{
4572 "contexts": {
4573 "reprocessing": {}
4574 }
4575 }"#;
4576 let mut event = Annotated::<Event>::from_json(json).unwrap();
4577 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4578 normalize_event(
4579 &mut event,
4580 &NormalizationConfig {
4581 is_renormalize: true,
4582 ..Default::default()
4583 },
4584 );
4585 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4586 }
4587
4588 #[test]
4589 fn test_normalize_user() {
4590 let json = r#"{
4591 "user": {
4592 "id": "123456",
4593 "username": "john",
4594 "other": "value"
4595 }
4596 }"#;
4597 let mut event = Annotated::<Event>::from_json(json).unwrap();
4598 normalize_user(event.value_mut().as_mut().unwrap());
4599
4600 let user = event.value().unwrap().user.value().unwrap();
4601 assert_eq!(user.data, {
4602 let mut map = Object::new();
4603 map.insert(
4604 "other".to_owned(),
4605 Annotated::new(Value::String("value".to_owned())),
4606 );
4607 Annotated::new(map)
4608 });
4609 assert_eq!(user.other, Object::new());
4610 assert_eq!(user.username, Annotated::new("john".to_owned().into()));
4611 assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
4612 }
4613
4614 #[test]
4615 fn test_handle_types_in_spaced_exception_values() {
4616 let mut exception = Annotated::new(Exception {
4617 value: Annotated::new("ValueError: unauthorized".to_owned().into()),
4618 ..Exception::default()
4619 });
4620 normalize_exception(&mut exception);
4621
4622 let exception = exception.value().unwrap();
4623 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4624 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4625 }
4626
4627 #[test]
4628 fn test_handle_types_in_non_spaced_excepton_values() {
4629 let mut exception = Annotated::new(Exception {
4630 value: Annotated::new("ValueError:unauthorized".to_owned().into()),
4631 ..Exception::default()
4632 });
4633 normalize_exception(&mut exception);
4634
4635 let exception = exception.value().unwrap();
4636 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4637 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4638 }
4639
4640 #[test]
4641 fn test_rejects_empty_exception_fields() {
4642 let mut exception = Annotated::new(Exception {
4643 value: Annotated::new("".to_owned().into()),
4644 ty: Annotated::new("".to_owned()),
4645 ..Default::default()
4646 });
4647
4648 normalize_exception(&mut exception);
4649
4650 assert!(exception.value().is_none());
4651 assert!(exception.meta().has_errors());
4652 }
4653
4654 #[test]
4655 fn test_json_value() {
4656 let mut exception = Annotated::new(Exception {
4657 value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
4658 ..Exception::default()
4659 });
4660
4661 normalize_exception(&mut exception);
4662
4663 let exception = exception.value().unwrap();
4664
4665 assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4667 assert_eq!(exception.ty.value(), None);
4668 }
4669
4670 #[test]
4671 fn test_exception_invalid() {
4672 let mut exception = Annotated::new(Exception::default());
4673
4674 normalize_exception(&mut exception);
4675
4676 let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4677 error.insert("attribute", "type or value");
4678 });
4679 assert_eq!(
4680 exception.meta().iter_errors().collect_tuple(),
4681 Some((&expected,))
4682 );
4683 }
4684
4685 #[test]
4686 fn test_normalize_exception() {
4687 let mut event = Annotated::new(Event {
4688 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4689 ty: Annotated::empty(),
4691 value: Annotated::empty(),
4692 ..Default::default()
4693 })])),
4694 ..Default::default()
4695 });
4696
4697 normalize_event(&mut event, &NormalizationConfig::default());
4698
4699 let exception = event
4700 .value()
4701 .unwrap()
4702 .exceptions
4703 .value()
4704 .unwrap()
4705 .values
4706 .value()
4707 .unwrap()
4708 .first()
4709 .unwrap();
4710
4711 assert_debug_snapshot!(exception.meta(), @r###"
4712 Meta {
4713 remarks: [],
4714 errors: [
4715 Error {
4716 kind: MissingAttribute,
4717 data: {
4718 "attribute": String(
4719 "type or value",
4720 ),
4721 },
4722 },
4723 ],
4724 original_length: None,
4725 original_value: Some(
4726 Object(
4727 {
4728 "mechanism": ~,
4729 "module": ~,
4730 "raw_stacktrace": ~,
4731 "stacktrace": ~,
4732 "thread_id": ~,
4733 "type": ~,
4734 "value": ~,
4735 },
4736 ),
4737 ),
4738 }
4739 "###);
4740 }
4741
4742 #[test]
4743 fn test_normalize_breadcrumbs() {
4744 let mut event = Event {
4745 breadcrumbs: Annotated::new(Values {
4746 values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4747 ..Default::default()
4748 }),
4749 ..Default::default()
4750 };
4751 normalize_breadcrumbs(&mut event);
4752
4753 let breadcrumb = event
4754 .breadcrumbs
4755 .value()
4756 .unwrap()
4757 .values
4758 .value()
4759 .unwrap()
4760 .first()
4761 .unwrap()
4762 .value()
4763 .unwrap();
4764 assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4765 assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4766 }
4767
4768 #[test]
4769 fn test_other_debug_images_have_meta_errors() {
4770 let mut event = Event {
4771 debug_meta: Annotated::new(DebugMeta {
4772 images: Annotated::new(vec![Annotated::new(
4773 DebugImage::Other(BTreeMap::default()),
4774 )]),
4775 ..Default::default()
4776 }),
4777 ..Default::default()
4778 };
4779 normalize_debug_meta(&mut event);
4780
4781 let debug_image_meta = event
4782 .debug_meta
4783 .value()
4784 .unwrap()
4785 .images
4786 .value()
4787 .unwrap()
4788 .first()
4789 .unwrap()
4790 .meta();
4791 assert_debug_snapshot!(debug_image_meta, @r###"
4792 Meta {
4793 remarks: [],
4794 errors: [
4795 Error {
4796 kind: InvalidData,
4797 data: {
4798 "reason": String(
4799 "unsupported debug image type",
4800 ),
4801 },
4802 },
4803 ],
4804 original_length: None,
4805 original_value: Some(
4806 Object(
4807 {},
4808 ),
4809 ),
4810 }
4811 "###);
4812 }
4813
4814 #[test]
4815 fn test_skip_span_normalization_when_configured() {
4816 let json = r#"{
4817 "type": "transaction",
4818 "start_timestamp": 1,
4819 "timestamp": 2,
4820 "contexts": {
4821 "trace": {
4822 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4823 "span_id": "aaaaaaaaaaaaaaaa"
4824 }
4825 },
4826 "spans": [
4827 {
4828 "op": "db",
4829 "description": "SELECT * FROM table;",
4830 "start_timestamp": 1,
4831 "timestamp": 2,
4832 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4833 "span_id": "bbbbbbbbbbbbbbbb",
4834 "parent_span_id": "aaaaaaaaaaaaaaaa"
4835 }
4836 ]
4837 }"#;
4838
4839 let mut event = Annotated::<Event>::from_json(json).unwrap();
4840 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4841 normalize_event(
4842 &mut event,
4843 &NormalizationConfig {
4844 is_renormalize: true,
4845 ..Default::default()
4846 },
4847 );
4848 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4849 normalize_event(
4850 &mut event,
4851 &NormalizationConfig {
4852 is_renormalize: false,
4853 ..Default::default()
4854 },
4855 );
4856 assert!(get_value!(event.spans[0].exclusive_time).is_some());
4857 }
4858
4859 #[test]
4860 fn test_normalize_trace_context_tags_extracts_lcp_info() {
4861 let json = r#"{
4862 "type": "transaction",
4863 "start_timestamp": 1,
4864 "timestamp": 2,
4865 "contexts": {
4866 "trace": {
4867 "data": {
4868 "lcp.element": "body > div#app > div > h1#header",
4869 "lcp.size": 24827,
4870 "lcp.id": "header",
4871 "lcp.url": "http://example.com/image.jpg"
4872 }
4873 }
4874 },
4875 "measurements": {
4876 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4877 }
4878 }"#;
4879 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4880 normalize_trace_context_tags(&mut event);
4881 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4882 {
4883 "type": "transaction",
4884 "timestamp": 2.0,
4885 "start_timestamp": 1.0,
4886 "contexts": {
4887 "trace": {
4888 "data": {
4889 "lcp.element": "body > div#app > div > h1#header",
4890 "lcp.size": 24827,
4891 "lcp.id": "header",
4892 "lcp.url": "http://example.com/image.jpg",
4893 },
4894 "type": "trace",
4895 },
4896 },
4897 "tags": [
4898 [
4899 "lcp.element",
4900 "body > div#app > div > h1#header",
4901 ],
4902 [
4903 "lcp.size",
4904 "24827",
4905 ],
4906 [
4907 "lcp.id",
4908 "header",
4909 ],
4910 [
4911 "lcp.url",
4912 "http://example.com/image.jpg",
4913 ],
4914 ],
4915 "measurements": {
4916 "lcp": {
4917 "value": 146.20000000298023,
4918 "unit": "millisecond",
4919 },
4920 },
4921 }
4922 "###);
4923 }
4924
4925 #[test]
4926 fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4927 let json = r#"{
4928 "type": "transaction",
4929 "start_timestamp": 1,
4930 "timestamp": 2,
4931 "contexts": {
4932 "trace": {
4933 "data": {
4934 "lcp.element": "body > div#app > div > h1#id",
4935 "lcp.size": 33333,
4936 "lcp.id": "id",
4937 "lcp.url": "http://example.com/another-image.jpg"
4938 }
4939 }
4940 },
4941 "tags": {
4942 "lcp.element": "body > div#app > div > h1#header",
4943 "lcp.size": 24827,
4944 "lcp.id": "header",
4945 "lcp.url": "http://example.com/image.jpg"
4946 },
4947 "measurements": {
4948 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4949 }
4950 }"#;
4951 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4952 normalize_trace_context_tags(&mut event);
4953 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4954 {
4955 "type": "transaction",
4956 "timestamp": 2.0,
4957 "start_timestamp": 1.0,
4958 "contexts": {
4959 "trace": {
4960 "data": {
4961 "lcp.element": "body > div#app > div > h1#id",
4962 "lcp.size": 33333,
4963 "lcp.id": "id",
4964 "lcp.url": "http://example.com/another-image.jpg",
4965 },
4966 "type": "trace",
4967 },
4968 },
4969 "tags": [
4970 [
4971 "lcp.element",
4972 "body > div#app > div > h1#header",
4973 ],
4974 [
4975 "lcp.id",
4976 "header",
4977 ],
4978 [
4979 "lcp.size",
4980 "24827",
4981 ],
4982 [
4983 "lcp.url",
4984 "http://example.com/image.jpg",
4985 ],
4986 ],
4987 "measurements": {
4988 "lcp": {
4989 "value": 146.20000000298023,
4990 "unit": "millisecond",
4991 },
4992 },
4993 }
4994 "###);
4995 }
4996
4997 #[test]
4998 fn test_tags_are_trimmed() {
4999 let json = r#"
5000 {
5001 "tags": {
5002 "key": "too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_",
5003 "too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_": "value"
5004 }
5005 }
5006 "#;
5007
5008 let mut event = Annotated::<Event>::from_json(json).unwrap();
5009
5010 normalize_event(
5011 &mut event,
5012 &NormalizationConfig {
5013 enable_trimming: true,
5014 ..NormalizationConfig::default()
5015 },
5016 );
5017
5018 insta::assert_debug_snapshot!(get_value!(event.tags!), @r###"
5019 Tags(
5020 PairList(
5021 [
5022 TagEntry(
5023 "key",
5024 Annotated(
5025 "too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__lo...",
5026 Meta {
5027 remarks: [
5028 Remark {
5029 ty: Substituted,
5030 rule_id: "!limit",
5031 range: Some(
5032 (
5033 197,
5034 200,
5035 ),
5036 ),
5037 },
5038 ],
5039 errors: [],
5040 original_length: Some(
5041 210,
5042 ),
5043 original_value: None,
5044 },
5045 ),
5046 ),
5047 TagEntry(
5048 Annotated(
5049 "too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__long_too__lo...",
5050 Meta {
5051 remarks: [
5052 Remark {
5053 ty: Substituted,
5054 rule_id: "!limit",
5055 range: Some(
5056 (
5057 197,
5058 200,
5059 ),
5060 ),
5061 },
5062 ],
5063 errors: [],
5064 original_length: Some(
5065 210,
5066 ),
5067 original_value: None,
5068 },
5069 ),
5070 "value",
5071 ),
5072 ],
5073 ),
5074 )
5075 "###);
5076 }
5077}