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