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.response.model": "claude-2.1",
2329 "gen_ai.request.model": "claude-2.1",
2330 "gen_ai.cost.total_tokens": 50.0,
2331 "gen_ai.cost.input_tokens": 10.0,
2332 "gen_ai.cost.output_tokens": 40.0,
2333 "gen_ai.response.tokens_per_second": 62500.0,
2334 "gen_ai.operation.type": "ai_client"
2335 }
2336 "#);
2337 assert_annotated_snapshot!(span2, @r#"
2338 {
2339 "gen_ai.usage.total_tokens": 3000.0,
2340 "gen_ai.usage.input_tokens": 1000.0,
2341 "gen_ai.usage.output_tokens": 2000.0,
2342 "gen_ai.response.model": "gpt4-21-04",
2343 "gen_ai.request.model": "gpt4-21-04",
2344 "gen_ai.cost.total_tokens": 80.0,
2345 "gen_ai.cost.input_tokens": 20.0,
2346 "gen_ai.cost.output_tokens": 60.0,
2347 "gen_ai.response.tokens_per_second": 62500.0,
2348 "gen_ai.operation.type": "ai_client"
2349 }
2350 "#);
2351 }
2352
2353 #[test]
2354 fn test_ai_data() {
2355 let json = r#"
2356 {
2357 "spans": [
2358 {
2359 "timestamp": 1702474614.0175,
2360 "start_timestamp": 1702474613.0175,
2361 "description": "OpenAI ",
2362 "op": "gen_ai.chat_completions.openai",
2363 "span_id": "9c01bd820a083e63",
2364 "parent_span_id": "a1e13f3f06239d69",
2365 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2366 "data": {
2367 "gen_ai.usage.input_tokens": 1000,
2368 "gen_ai.usage.output_tokens": 2000,
2369 "gen_ai.usage.output_tokens.reasoning": 1000,
2370 "gen_ai.usage.input_tokens.cached": 500,
2371 "gen_ai.request.model": "claude-2.1"
2372 }
2373 },
2374 {
2375 "timestamp": 1702474614.0175,
2376 "start_timestamp": 1702474613.0175,
2377 "description": "OpenAI ",
2378 "op": "gen_ai.chat_completions.openai",
2379 "span_id": "ac01bd820a083e63",
2380 "parent_span_id": "a1e13f3f06239d69",
2381 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2382 "data": {
2383 "gen_ai.usage.input_tokens": 1000,
2384 "gen_ai.usage.output_tokens": 2000,
2385 "gen_ai.request.model": "gpt4-21-04"
2386 }
2387 },
2388 {
2389 "timestamp": 1702474614.0175,
2390 "start_timestamp": 1702474613.0175,
2391 "description": "OpenAI ",
2392 "op": "gen_ai.chat_completions.openai",
2393 "span_id": "ac01bd820a083e63",
2394 "parent_span_id": "a1e13f3f06239d69",
2395 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2396 "data": {
2397 "gen_ai.usage.input_tokens": 1000,
2398 "gen_ai.usage.output_tokens": 2000,
2399 "gen_ai.response.model": "gpt4-21-04"
2400 }
2401 }
2402 ]
2403 }
2404 "#;
2405
2406 let mut event = Annotated::<Event>::from_json(json).unwrap();
2407
2408 normalize_event(
2409 &mut event,
2410 &NormalizationConfig {
2411 ai_model_costs: Some(&ModelCosts {
2412 version: 2,
2413 models: HashMap::from([
2414 (
2415 Pattern::new("claude-2.1").unwrap(),
2416 ModelCostV2 {
2417 input_per_token: 0.01,
2418 output_per_token: 0.02,
2419 output_reasoning_per_token: 0.03,
2420 input_cached_per_token: 0.04,
2421 input_cache_write_per_token: 0.0,
2422 },
2423 ),
2424 (
2425 Pattern::new("gpt4-21-04").unwrap(),
2426 ModelCostV2 {
2427 input_per_token: 0.09,
2428 output_per_token: 0.05,
2429 output_reasoning_per_token: 0.0,
2430 input_cached_per_token: 0.0,
2431 input_cache_write_per_token: 0.0,
2432 },
2433 ),
2434 ]),
2435 }),
2436 ..NormalizationConfig::default()
2437 },
2438 );
2439
2440 let [span1, span2, span3] = collect_span_data(event);
2441
2442 assert_annotated_snapshot!(span1, @r#"
2443 {
2444 "gen_ai.usage.total_tokens": 3000.0,
2445 "gen_ai.usage.input_tokens": 1000,
2446 "gen_ai.usage.input_tokens.cached": 500,
2447 "gen_ai.usage.output_tokens": 2000,
2448 "gen_ai.usage.output_tokens.reasoning": 1000,
2449 "gen_ai.response.model": "claude-2.1",
2450 "gen_ai.request.model": "claude-2.1",
2451 "gen_ai.cost.total_tokens": 75.0,
2452 "gen_ai.cost.input_tokens": 25.0,
2453 "gen_ai.cost.output_tokens": 50.0,
2454 "gen_ai.response.tokens_per_second": 2000.0,
2455 "gen_ai.operation.type": "ai_client"
2456 }
2457 "#);
2458 assert_annotated_snapshot!(span2, @r#"
2459 {
2460 "gen_ai.usage.total_tokens": 3000.0,
2461 "gen_ai.usage.input_tokens": 1000,
2462 "gen_ai.usage.output_tokens": 2000,
2463 "gen_ai.response.model": "gpt4-21-04",
2464 "gen_ai.request.model": "gpt4-21-04",
2465 "gen_ai.cost.total_tokens": 190.0,
2466 "gen_ai.cost.input_tokens": 90.0,
2467 "gen_ai.cost.output_tokens": 100.0,
2468 "gen_ai.response.tokens_per_second": 2000.0,
2469 "gen_ai.operation.type": "ai_client"
2470 }
2471 "#);
2472 assert_annotated_snapshot!(span3, @r#"
2473 {
2474 "gen_ai.usage.total_tokens": 3000.0,
2475 "gen_ai.usage.input_tokens": 1000,
2476 "gen_ai.usage.output_tokens": 2000,
2477 "gen_ai.response.model": "gpt4-21-04",
2478 "gen_ai.cost.total_tokens": 190.0,
2479 "gen_ai.cost.input_tokens": 90.0,
2480 "gen_ai.cost.output_tokens": 100.0,
2481 "gen_ai.response.tokens_per_second": 2000.0,
2482 "gen_ai.operation.type": "ai_client"
2483 }
2484 "#);
2485 }
2486
2487 #[test]
2488 fn test_ai_data_with_no_tokens() {
2489 let json = r#"
2490 {
2491 "spans": [
2492 {
2493 "timestamp": 1702474613.0495,
2494 "start_timestamp": 1702474613.0175,
2495 "description": "OpenAI ",
2496 "op": "gen_ai.invoke_agent",
2497 "span_id": "9c01bd820a083e63",
2498 "parent_span_id": "a1e13f3f06239d69",
2499 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2500 "data": {
2501 "gen_ai.request.model": "claude-2.1"
2502 }
2503 }
2504 ]
2505 }
2506 "#;
2507
2508 let mut event = Annotated::<Event>::from_json(json).unwrap();
2509
2510 normalize_event(
2511 &mut event,
2512 &NormalizationConfig {
2513 ai_model_costs: Some(&ModelCosts {
2514 version: 2,
2515 models: HashMap::from([(
2516 Pattern::new("claude-2.1").unwrap(),
2517 ModelCostV2 {
2518 input_per_token: 0.01,
2519 output_per_token: 0.02,
2520 output_reasoning_per_token: 0.03,
2521 input_cached_per_token: 0.0,
2522 input_cache_write_per_token: 0.0,
2523 },
2524 )]),
2525 }),
2526 ..NormalizationConfig::default()
2527 },
2528 );
2529
2530 let [span] = collect_span_data(event);
2531
2532 assert_annotated_snapshot!(span, @r#"
2533 {
2534 "gen_ai.response.model": "claude-2.1",
2535 "gen_ai.request.model": "claude-2.1",
2536 "gen_ai.operation.type": "agent"
2537 }
2538 "#);
2539 }
2540
2541 #[test]
2542 fn test_ai_data_with_ai_op_prefix() {
2543 let json = r#"
2544 {
2545 "spans": [
2546 {
2547 "timestamp": 1702474613.0495,
2548 "start_timestamp": 1702474613.0175,
2549 "description": "OpenAI ",
2550 "op": "ai.chat_completions.openai",
2551 "span_id": "9c01bd820a083e63",
2552 "parent_span_id": "a1e13f3f06239d69",
2553 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2554 "data": {
2555 "gen_ai.usage.input_tokens": 1000,
2556 "gen_ai.usage.output_tokens": 2000,
2557 "gen_ai.usage.output_tokens.reasoning": 1000,
2558 "gen_ai.usage.input_tokens.cached": 500,
2559 "gen_ai.request.model": "claude-2.1"
2560 }
2561 },
2562 {
2563 "timestamp": 1702474613.0495,
2564 "start_timestamp": 1702474613.0175,
2565 "description": "OpenAI ",
2566 "op": "ai.chat_completions.openai",
2567 "span_id": "ac01bd820a083e63",
2568 "parent_span_id": "a1e13f3f06239d69",
2569 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2570 "data": {
2571 "gen_ai.usage.input_tokens": 1000,
2572 "gen_ai.usage.output_tokens": 2000,
2573 "gen_ai.request.model": "gpt4-21-04"
2574 }
2575 }
2576 ]
2577 }
2578 "#;
2579
2580 let mut event = Annotated::<Event>::from_json(json).unwrap();
2581
2582 normalize_event(
2583 &mut event,
2584 &NormalizationConfig {
2585 ai_model_costs: Some(&ModelCosts {
2586 version: 2,
2587 models: HashMap::from([
2588 (
2589 Pattern::new("claude-2.1").unwrap(),
2590 ModelCostV2 {
2591 input_per_token: 0.01,
2592 output_per_token: 0.02,
2593 output_reasoning_per_token: 0.0,
2594 input_cached_per_token: 0.04,
2595 input_cache_write_per_token: 0.0,
2596 },
2597 ),
2598 (
2599 Pattern::new("gpt4-21-04").unwrap(),
2600 ModelCostV2 {
2601 input_per_token: 0.09,
2602 output_per_token: 0.05,
2603 output_reasoning_per_token: 0.06,
2604 input_cached_per_token: 0.0,
2605 input_cache_write_per_token: 0.0,
2606 },
2607 ),
2608 ]),
2609 }),
2610 ..NormalizationConfig::default()
2611 },
2612 );
2613
2614 let [span1, span2] = collect_span_data(event);
2615
2616 assert_annotated_snapshot!(span1, @r#"
2617 {
2618 "gen_ai.usage.total_tokens": 3000.0,
2619 "gen_ai.usage.input_tokens": 1000,
2620 "gen_ai.usage.input_tokens.cached": 500,
2621 "gen_ai.usage.output_tokens": 2000,
2622 "gen_ai.usage.output_tokens.reasoning": 1000,
2623 "gen_ai.response.model": "claude-2.1",
2624 "gen_ai.request.model": "claude-2.1",
2625 "gen_ai.cost.total_tokens": 65.0,
2626 "gen_ai.cost.input_tokens": 25.0,
2627 "gen_ai.cost.output_tokens": 40.0,
2628 "gen_ai.response.tokens_per_second": 62500.0,
2629 "gen_ai.operation.type": "ai_client"
2630 }
2631 "#);
2632 assert_annotated_snapshot!(span2, @r#"
2633 {
2634 "gen_ai.usage.total_tokens": 3000.0,
2635 "gen_ai.usage.input_tokens": 1000,
2636 "gen_ai.usage.output_tokens": 2000,
2637 "gen_ai.response.model": "gpt4-21-04",
2638 "gen_ai.request.model": "gpt4-21-04",
2639 "gen_ai.cost.total_tokens": 190.0,
2640 "gen_ai.cost.input_tokens": 90.0,
2641 "gen_ai.cost.output_tokens": 100.0,
2642 "gen_ai.response.tokens_per_second": 62500.0,
2643 "gen_ai.operation.type": "ai_client"
2644 }
2645 "#);
2646 }
2647
2648 #[test]
2649 fn test_ai_response_tokens_per_second_no_output_tokens() {
2650 let json = r#"
2651 {
2652 "spans": [
2653 {
2654 "timestamp": 1702474614.0175,
2655 "start_timestamp": 1702474613.0175,
2656 "op": "gen_ai.chat_completions",
2657 "span_id": "9c01bd820a083e63",
2658 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2659 "data": {
2660 "gen_ai.usage.input_tokens": 500
2661 }
2662 }
2663 ]
2664 }
2665 "#;
2666
2667 let mut event = Annotated::<Event>::from_json(json).unwrap();
2668
2669 normalize_event(
2670 &mut event,
2671 &NormalizationConfig {
2672 ai_model_costs: Some(&ModelCosts {
2673 version: 2,
2674 models: HashMap::new(),
2675 }),
2676 ..NormalizationConfig::default()
2677 },
2678 );
2679
2680 let [span] = collect_span_data(event);
2681
2682 assert_annotated_snapshot!(span, @r#"
2684 {
2685 "gen_ai.usage.total_tokens": 500.0,
2686 "gen_ai.usage.input_tokens": 500,
2687 "gen_ai.operation.type": "ai_client"
2688 }
2689 "#);
2690 }
2691
2692 #[test]
2693 fn test_ai_response_tokens_per_second_zero_duration() {
2694 let json = r#"
2695 {
2696 "spans": [
2697 {
2698 "timestamp": 1702474613.0175,
2699 "start_timestamp": 1702474613.0175,
2700 "op": "gen_ai.chat_completions",
2701 "span_id": "9c01bd820a083e63",
2702 "trace_id": "922dda2462ea4ac2b6a4b339bee90863",
2703 "data": {
2704 "gen_ai.usage.output_tokens": 1000
2705 }
2706 }
2707 ]
2708 }
2709 "#;
2710
2711 let mut event = Annotated::<Event>::from_json(json).unwrap();
2712
2713 normalize_event(
2714 &mut event,
2715 &NormalizationConfig {
2716 ai_model_costs: Some(&ModelCosts {
2717 version: 2,
2718 models: HashMap::new(),
2719 }),
2720 ..NormalizationConfig::default()
2721 },
2722 );
2723
2724 let [span] = collect_span_data(event);
2725
2726 assert_annotated_snapshot!(span, @r#"
2728 {
2729 "gen_ai.usage.total_tokens": 1000.0,
2730 "gen_ai.usage.output_tokens": 1000,
2731 "gen_ai.operation.type": "ai_client"
2732 }
2733 "#);
2734 }
2735
2736 #[test]
2737 fn test_ai_operation_type_mapping() {
2738 let json = r#"
2739 {
2740 "type": "transaction",
2741 "transaction": "test-transaction",
2742 "spans": [
2743 {
2744 "op": "gen_ai.chat",
2745 "description": "AI chat completion",
2746 "data": {}
2747 },
2748 {
2749 "op": "gen_ai.handoff",
2750 "description": "AI agent handoff",
2751 "data": {}
2752 },
2753 {
2754 "op": "gen_ai.unknown",
2755 "description": "Unknown AI operation",
2756 "data": {}
2757 }
2758 ]
2759 }
2760 "#;
2761
2762 let mut event = Annotated::<Event>::from_json(json).unwrap();
2763
2764 normalize_event(&mut event, &NormalizationConfig::default());
2765
2766 let [span1, span2, span3] = collect_span_data(event);
2767
2768 assert_annotated_snapshot!(span1, @r#"
2769 {
2770 "gen_ai.operation.type": "ai_client"
2771 }
2772 "#);
2773 assert_annotated_snapshot!(span2, @r#"
2774 {
2775 "gen_ai.operation.type": "handoff"
2776 }
2777 "#);
2778 assert_annotated_snapshot!(span3, @r#"
2779 {
2780 "gen_ai.operation.type": "ai_client"
2781 }
2782 "#);
2783 }
2784
2785 #[test]
2786 fn test_apple_high_device_class() {
2787 let mut event = Event {
2788 contexts: {
2789 let mut contexts = Contexts::new();
2790 contexts.add(DeviceContext {
2791 family: "iPhone".to_owned().into(),
2792 model: "iPhone15,3".to_owned().into(),
2793 ..Default::default()
2794 });
2795 Annotated::new(contexts)
2796 },
2797 ..Default::default()
2798 };
2799 normalize_device_class(&mut event);
2800 assert_debug_snapshot!(event.tags, @r###"
2801 Tags(
2802 PairList(
2803 [
2804 TagEntry(
2805 "device.class",
2806 "3",
2807 ),
2808 ],
2809 ),
2810 )
2811 "###);
2812 }
2813
2814 #[test]
2815 fn test_filter_mobile_outliers() {
2816 let mut measurements =
2817 Annotated::<Measurements>::from_json(r#"{"app_start_warm": {"value": 180001}}"#)
2818 .unwrap()
2819 .into_value()
2820 .unwrap();
2821 assert_eq!(measurements.len(), 1);
2822 filter_mobile_outliers(&mut measurements);
2823 assert_eq!(measurements.len(), 0);
2824 }
2825
2826 #[test]
2827 fn test_computed_performance_score() {
2828 let json = r#"
2829 {
2830 "type": "transaction",
2831 "timestamp": "2021-04-26T08:00:05+0100",
2832 "start_timestamp": "2021-04-26T08:00:00+0100",
2833 "measurements": {
2834 "fid": {"value": 213, "unit": "millisecond"},
2835 "fcp": {"value": 1237, "unit": "millisecond"},
2836 "lcp": {"value": 6596, "unit": "millisecond"},
2837 "cls": {"value": 0.11}
2838 },
2839 "contexts": {
2840 "browser": {
2841 "name": "Chrome",
2842 "version": "120.1.1",
2843 "type": "browser"
2844 }
2845 }
2846 }
2847 "#;
2848
2849 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
2850
2851 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
2852 "profiles": [
2853 {
2854 "name": "Desktop",
2855 "scoreComponents": [
2856 {
2857 "measurement": "fcp",
2858 "weight": 0.15,
2859 "p10": 900,
2860 "p50": 1600
2861 },
2862 {
2863 "measurement": "lcp",
2864 "weight": 0.30,
2865 "p10": 1200,
2866 "p50": 2400
2867 },
2868 {
2869 "measurement": "fid",
2870 "weight": 0.30,
2871 "p10": 100,
2872 "p50": 300
2873 },
2874 {
2875 "measurement": "cls",
2876 "weight": 0.25,
2877 "p10": 0.1,
2878 "p50": 0.25
2879 },
2880 {
2881 "measurement": "ttfb",
2882 "weight": 0.0,
2883 "p10": 0.2,
2884 "p50": 0.4
2885 },
2886 ],
2887 "condition": {
2888 "op":"eq",
2889 "name": "event.contexts.browser.name",
2890 "value": "Chrome"
2891 }
2892 }
2893 ]
2894 }))
2895 .unwrap();
2896
2897 normalize_performance_score(&mut event, Some(&performance_score));
2898
2899 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
2900 {
2901 "type": "transaction",
2902 "timestamp": 1619420405.0,
2903 "start_timestamp": 1619420400.0,
2904 "contexts": {
2905 "browser": {
2906 "name": "Chrome",
2907 "version": "120.1.1",
2908 "type": "browser",
2909 },
2910 },
2911 "measurements": {
2912 "cls": {
2913 "value": 0.11,
2914 },
2915 "fcp": {
2916 "value": 1237.0,
2917 "unit": "millisecond",
2918 },
2919 "fid": {
2920 "value": 213.0,
2921 "unit": "millisecond",
2922 },
2923 "lcp": {
2924 "value": 6596.0,
2925 "unit": "millisecond",
2926 },
2927 "score.cls": {
2928 "value": 0.21864170607444863,
2929 "unit": "ratio",
2930 },
2931 "score.fcp": {
2932 "value": 0.10750855443790831,
2933 "unit": "ratio",
2934 },
2935 "score.fid": {
2936 "value": 0.19657361348282545,
2937 "unit": "ratio",
2938 },
2939 "score.lcp": {
2940 "value": 0.009238896571386584,
2941 "unit": "ratio",
2942 },
2943 "score.ratio.cls": {
2944 "value": 0.8745668242977945,
2945 "unit": "ratio",
2946 },
2947 "score.ratio.fcp": {
2948 "value": 0.7167236962527221,
2949 "unit": "ratio",
2950 },
2951 "score.ratio.fid": {
2952 "value": 0.6552453782760849,
2953 "unit": "ratio",
2954 },
2955 "score.ratio.lcp": {
2956 "value": 0.03079632190462195,
2957 "unit": "ratio",
2958 },
2959 "score.total": {
2960 "value": 0.531962770566569,
2961 "unit": "ratio",
2962 },
2963 "score.weight.cls": {
2964 "value": 0.25,
2965 "unit": "ratio",
2966 },
2967 "score.weight.fcp": {
2968 "value": 0.15,
2969 "unit": "ratio",
2970 },
2971 "score.weight.fid": {
2972 "value": 0.3,
2973 "unit": "ratio",
2974 },
2975 "score.weight.lcp": {
2976 "value": 0.3,
2977 "unit": "ratio",
2978 },
2979 "score.weight.ttfb": {
2980 "value": 0.0,
2981 "unit": "ratio",
2982 },
2983 },
2984 }
2985 "###);
2986 }
2987
2988 #[test]
2991 fn test_computed_performance_score_with_under_normalized_weights() {
2992 let json = r#"
2993 {
2994 "type": "transaction",
2995 "timestamp": "2021-04-26T08:00:05+0100",
2996 "start_timestamp": "2021-04-26T08:00:00+0100",
2997 "measurements": {
2998 "fid": {"value": 213, "unit": "millisecond"},
2999 "fcp": {"value": 1237, "unit": "millisecond"},
3000 "lcp": {"value": 6596, "unit": "millisecond"},
3001 "cls": {"value": 0.11}
3002 },
3003 "contexts": {
3004 "browser": {
3005 "name": "Chrome",
3006 "version": "120.1.1",
3007 "type": "browser"
3008 }
3009 }
3010 }
3011 "#;
3012
3013 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3014
3015 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3016 "profiles": [
3017 {
3018 "name": "Desktop",
3019 "scoreComponents": [
3020 {
3021 "measurement": "fcp",
3022 "weight": 0.03,
3023 "p10": 900,
3024 "p50": 1600
3025 },
3026 {
3027 "measurement": "lcp",
3028 "weight": 0.06,
3029 "p10": 1200,
3030 "p50": 2400
3031 },
3032 {
3033 "measurement": "fid",
3034 "weight": 0.06,
3035 "p10": 100,
3036 "p50": 300
3037 },
3038 {
3039 "measurement": "cls",
3040 "weight": 0.05,
3041 "p10": 0.1,
3042 "p50": 0.25
3043 },
3044 {
3045 "measurement": "ttfb",
3046 "weight": 0.0,
3047 "p10": 0.2,
3048 "p50": 0.4
3049 },
3050 ],
3051 "condition": {
3052 "op":"eq",
3053 "name": "event.contexts.browser.name",
3054 "value": "Chrome"
3055 }
3056 }
3057 ]
3058 }))
3059 .unwrap();
3060
3061 normalize_performance_score(&mut event, Some(&performance_score));
3062
3063 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3064 {
3065 "type": "transaction",
3066 "timestamp": 1619420405.0,
3067 "start_timestamp": 1619420400.0,
3068 "contexts": {
3069 "browser": {
3070 "name": "Chrome",
3071 "version": "120.1.1",
3072 "type": "browser",
3073 },
3074 },
3075 "measurements": {
3076 "cls": {
3077 "value": 0.11,
3078 },
3079 "fcp": {
3080 "value": 1237.0,
3081 "unit": "millisecond",
3082 },
3083 "fid": {
3084 "value": 213.0,
3085 "unit": "millisecond",
3086 },
3087 "lcp": {
3088 "value": 6596.0,
3089 "unit": "millisecond",
3090 },
3091 "score.cls": {
3092 "value": 0.21864170607444863,
3093 "unit": "ratio",
3094 },
3095 "score.fcp": {
3096 "value": 0.10750855443790831,
3097 "unit": "ratio",
3098 },
3099 "score.fid": {
3100 "value": 0.19657361348282545,
3101 "unit": "ratio",
3102 },
3103 "score.lcp": {
3104 "value": 0.009238896571386584,
3105 "unit": "ratio",
3106 },
3107 "score.ratio.cls": {
3108 "value": 0.8745668242977945,
3109 "unit": "ratio",
3110 },
3111 "score.ratio.fcp": {
3112 "value": 0.7167236962527221,
3113 "unit": "ratio",
3114 },
3115 "score.ratio.fid": {
3116 "value": 0.6552453782760849,
3117 "unit": "ratio",
3118 },
3119 "score.ratio.lcp": {
3120 "value": 0.03079632190462195,
3121 "unit": "ratio",
3122 },
3123 "score.total": {
3124 "value": 0.531962770566569,
3125 "unit": "ratio",
3126 },
3127 "score.weight.cls": {
3128 "value": 0.25,
3129 "unit": "ratio",
3130 },
3131 "score.weight.fcp": {
3132 "value": 0.15,
3133 "unit": "ratio",
3134 },
3135 "score.weight.fid": {
3136 "value": 0.3,
3137 "unit": "ratio",
3138 },
3139 "score.weight.lcp": {
3140 "value": 0.3,
3141 "unit": "ratio",
3142 },
3143 "score.weight.ttfb": {
3144 "value": 0.0,
3145 "unit": "ratio",
3146 },
3147 },
3148 }
3149 "###);
3150 }
3151
3152 #[test]
3155 fn test_computed_performance_score_with_over_normalized_weights() {
3156 let json = r#"
3157 {
3158 "type": "transaction",
3159 "timestamp": "2021-04-26T08:00:05+0100",
3160 "start_timestamp": "2021-04-26T08:00:00+0100",
3161 "measurements": {
3162 "fid": {"value": 213, "unit": "millisecond"},
3163 "fcp": {"value": 1237, "unit": "millisecond"},
3164 "lcp": {"value": 6596, "unit": "millisecond"},
3165 "cls": {"value": 0.11}
3166 },
3167 "contexts": {
3168 "browser": {
3169 "name": "Chrome",
3170 "version": "120.1.1",
3171 "type": "browser"
3172 }
3173 }
3174 }
3175 "#;
3176
3177 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3178
3179 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3180 "profiles": [
3181 {
3182 "name": "Desktop",
3183 "scoreComponents": [
3184 {
3185 "measurement": "fcp",
3186 "weight": 0.30,
3187 "p10": 900,
3188 "p50": 1600
3189 },
3190 {
3191 "measurement": "lcp",
3192 "weight": 0.60,
3193 "p10": 1200,
3194 "p50": 2400
3195 },
3196 {
3197 "measurement": "fid",
3198 "weight": 0.60,
3199 "p10": 100,
3200 "p50": 300
3201 },
3202 {
3203 "measurement": "cls",
3204 "weight": 0.50,
3205 "p10": 0.1,
3206 "p50": 0.25
3207 },
3208 {
3209 "measurement": "ttfb",
3210 "weight": 0.0,
3211 "p10": 0.2,
3212 "p50": 0.4
3213 },
3214 ],
3215 "condition": {
3216 "op":"eq",
3217 "name": "event.contexts.browser.name",
3218 "value": "Chrome"
3219 }
3220 }
3221 ]
3222 }))
3223 .unwrap();
3224
3225 normalize_performance_score(&mut event, Some(&performance_score));
3226
3227 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3228 {
3229 "type": "transaction",
3230 "timestamp": 1619420405.0,
3231 "start_timestamp": 1619420400.0,
3232 "contexts": {
3233 "browser": {
3234 "name": "Chrome",
3235 "version": "120.1.1",
3236 "type": "browser",
3237 },
3238 },
3239 "measurements": {
3240 "cls": {
3241 "value": 0.11,
3242 },
3243 "fcp": {
3244 "value": 1237.0,
3245 "unit": "millisecond",
3246 },
3247 "fid": {
3248 "value": 213.0,
3249 "unit": "millisecond",
3250 },
3251 "lcp": {
3252 "value": 6596.0,
3253 "unit": "millisecond",
3254 },
3255 "score.cls": {
3256 "value": 0.21864170607444863,
3257 "unit": "ratio",
3258 },
3259 "score.fcp": {
3260 "value": 0.10750855443790831,
3261 "unit": "ratio",
3262 },
3263 "score.fid": {
3264 "value": 0.19657361348282545,
3265 "unit": "ratio",
3266 },
3267 "score.lcp": {
3268 "value": 0.009238896571386584,
3269 "unit": "ratio",
3270 },
3271 "score.ratio.cls": {
3272 "value": 0.8745668242977945,
3273 "unit": "ratio",
3274 },
3275 "score.ratio.fcp": {
3276 "value": 0.7167236962527221,
3277 "unit": "ratio",
3278 },
3279 "score.ratio.fid": {
3280 "value": 0.6552453782760849,
3281 "unit": "ratio",
3282 },
3283 "score.ratio.lcp": {
3284 "value": 0.03079632190462195,
3285 "unit": "ratio",
3286 },
3287 "score.total": {
3288 "value": 0.531962770566569,
3289 "unit": "ratio",
3290 },
3291 "score.weight.cls": {
3292 "value": 0.25,
3293 "unit": "ratio",
3294 },
3295 "score.weight.fcp": {
3296 "value": 0.15,
3297 "unit": "ratio",
3298 },
3299 "score.weight.fid": {
3300 "value": 0.3,
3301 "unit": "ratio",
3302 },
3303 "score.weight.lcp": {
3304 "value": 0.3,
3305 "unit": "ratio",
3306 },
3307 "score.weight.ttfb": {
3308 "value": 0.0,
3309 "unit": "ratio",
3310 },
3311 },
3312 }
3313 "###);
3314 }
3315
3316 #[test]
3317 fn test_computed_performance_score_missing_measurement() {
3318 let json = r#"
3319 {
3320 "type": "transaction",
3321 "timestamp": "2021-04-26T08:00:05+0100",
3322 "start_timestamp": "2021-04-26T08:00:00+0100",
3323 "measurements": {
3324 "a": {"value": 213, "unit": "millisecond"}
3325 },
3326 "contexts": {
3327 "browser": {
3328 "name": "Chrome",
3329 "version": "120.1.1",
3330 "type": "browser"
3331 }
3332 }
3333 }
3334 "#;
3335
3336 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3337
3338 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3339 "profiles": [
3340 {
3341 "name": "Desktop",
3342 "scoreComponents": [
3343 {
3344 "measurement": "a",
3345 "weight": 0.15,
3346 "p10": 900,
3347 "p50": 1600
3348 },
3349 {
3350 "measurement": "b",
3351 "weight": 0.30,
3352 "p10": 1200,
3353 "p50": 2400
3354 },
3355 ],
3356 "condition": {
3357 "op":"eq",
3358 "name": "event.contexts.browser.name",
3359 "value": "Chrome"
3360 }
3361 }
3362 ]
3363 }))
3364 .unwrap();
3365
3366 normalize_performance_score(&mut event, Some(&performance_score));
3367
3368 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3369 {
3370 "type": "transaction",
3371 "timestamp": 1619420405.0,
3372 "start_timestamp": 1619420400.0,
3373 "contexts": {
3374 "browser": {
3375 "name": "Chrome",
3376 "version": "120.1.1",
3377 "type": "browser",
3378 },
3379 },
3380 "measurements": {
3381 "a": {
3382 "value": 213.0,
3383 "unit": "millisecond",
3384 },
3385 },
3386 }
3387 "###);
3388 }
3389
3390 #[test]
3391 fn test_computed_performance_score_optional_measurement() {
3392 let json = r#"
3393 {
3394 "type": "transaction",
3395 "timestamp": "2021-04-26T08:00:05+0100",
3396 "start_timestamp": "2021-04-26T08:00:00+0100",
3397 "measurements": {
3398 "a": {"value": 213, "unit": "millisecond"},
3399 "b": {"value": 213, "unit": "millisecond"}
3400 },
3401 "contexts": {
3402 "browser": {
3403 "name": "Chrome",
3404 "version": "120.1.1",
3405 "type": "browser"
3406 }
3407 }
3408 }
3409 "#;
3410
3411 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3412
3413 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3414 "profiles": [
3415 {
3416 "name": "Desktop",
3417 "scoreComponents": [
3418 {
3419 "measurement": "a",
3420 "weight": 0.15,
3421 "p10": 900,
3422 "p50": 1600,
3423 },
3424 {
3425 "measurement": "b",
3426 "weight": 0.30,
3427 "p10": 1200,
3428 "p50": 2400,
3429 "optional": true
3430 },
3431 {
3432 "measurement": "c",
3433 "weight": 0.55,
3434 "p10": 1200,
3435 "p50": 2400,
3436 "optional": true
3437 },
3438 ],
3439 "condition": {
3440 "op":"eq",
3441 "name": "event.contexts.browser.name",
3442 "value": "Chrome"
3443 }
3444 }
3445 ]
3446 }))
3447 .unwrap();
3448
3449 normalize_performance_score(&mut event, Some(&performance_score));
3450
3451 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3452 {
3453 "type": "transaction",
3454 "timestamp": 1619420405.0,
3455 "start_timestamp": 1619420400.0,
3456 "contexts": {
3457 "browser": {
3458 "name": "Chrome",
3459 "version": "120.1.1",
3460 "type": "browser",
3461 },
3462 },
3463 "measurements": {
3464 "a": {
3465 "value": 213.0,
3466 "unit": "millisecond",
3467 },
3468 "b": {
3469 "value": 213.0,
3470 "unit": "millisecond",
3471 },
3472 "score.a": {
3473 "value": 0.33333215313291975,
3474 "unit": "ratio",
3475 },
3476 "score.b": {
3477 "value": 0.66666415149198,
3478 "unit": "ratio",
3479 },
3480 "score.ratio.a": {
3481 "value": 0.9999964593987591,
3482 "unit": "ratio",
3483 },
3484 "score.ratio.b": {
3485 "value": 0.9999962272379699,
3486 "unit": "ratio",
3487 },
3488 "score.total": {
3489 "value": 0.9999963046248997,
3490 "unit": "ratio",
3491 },
3492 "score.weight.a": {
3493 "value": 0.33333333333333337,
3494 "unit": "ratio",
3495 },
3496 "score.weight.b": {
3497 "value": 0.6666666666666667,
3498 "unit": "ratio",
3499 },
3500 "score.weight.c": {
3501 "value": 0.0,
3502 "unit": "ratio",
3503 },
3504 },
3505 }
3506 "###);
3507 }
3508
3509 #[test]
3510 fn test_computed_performance_score_weight_0() {
3511 let json = r#"
3512 {
3513 "type": "transaction",
3514 "timestamp": "2021-04-26T08:00:05+0100",
3515 "start_timestamp": "2021-04-26T08:00:00+0100",
3516 "measurements": {
3517 "cls": {"value": 0.11}
3518 }
3519 }
3520 "#;
3521
3522 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3523
3524 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3525 "profiles": [
3526 {
3527 "name": "Desktop",
3528 "scoreComponents": [
3529 {
3530 "measurement": "cls",
3531 "weight": 0,
3532 "p10": 0.1,
3533 "p50": 0.25
3534 },
3535 ],
3536 "condition": {
3537 "op":"and",
3538 "inner": []
3539 }
3540 }
3541 ]
3542 }))
3543 .unwrap();
3544
3545 normalize_performance_score(&mut event, Some(&performance_score));
3546
3547 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3548 {
3549 "type": "transaction",
3550 "timestamp": 1619420405.0,
3551 "start_timestamp": 1619420400.0,
3552 "measurements": {
3553 "cls": {
3554 "value": 0.11,
3555 },
3556 },
3557 }
3558 "###);
3559 }
3560
3561 #[test]
3562 fn test_computed_performance_score_negative_value() {
3563 let json = r#"
3564 {
3565 "type": "transaction",
3566 "timestamp": "2021-04-26T08:00:05+0100",
3567 "start_timestamp": "2021-04-26T08:00:00+0100",
3568 "measurements": {
3569 "ttfb": {"value": -100, "unit": "millisecond"}
3570 }
3571 }
3572 "#;
3573
3574 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3575
3576 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3577 "profiles": [
3578 {
3579 "name": "Desktop",
3580 "scoreComponents": [
3581 {
3582 "measurement": "ttfb",
3583 "weight": 1.0,
3584 "p10": 100.0,
3585 "p50": 250.0
3586 },
3587 ],
3588 "condition": {
3589 "op":"and",
3590 "inner": []
3591 }
3592 }
3593 ]
3594 }))
3595 .unwrap();
3596
3597 normalize_performance_score(&mut event, Some(&performance_score));
3598
3599 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3600 {
3601 "type": "transaction",
3602 "timestamp": 1619420405.0,
3603 "start_timestamp": 1619420400.0,
3604 "measurements": {
3605 "score.ratio.ttfb": {
3606 "value": 1.0,
3607 "unit": "ratio",
3608 },
3609 "score.total": {
3610 "value": 1.0,
3611 "unit": "ratio",
3612 },
3613 "score.ttfb": {
3614 "value": 1.0,
3615 "unit": "ratio",
3616 },
3617 "score.weight.ttfb": {
3618 "value": 1.0,
3619 "unit": "ratio",
3620 },
3621 "ttfb": {
3622 "value": -100.0,
3623 "unit": "millisecond",
3624 },
3625 },
3626 }
3627 "###);
3628 }
3629
3630 #[test]
3631 fn test_filter_negative_web_vital_measurements() {
3632 let json = r#"
3633 {
3634 "type": "transaction",
3635 "timestamp": "2021-04-26T08:00:05+0100",
3636 "start_timestamp": "2021-04-26T08:00:00+0100",
3637 "measurements": {
3638 "ttfb": {"value": -100, "unit": "millisecond"}
3639 }
3640 }
3641 "#;
3642 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3643
3644 let project_measurement_config: MeasurementsConfig = serde_json::from_value(json!({
3646 "builtinMeasurements": [
3647 {"name": "ttfb", "unit": "millisecond"},
3648 ],
3649 }))
3650 .unwrap();
3651
3652 let dynamic_measurement_config =
3653 CombinedMeasurementsConfig::new(Some(&project_measurement_config), None);
3654
3655 normalize_event_measurements(&mut event, Some(dynamic_measurement_config), None);
3656
3657 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3658 {
3659 "type": "transaction",
3660 "timestamp": 1619420405.0,
3661 "start_timestamp": 1619420400.0,
3662 "measurements": {},
3663 "_meta": {
3664 "measurements": {
3665 "": Meta(Some(MetaInner(
3666 err: [
3667 [
3668 "invalid_data",
3669 {
3670 "reason": "Negative value for measurement ttfb not allowed: -100",
3671 },
3672 ],
3673 ],
3674 val: Some({
3675 "ttfb": {
3676 "unit": "millisecond",
3677 "value": -100.0,
3678 },
3679 }),
3680 ))),
3681 },
3682 },
3683 }
3684 "###);
3685 }
3686
3687 #[test]
3688 fn test_computed_performance_score_multiple_profiles() {
3689 let json = r#"
3690 {
3691 "type": "transaction",
3692 "timestamp": "2021-04-26T08:00:05+0100",
3693 "start_timestamp": "2021-04-26T08:00:00+0100",
3694 "measurements": {
3695 "cls": {"value": 0.11},
3696 "inp": {"value": 120.0}
3697 }
3698 }
3699 "#;
3700
3701 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3702
3703 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3704 "profiles": [
3705 {
3706 "name": "Desktop",
3707 "scoreComponents": [
3708 {
3709 "measurement": "cls",
3710 "weight": 0,
3711 "p10": 0.1,
3712 "p50": 0.25
3713 },
3714 ],
3715 "condition": {
3716 "op":"and",
3717 "inner": []
3718 }
3719 },
3720 {
3721 "name": "Desktop",
3722 "scoreComponents": [
3723 {
3724 "measurement": "inp",
3725 "weight": 1.0,
3726 "p10": 0.1,
3727 "p50": 0.25
3728 },
3729 ],
3730 "condition": {
3731 "op":"and",
3732 "inner": []
3733 }
3734 }
3735 ]
3736 }))
3737 .unwrap();
3738
3739 normalize_performance_score(&mut event, Some(&performance_score));
3740
3741 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
3742 {
3743 "type": "transaction",
3744 "timestamp": 1619420405.0,
3745 "start_timestamp": 1619420400.0,
3746 "measurements": {
3747 "cls": {
3748 "value": 0.11,
3749 },
3750 "inp": {
3751 "value": 120.0,
3752 },
3753 "score.inp": {
3754 "value": 0.0,
3755 "unit": "ratio",
3756 },
3757 "score.ratio.inp": {
3758 "value": 0.0,
3759 "unit": "ratio",
3760 },
3761 "score.total": {
3762 "value": 0.0,
3763 "unit": "ratio",
3764 },
3765 "score.weight.inp": {
3766 "value": 1.0,
3767 "unit": "ratio",
3768 },
3769 },
3770 }
3771 "###);
3772 }
3773
3774 #[test]
3775 fn test_compute_performance_score_for_mobile_ios_profile() {
3776 let mut event = Annotated::<Event>::from_json(IOS_MOBILE_EVENT)
3777 .unwrap()
3778 .0
3779 .unwrap();
3780
3781 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3782 "profiles": [
3783 {
3784 "name": "Mobile",
3785 "scoreComponents": [
3786 {
3787 "measurement": "time_to_initial_display",
3788 "weight": 0.25,
3789 "p10": 1800.0,
3790 "p50": 3000.0,
3791 "optional": true
3792 },
3793 {
3794 "measurement": "time_to_full_display",
3795 "weight": 0.25,
3796 "p10": 2500.0,
3797 "p50": 4000.0,
3798 "optional": true
3799 },
3800 {
3801 "measurement": "app_start_warm",
3802 "weight": 0.25,
3803 "p10": 200.0,
3804 "p50": 500.0,
3805 "optional": true
3806 },
3807 {
3808 "measurement": "app_start_cold",
3809 "weight": 0.25,
3810 "p10": 200.0,
3811 "p50": 500.0,
3812 "optional": true
3813 }
3814 ],
3815 "condition": {
3816 "op": "and",
3817 "inner": [
3818 {
3819 "op": "or",
3820 "inner": [
3821 {
3822 "op": "eq",
3823 "name": "event.sdk.name",
3824 "value": "sentry.cocoa"
3825 },
3826 {
3827 "op": "eq",
3828 "name": "event.sdk.name",
3829 "value": "sentry.java.android"
3830 }
3831 ]
3832 },
3833 {
3834 "op": "eq",
3835 "name": "event.contexts.trace.op",
3836 "value": "ui.load"
3837 }
3838 ]
3839 }
3840 }
3841 ]
3842 }))
3843 .unwrap();
3844
3845 normalize_performance_score(&mut event, Some(&performance_score));
3846
3847 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3848 }
3849
3850 #[test]
3851 fn test_compute_performance_score_for_mobile_android_profile() {
3852 let mut event = Annotated::<Event>::from_json(ANDROID_MOBILE_EVENT)
3853 .unwrap()
3854 .0
3855 .unwrap();
3856
3857 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3858 "profiles": [
3859 {
3860 "name": "Mobile",
3861 "scoreComponents": [
3862 {
3863 "measurement": "time_to_initial_display",
3864 "weight": 0.25,
3865 "p10": 1800.0,
3866 "p50": 3000.0,
3867 "optional": true
3868 },
3869 {
3870 "measurement": "time_to_full_display",
3871 "weight": 0.25,
3872 "p10": 2500.0,
3873 "p50": 4000.0,
3874 "optional": true
3875 },
3876 {
3877 "measurement": "app_start_warm",
3878 "weight": 0.25,
3879 "p10": 200.0,
3880 "p50": 500.0,
3881 "optional": true
3882 },
3883 {
3884 "measurement": "app_start_cold",
3885 "weight": 0.25,
3886 "p10": 200.0,
3887 "p50": 500.0,
3888 "optional": true
3889 }
3890 ],
3891 "condition": {
3892 "op": "and",
3893 "inner": [
3894 {
3895 "op": "or",
3896 "inner": [
3897 {
3898 "op": "eq",
3899 "name": "event.sdk.name",
3900 "value": "sentry.cocoa"
3901 },
3902 {
3903 "op": "eq",
3904 "name": "event.sdk.name",
3905 "value": "sentry.java.android"
3906 }
3907 ]
3908 },
3909 {
3910 "op": "eq",
3911 "name": "event.contexts.trace.op",
3912 "value": "ui.load"
3913 }
3914 ]
3915 }
3916 }
3917 ]
3918 }))
3919 .unwrap();
3920
3921 normalize_performance_score(&mut event, Some(&performance_score));
3922
3923 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {});
3924 }
3925
3926 #[test]
3927 fn test_computes_performance_score_and_tags_with_profile_version() {
3928 let json = r#"
3929 {
3930 "type": "transaction",
3931 "timestamp": "2021-04-26T08:00:05+0100",
3932 "start_timestamp": "2021-04-26T08:00:00+0100",
3933 "measurements": {
3934 "inp": {"value": 120.0}
3935 }
3936 }
3937 "#;
3938
3939 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
3940
3941 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
3942 "profiles": [
3943 {
3944 "name": "Desktop",
3945 "scoreComponents": [
3946 {
3947 "measurement": "inp",
3948 "weight": 1.0,
3949 "p10": 0.1,
3950 "p50": 0.25
3951 },
3952 ],
3953 "condition": {
3954 "op":"and",
3955 "inner": []
3956 },
3957 "version": "beta"
3958 }
3959 ]
3960 }))
3961 .unwrap();
3962
3963 normalize(
3964 &mut event,
3965 &mut Meta::default(),
3966 &NormalizationConfig {
3967 performance_score: Some(&performance_score),
3968 ..Default::default()
3969 },
3970 );
3971
3972 insta::assert_ron_snapshot!(SerializableAnnotated(&event.contexts), {}, @r###"
3973 {
3974 "performance_score": {
3975 "score_profile_version": "beta",
3976 "type": "performancescore",
3977 },
3978 }
3979 "###);
3980 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
3981 {
3982 "inp": {
3983 "value": 120.0,
3984 "unit": "millisecond",
3985 },
3986 "score.inp": {
3987 "value": 0.0,
3988 "unit": "ratio",
3989 },
3990 "score.ratio.inp": {
3991 "value": 0.0,
3992 "unit": "ratio",
3993 },
3994 "score.total": {
3995 "value": 0.0,
3996 "unit": "ratio",
3997 },
3998 "score.weight.inp": {
3999 "value": 1.0,
4000 "unit": "ratio",
4001 },
4002 }
4003 "###);
4004 }
4005
4006 #[test]
4007 fn test_computes_standalone_cls_performance_score() {
4008 let json = r#"
4009 {
4010 "type": "transaction",
4011 "timestamp": "2021-04-26T08:00:05+0100",
4012 "start_timestamp": "2021-04-26T08:00:00+0100",
4013 "measurements": {
4014 "cls": {"value": 0.5}
4015 }
4016 }
4017 "#;
4018
4019 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4020
4021 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4022 "profiles": [
4023 {
4024 "name": "Default",
4025 "scoreComponents": [
4026 {
4027 "measurement": "fcp",
4028 "weight": 0.15,
4029 "p10": 900.0,
4030 "p50": 1600.0,
4031 "optional": true,
4032 },
4033 {
4034 "measurement": "lcp",
4035 "weight": 0.30,
4036 "p10": 1200.0,
4037 "p50": 2400.0,
4038 "optional": true,
4039 },
4040 {
4041 "measurement": "cls",
4042 "weight": 0.15,
4043 "p10": 0.1,
4044 "p50": 0.25,
4045 "optional": true,
4046 },
4047 {
4048 "measurement": "ttfb",
4049 "weight": 0.10,
4050 "p10": 200.0,
4051 "p50": 400.0,
4052 "optional": true,
4053 },
4054 ],
4055 "condition": {
4056 "op": "and",
4057 "inner": [],
4058 },
4059 }
4060 ]
4061 }))
4062 .unwrap();
4063
4064 normalize(
4065 &mut event,
4066 &mut Meta::default(),
4067 &NormalizationConfig {
4068 performance_score: Some(&performance_score),
4069 ..Default::default()
4070 },
4071 );
4072
4073 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4074 {
4075 "cls": {
4076 "value": 0.5,
4077 "unit": "none",
4078 },
4079 "score.cls": {
4080 "value": 0.16615877613713903,
4081 "unit": "ratio",
4082 },
4083 "score.ratio.cls": {
4084 "value": 0.16615877613713903,
4085 "unit": "ratio",
4086 },
4087 "score.total": {
4088 "value": 0.16615877613713903,
4089 "unit": "ratio",
4090 },
4091 "score.weight.cls": {
4092 "value": 1.0,
4093 "unit": "ratio",
4094 },
4095 "score.weight.fcp": {
4096 "value": 0.0,
4097 "unit": "ratio",
4098 },
4099 "score.weight.lcp": {
4100 "value": 0.0,
4101 "unit": "ratio",
4102 },
4103 "score.weight.ttfb": {
4104 "value": 0.0,
4105 "unit": "ratio",
4106 },
4107 }
4108 "###);
4109 }
4110
4111 #[test]
4112 fn test_computes_standalone_lcp_performance_score() {
4113 let json = r#"
4114 {
4115 "type": "transaction",
4116 "timestamp": "2021-04-26T08:00:05+0100",
4117 "start_timestamp": "2021-04-26T08:00:00+0100",
4118 "measurements": {
4119 "lcp": {"value": 1200.0}
4120 }
4121 }
4122 "#;
4123
4124 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4125
4126 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4127 "profiles": [
4128 {
4129 "name": "Default",
4130 "scoreComponents": [
4131 {
4132 "measurement": "fcp",
4133 "weight": 0.15,
4134 "p10": 900.0,
4135 "p50": 1600.0,
4136 "optional": true,
4137 },
4138 {
4139 "measurement": "lcp",
4140 "weight": 0.30,
4141 "p10": 1200.0,
4142 "p50": 2400.0,
4143 "optional": true,
4144 },
4145 {
4146 "measurement": "cls",
4147 "weight": 0.15,
4148 "p10": 0.1,
4149 "p50": 0.25,
4150 "optional": true,
4151 },
4152 {
4153 "measurement": "ttfb",
4154 "weight": 0.10,
4155 "p10": 200.0,
4156 "p50": 400.0,
4157 "optional": true,
4158 },
4159 ],
4160 "condition": {
4161 "op": "and",
4162 "inner": [],
4163 },
4164 }
4165 ]
4166 }))
4167 .unwrap();
4168
4169 normalize(
4170 &mut event,
4171 &mut Meta::default(),
4172 &NormalizationConfig {
4173 performance_score: Some(&performance_score),
4174 ..Default::default()
4175 },
4176 );
4177
4178 insta::assert_ron_snapshot!(SerializableAnnotated(&event.measurements), {}, @r###"
4179 {
4180 "lcp": {
4181 "value": 1200.0,
4182 "unit": "millisecond",
4183 },
4184 "score.lcp": {
4185 "value": 0.8999999314038525,
4186 "unit": "ratio",
4187 },
4188 "score.ratio.lcp": {
4189 "value": 0.8999999314038525,
4190 "unit": "ratio",
4191 },
4192 "score.total": {
4193 "value": 0.8999999314038525,
4194 "unit": "ratio",
4195 },
4196 "score.weight.cls": {
4197 "value": 0.0,
4198 "unit": "ratio",
4199 },
4200 "score.weight.fcp": {
4201 "value": 0.0,
4202 "unit": "ratio",
4203 },
4204 "score.weight.lcp": {
4205 "value": 1.0,
4206 "unit": "ratio",
4207 },
4208 "score.weight.ttfb": {
4209 "value": 0.0,
4210 "unit": "ratio",
4211 },
4212 }
4213 "###);
4214 }
4215
4216 #[test]
4217 fn test_computed_performance_score_uses_first_matching_profile() {
4218 let json = r#"
4219 {
4220 "type": "transaction",
4221 "timestamp": "2021-04-26T08:00:05+0100",
4222 "start_timestamp": "2021-04-26T08:00:00+0100",
4223 "measurements": {
4224 "a": {"value": 213, "unit": "millisecond"},
4225 "b": {"value": 213, "unit": "millisecond"}
4226 },
4227 "contexts": {
4228 "browser": {
4229 "name": "Chrome",
4230 "version": "120.1.1",
4231 "type": "browser"
4232 }
4233 }
4234 }
4235 "#;
4236
4237 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4238
4239 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4240 "profiles": [
4241 {
4242 "name": "Mobile",
4243 "scoreComponents": [
4244 {
4245 "measurement": "a",
4246 "weight": 0.15,
4247 "p10": 100,
4248 "p50": 200,
4249 },
4250 {
4251 "measurement": "b",
4252 "weight": 0.30,
4253 "p10": 100,
4254 "p50": 200,
4255 "optional": true
4256 },
4257 {
4258 "measurement": "c",
4259 "weight": 0.55,
4260 "p10": 100,
4261 "p50": 200,
4262 "optional": true
4263 },
4264 ],
4265 "condition": {
4266 "op":"eq",
4267 "name": "event.contexts.browser.name",
4268 "value": "Chrome Mobile"
4269 }
4270 },
4271 {
4272 "name": "Desktop",
4273 "scoreComponents": [
4274 {
4275 "measurement": "a",
4276 "weight": 0.15,
4277 "p10": 900,
4278 "p50": 1600,
4279 },
4280 {
4281 "measurement": "b",
4282 "weight": 0.30,
4283 "p10": 1200,
4284 "p50": 2400,
4285 "optional": true
4286 },
4287 {
4288 "measurement": "c",
4289 "weight": 0.55,
4290 "p10": 1200,
4291 "p50": 2400,
4292 "optional": true
4293 },
4294 ],
4295 "condition": {
4296 "op":"eq",
4297 "name": "event.contexts.browser.name",
4298 "value": "Chrome"
4299 }
4300 },
4301 {
4302 "name": "Default",
4303 "scoreComponents": [
4304 {
4305 "measurement": "a",
4306 "weight": 0.15,
4307 "p10": 100,
4308 "p50": 200,
4309 },
4310 {
4311 "measurement": "b",
4312 "weight": 0.30,
4313 "p10": 100,
4314 "p50": 200,
4315 "optional": true
4316 },
4317 {
4318 "measurement": "c",
4319 "weight": 0.55,
4320 "p10": 100,
4321 "p50": 200,
4322 "optional": true
4323 },
4324 ],
4325 "condition": {
4326 "op": "and",
4327 "inner": [],
4328 }
4329 }
4330 ]
4331 }))
4332 .unwrap();
4333
4334 normalize_performance_score(&mut event, Some(&performance_score));
4335
4336 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4337 {
4338 "type": "transaction",
4339 "timestamp": 1619420405.0,
4340 "start_timestamp": 1619420400.0,
4341 "contexts": {
4342 "browser": {
4343 "name": "Chrome",
4344 "version": "120.1.1",
4345 "type": "browser",
4346 },
4347 },
4348 "measurements": {
4349 "a": {
4350 "value": 213.0,
4351 "unit": "millisecond",
4352 },
4353 "b": {
4354 "value": 213.0,
4355 "unit": "millisecond",
4356 },
4357 "score.a": {
4358 "value": 0.33333215313291975,
4359 "unit": "ratio",
4360 },
4361 "score.b": {
4362 "value": 0.66666415149198,
4363 "unit": "ratio",
4364 },
4365 "score.ratio.a": {
4366 "value": 0.9999964593987591,
4367 "unit": "ratio",
4368 },
4369 "score.ratio.b": {
4370 "value": 0.9999962272379699,
4371 "unit": "ratio",
4372 },
4373 "score.total": {
4374 "value": 0.9999963046248997,
4375 "unit": "ratio",
4376 },
4377 "score.weight.a": {
4378 "value": 0.33333333333333337,
4379 "unit": "ratio",
4380 },
4381 "score.weight.b": {
4382 "value": 0.6666666666666667,
4383 "unit": "ratio",
4384 },
4385 "score.weight.c": {
4386 "value": 0.0,
4387 "unit": "ratio",
4388 },
4389 },
4390 }
4391 "###);
4392 }
4393
4394 #[test]
4395 fn test_computed_performance_score_falls_back_to_default_profile() {
4396 let json = r#"
4397 {
4398 "type": "transaction",
4399 "timestamp": "2021-04-26T08:00:05+0100",
4400 "start_timestamp": "2021-04-26T08:00:00+0100",
4401 "measurements": {
4402 "a": {"value": 213, "unit": "millisecond"},
4403 "b": {"value": 213, "unit": "millisecond"}
4404 },
4405 "contexts": {}
4406 }
4407 "#;
4408
4409 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4410
4411 let performance_score: PerformanceScoreConfig = serde_json::from_value(json!({
4412 "profiles": [
4413 {
4414 "name": "Mobile",
4415 "scoreComponents": [
4416 {
4417 "measurement": "a",
4418 "weight": 0.15,
4419 "p10": 900,
4420 "p50": 1600,
4421 "optional": true
4422 },
4423 {
4424 "measurement": "b",
4425 "weight": 0.30,
4426 "p10": 1200,
4427 "p50": 2400,
4428 "optional": true
4429 },
4430 {
4431 "measurement": "c",
4432 "weight": 0.55,
4433 "p10": 1200,
4434 "p50": 2400,
4435 "optional": true
4436 },
4437 ],
4438 "condition": {
4439 "op":"eq",
4440 "name": "event.contexts.browser.name",
4441 "value": "Chrome Mobile"
4442 }
4443 },
4444 {
4445 "name": "Desktop",
4446 "scoreComponents": [
4447 {
4448 "measurement": "a",
4449 "weight": 0.15,
4450 "p10": 900,
4451 "p50": 1600,
4452 "optional": true
4453 },
4454 {
4455 "measurement": "b",
4456 "weight": 0.30,
4457 "p10": 1200,
4458 "p50": 2400,
4459 "optional": true
4460 },
4461 {
4462 "measurement": "c",
4463 "weight": 0.55,
4464 "p10": 1200,
4465 "p50": 2400,
4466 "optional": true
4467 },
4468 ],
4469 "condition": {
4470 "op":"eq",
4471 "name": "event.contexts.browser.name",
4472 "value": "Chrome"
4473 }
4474 },
4475 {
4476 "name": "Default",
4477 "scoreComponents": [
4478 {
4479 "measurement": "a",
4480 "weight": 0.15,
4481 "p10": 100,
4482 "p50": 200,
4483 "optional": true
4484 },
4485 {
4486 "measurement": "b",
4487 "weight": 0.30,
4488 "p10": 100,
4489 "p50": 200,
4490 "optional": true
4491 },
4492 {
4493 "measurement": "c",
4494 "weight": 0.55,
4495 "p10": 100,
4496 "p50": 200,
4497 "optional": true
4498 },
4499 ],
4500 "condition": {
4501 "op": "and",
4502 "inner": [],
4503 }
4504 }
4505 ]
4506 }))
4507 .unwrap();
4508
4509 normalize_performance_score(&mut event, Some(&performance_score));
4510
4511 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4512 {
4513 "type": "transaction",
4514 "timestamp": 1619420405.0,
4515 "start_timestamp": 1619420400.0,
4516 "contexts": {},
4517 "measurements": {
4518 "a": {
4519 "value": 213.0,
4520 "unit": "millisecond",
4521 },
4522 "b": {
4523 "value": 213.0,
4524 "unit": "millisecond",
4525 },
4526 "score.a": {
4527 "value": 0.15121816827413334,
4528 "unit": "ratio",
4529 },
4530 "score.b": {
4531 "value": 0.3024363365482667,
4532 "unit": "ratio",
4533 },
4534 "score.ratio.a": {
4535 "value": 0.45365450482239994,
4536 "unit": "ratio",
4537 },
4538 "score.ratio.b": {
4539 "value": 0.45365450482239994,
4540 "unit": "ratio",
4541 },
4542 "score.total": {
4543 "value": 0.4536545048224,
4544 "unit": "ratio",
4545 },
4546 "score.weight.a": {
4547 "value": 0.33333333333333337,
4548 "unit": "ratio",
4549 },
4550 "score.weight.b": {
4551 "value": 0.6666666666666667,
4552 "unit": "ratio",
4553 },
4554 "score.weight.c": {
4555 "value": 0.0,
4556 "unit": "ratio",
4557 },
4558 },
4559 }
4560 "###);
4561 }
4562
4563 #[test]
4564 fn test_normalization_removes_reprocessing_context() {
4565 let json = r#"{
4566 "contexts": {
4567 "reprocessing": {}
4568 }
4569 }"#;
4570 let mut event = Annotated::<Event>::from_json(json).unwrap();
4571 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4572 normalize_event(&mut event, &NormalizationConfig::default());
4573 assert!(!get_value!(event.contexts!).contains_key("reprocessing"));
4574 }
4575
4576 #[test]
4577 fn test_renormalization_does_not_remove_reprocessing_context() {
4578 let json = r#"{
4579 "contexts": {
4580 "reprocessing": {}
4581 }
4582 }"#;
4583 let mut event = Annotated::<Event>::from_json(json).unwrap();
4584 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4585 normalize_event(
4586 &mut event,
4587 &NormalizationConfig {
4588 is_renormalize: true,
4589 ..Default::default()
4590 },
4591 );
4592 assert!(get_value!(event.contexts!).contains_key("reprocessing"));
4593 }
4594
4595 #[test]
4596 fn test_normalize_user() {
4597 let json = r#"{
4598 "user": {
4599 "id": "123456",
4600 "username": "john",
4601 "other": "value"
4602 }
4603 }"#;
4604 let mut event = Annotated::<Event>::from_json(json).unwrap();
4605 normalize_user(event.value_mut().as_mut().unwrap());
4606
4607 let user = event.value().unwrap().user.value().unwrap();
4608 assert_eq!(user.data, {
4609 let mut map = Object::new();
4610 map.insert(
4611 "other".to_owned(),
4612 Annotated::new(Value::String("value".to_owned())),
4613 );
4614 Annotated::new(map)
4615 });
4616 assert_eq!(user.other, Object::new());
4617 assert_eq!(user.username, Annotated::new("john".to_owned().into()));
4618 assert_eq!(user.sentry_user, Annotated::new("id:123456".to_owned()));
4619 }
4620
4621 #[test]
4622 fn test_handle_types_in_spaced_exception_values() {
4623 let mut exception = Annotated::new(Exception {
4624 value: Annotated::new("ValueError: unauthorized".to_owned().into()),
4625 ..Exception::default()
4626 });
4627 normalize_exception(&mut exception);
4628
4629 let exception = exception.value().unwrap();
4630 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4631 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4632 }
4633
4634 #[test]
4635 fn test_handle_types_in_non_spaced_excepton_values() {
4636 let mut exception = Annotated::new(Exception {
4637 value: Annotated::new("ValueError:unauthorized".to_owned().into()),
4638 ..Exception::default()
4639 });
4640 normalize_exception(&mut exception);
4641
4642 let exception = exception.value().unwrap();
4643 assert_eq!(exception.value.as_str(), Some("unauthorized"));
4644 assert_eq!(exception.ty.as_str(), Some("ValueError"));
4645 }
4646
4647 #[test]
4648 fn test_rejects_empty_exception_fields() {
4649 let mut exception = Annotated::new(Exception {
4650 value: Annotated::new("".to_owned().into()),
4651 ty: Annotated::new("".to_owned()),
4652 ..Default::default()
4653 });
4654
4655 normalize_exception(&mut exception);
4656
4657 assert!(exception.value().is_none());
4658 assert!(exception.meta().has_errors());
4659 }
4660
4661 #[test]
4662 fn test_json_value() {
4663 let mut exception = Annotated::new(Exception {
4664 value: Annotated::new(r#"{"unauthorized":true}"#.to_owned().into()),
4665 ..Exception::default()
4666 });
4667
4668 normalize_exception(&mut exception);
4669
4670 let exception = exception.value().unwrap();
4671
4672 assert_eq!(exception.value.as_str(), Some(r#"{"unauthorized":true}"#));
4674 assert_eq!(exception.ty.value(), None);
4675 }
4676
4677 #[test]
4678 fn test_exception_invalid() {
4679 let mut exception = Annotated::new(Exception::default());
4680
4681 normalize_exception(&mut exception);
4682
4683 let expected = Error::with(ErrorKind::MissingAttribute, |error| {
4684 error.insert("attribute", "type or value");
4685 });
4686 assert_eq!(
4687 exception.meta().iter_errors().collect_tuple(),
4688 Some((&expected,))
4689 );
4690 }
4691
4692 #[test]
4693 fn test_normalize_exception() {
4694 let mut event = Annotated::new(Event {
4695 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
4696 ty: Annotated::empty(),
4698 value: Annotated::empty(),
4699 ..Default::default()
4700 })])),
4701 ..Default::default()
4702 });
4703
4704 normalize_event(&mut event, &NormalizationConfig::default());
4705
4706 let exception = event
4707 .value()
4708 .unwrap()
4709 .exceptions
4710 .value()
4711 .unwrap()
4712 .values
4713 .value()
4714 .unwrap()
4715 .first()
4716 .unwrap();
4717
4718 assert_debug_snapshot!(exception.meta(), @r###"
4719 Meta {
4720 remarks: [],
4721 errors: [
4722 Error {
4723 kind: MissingAttribute,
4724 data: {
4725 "attribute": String(
4726 "type or value",
4727 ),
4728 },
4729 },
4730 ],
4731 original_length: None,
4732 original_value: Some(
4733 Object(
4734 {
4735 "mechanism": ~,
4736 "module": ~,
4737 "raw_stacktrace": ~,
4738 "stacktrace": ~,
4739 "thread_id": ~,
4740 "type": ~,
4741 "value": ~,
4742 },
4743 ),
4744 ),
4745 }
4746 "###);
4747 }
4748
4749 #[test]
4750 fn test_normalize_breadcrumbs() {
4751 let mut event = Event {
4752 breadcrumbs: Annotated::new(Values {
4753 values: Annotated::new(vec![Annotated::new(Breadcrumb::default())]),
4754 ..Default::default()
4755 }),
4756 ..Default::default()
4757 };
4758 normalize_breadcrumbs(&mut event);
4759
4760 let breadcrumb = event
4761 .breadcrumbs
4762 .value()
4763 .unwrap()
4764 .values
4765 .value()
4766 .unwrap()
4767 .first()
4768 .unwrap()
4769 .value()
4770 .unwrap();
4771 assert_eq!(breadcrumb.ty.value().unwrap(), "default");
4772 assert_eq!(&breadcrumb.level.value().unwrap().to_string(), "info");
4773 }
4774
4775 #[test]
4776 fn test_other_debug_images_have_meta_errors() {
4777 let mut event = Event {
4778 debug_meta: Annotated::new(DebugMeta {
4779 images: Annotated::new(vec![Annotated::new(
4780 DebugImage::Other(BTreeMap::default()),
4781 )]),
4782 ..Default::default()
4783 }),
4784 ..Default::default()
4785 };
4786 normalize_debug_meta(&mut event);
4787
4788 let debug_image_meta = event
4789 .debug_meta
4790 .value()
4791 .unwrap()
4792 .images
4793 .value()
4794 .unwrap()
4795 .first()
4796 .unwrap()
4797 .meta();
4798 assert_debug_snapshot!(debug_image_meta, @r###"
4799 Meta {
4800 remarks: [],
4801 errors: [
4802 Error {
4803 kind: InvalidData,
4804 data: {
4805 "reason": String(
4806 "unsupported debug image type",
4807 ),
4808 },
4809 },
4810 ],
4811 original_length: None,
4812 original_value: Some(
4813 Object(
4814 {},
4815 ),
4816 ),
4817 }
4818 "###);
4819 }
4820
4821 #[test]
4822 fn test_skip_span_normalization_when_configured() {
4823 let json = r#"{
4824 "type": "transaction",
4825 "start_timestamp": 1,
4826 "timestamp": 2,
4827 "contexts": {
4828 "trace": {
4829 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4830 "span_id": "aaaaaaaaaaaaaaaa"
4831 }
4832 },
4833 "spans": [
4834 {
4835 "op": "db",
4836 "description": "SELECT * FROM table;",
4837 "start_timestamp": 1,
4838 "timestamp": 2,
4839 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
4840 "span_id": "bbbbbbbbbbbbbbbb",
4841 "parent_span_id": "aaaaaaaaaaaaaaaa"
4842 }
4843 ]
4844 }"#;
4845
4846 let mut event = Annotated::<Event>::from_json(json).unwrap();
4847 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4848 normalize_event(
4849 &mut event,
4850 &NormalizationConfig {
4851 is_renormalize: true,
4852 ..Default::default()
4853 },
4854 );
4855 assert!(get_value!(event.spans[0].exclusive_time).is_none());
4856 normalize_event(
4857 &mut event,
4858 &NormalizationConfig {
4859 is_renormalize: false,
4860 ..Default::default()
4861 },
4862 );
4863 assert!(get_value!(event.spans[0].exclusive_time).is_some());
4864 }
4865
4866 #[test]
4867 fn test_normalize_trace_context_tags_extracts_lcp_info() {
4868 let json = r#"{
4869 "type": "transaction",
4870 "start_timestamp": 1,
4871 "timestamp": 2,
4872 "contexts": {
4873 "trace": {
4874 "data": {
4875 "lcp.element": "body > div#app > div > h1#header",
4876 "lcp.size": 24827,
4877 "lcp.id": "header",
4878 "lcp.url": "http://example.com/image.jpg"
4879 }
4880 }
4881 },
4882 "measurements": {
4883 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4884 }
4885 }"#;
4886 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4887 normalize_trace_context_tags(&mut event);
4888 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4889 {
4890 "type": "transaction",
4891 "timestamp": 2.0,
4892 "start_timestamp": 1.0,
4893 "contexts": {
4894 "trace": {
4895 "data": {
4896 "lcp.element": "body > div#app > div > h1#header",
4897 "lcp.size": 24827,
4898 "lcp.id": "header",
4899 "lcp.url": "http://example.com/image.jpg",
4900 },
4901 "type": "trace",
4902 },
4903 },
4904 "tags": [
4905 [
4906 "lcp.element",
4907 "body > div#app > div > h1#header",
4908 ],
4909 [
4910 "lcp.size",
4911 "24827",
4912 ],
4913 [
4914 "lcp.id",
4915 "header",
4916 ],
4917 [
4918 "lcp.url",
4919 "http://example.com/image.jpg",
4920 ],
4921 ],
4922 "measurements": {
4923 "lcp": {
4924 "value": 146.20000000298023,
4925 "unit": "millisecond",
4926 },
4927 },
4928 }
4929 "###);
4930 }
4931
4932 #[test]
4933 fn test_normalize_trace_context_tags_does_not_overwrite_lcp_tags() {
4934 let json = r#"{
4935 "type": "transaction",
4936 "start_timestamp": 1,
4937 "timestamp": 2,
4938 "contexts": {
4939 "trace": {
4940 "data": {
4941 "lcp.element": "body > div#app > div > h1#id",
4942 "lcp.size": 33333,
4943 "lcp.id": "id",
4944 "lcp.url": "http://example.com/another-image.jpg"
4945 }
4946 }
4947 },
4948 "tags": {
4949 "lcp.element": "body > div#app > div > h1#header",
4950 "lcp.size": 24827,
4951 "lcp.id": "header",
4952 "lcp.url": "http://example.com/image.jpg"
4953 },
4954 "measurements": {
4955 "lcp": { "value": 146.20000000298023, "unit": "millisecond" }
4956 }
4957 }"#;
4958 let mut event = Annotated::<Event>::from_json(json).unwrap().0.unwrap();
4959 normalize_trace_context_tags(&mut event);
4960 insta::assert_ron_snapshot!(SerializableAnnotated(&Annotated::new(event)), {}, @r###"
4961 {
4962 "type": "transaction",
4963 "timestamp": 2.0,
4964 "start_timestamp": 1.0,
4965 "contexts": {
4966 "trace": {
4967 "data": {
4968 "lcp.element": "body > div#app > div > h1#id",
4969 "lcp.size": 33333,
4970 "lcp.id": "id",
4971 "lcp.url": "http://example.com/another-image.jpg",
4972 },
4973 "type": "trace",
4974 },
4975 },
4976 "tags": [
4977 [
4978 "lcp.element",
4979 "body > div#app > div > h1#header",
4980 ],
4981 [
4982 "lcp.id",
4983 "header",
4984 ],
4985 [
4986 "lcp.size",
4987 "24827",
4988 ],
4989 [
4990 "lcp.url",
4991 "http://example.com/image.jpg",
4992 ],
4993 ],
4994 "measurements": {
4995 "lcp": {
4996 "value": 146.20000000298023,
4997 "unit": "millisecond",
4998 },
4999 },
5000 }
5001 "###);
5002 }
5003
5004 #[test]
5005 fn test_tags_are_trimmed() {
5006 let json = r#"
5007 {
5008 "tags": {
5009 "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_",
5010 "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"
5011 }
5012 }
5013 "#;
5014
5015 let mut event = Annotated::<Event>::from_json(json).unwrap();
5016
5017 normalize_event(
5018 &mut event,
5019 &NormalizationConfig {
5020 enable_trimming: true,
5021 ..NormalizationConfig::default()
5022 },
5023 );
5024
5025 insta::assert_debug_snapshot!(get_value!(event.tags!), @r###"
5026 Tags(
5027 PairList(
5028 [
5029 TagEntry(
5030 "key",
5031 Annotated(
5032 "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...",
5033 Meta {
5034 remarks: [
5035 Remark {
5036 ty: Substituted,
5037 rule_id: "!limit",
5038 range: Some(
5039 (
5040 197,
5041 200,
5042 ),
5043 ),
5044 },
5045 ],
5046 errors: [],
5047 original_length: Some(
5048 210,
5049 ),
5050 original_value: None,
5051 },
5052 ),
5053 ),
5054 TagEntry(
5055 Annotated(
5056 "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...",
5057 Meta {
5058 remarks: [
5059 Remark {
5060 ty: Substituted,
5061 rule_id: "!limit",
5062 range: Some(
5063 (
5064 197,
5065 200,
5066 ),
5067 ),
5068 },
5069 ],
5070 errors: [],
5071 original_length: Some(
5072 210,
5073 ),
5074 original_value: None,
5075 },
5076 ),
5077 "value",
5078 ),
5079 ],
5080 ),
5081 )
5082 "###);
5083 }
5084}