1use std::collections::HashMap;
2use std::hash::Hash;
3
4use relay_base_schema::metrics::MetricUnit;
5use relay_event_schema::protocol::{Event, VALID_PLATFORMS};
6use relay_protocol::{FiniteF64, RuleCondition};
7use serde::{Deserialize, Serialize};
8
9pub mod breakdowns;
10pub mod contexts;
11pub mod nel;
12pub mod request;
13pub mod span;
14pub mod user_agent;
15pub mod utils;
16
17#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)]
19#[serde(default, rename_all = "camelCase")]
20pub struct BuiltinMeasurementKey {
21 name: String,
22 unit: MetricUnit,
23 #[serde(skip_serializing_if = "is_false")]
24 allow_negative: bool,
25}
26
27fn is_false(b: &bool) -> bool {
28 !b
29}
30
31impl BuiltinMeasurementKey {
32 pub fn new(name: impl Into<String>, unit: MetricUnit) -> Self {
34 Self {
35 name: name.into(),
36 unit,
37 allow_negative: false,
38 }
39 }
40
41 pub fn name(&self) -> &str {
43 &self.name
44 }
45
46 pub fn unit(&self) -> &MetricUnit {
48 &self.unit
49 }
50
51 pub fn allow_negative(&self) -> &bool {
53 &self.allow_negative
54 }
55}
56
57#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Hash)]
59#[serde(default, rename_all = "camelCase")]
60pub struct MeasurementsConfig {
61 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub builtin_measurements: Vec<BuiltinMeasurementKey>,
64
65 pub max_custom_measurements: usize,
67}
68
69impl MeasurementsConfig {
70 pub const MEASUREMENT_MRI_OVERHEAD: usize = 29;
73}
74
75pub fn is_valid_platform(platform: &str) -> bool {
79 VALID_PLATFORMS.contains(&platform)
80}
81
82pub fn normalize_app_start_spans(event: &mut Event) {
86 if !event.sdk_name().eq("sentry.javascript.react-native")
87 || !(event.sdk_version().starts_with("4.4")
88 || event.sdk_version().starts_with("4.3")
89 || event.sdk_version().starts_with("4.2")
90 || event.sdk_version().starts_with("4.1")
91 || event.sdk_version().starts_with("4.0")
92 || event.sdk_version().starts_with('3'))
93 {
94 return;
95 }
96
97 if let Some(spans) = event.spans.value_mut() {
98 for span in spans {
99 if let Some(span) = span.value_mut() {
100 if let Some(op) = span.op.value() {
101 if op == "app_start_cold" {
102 span.op.set_value(Some("app.start.cold".to_owned()));
103 break;
104 } else if op == "app_start_warm" {
105 span.op.set_value(Some("app.start.warm".to_owned()));
106 break;
107 }
108 }
109 }
110 }
111 }
112}
113
114#[derive(Clone, Debug)]
117pub struct CombinedMeasurementsConfig<'a> {
118 project: Option<&'a MeasurementsConfig>,
119 global: Option<&'a MeasurementsConfig>,
120}
121
122impl<'a> CombinedMeasurementsConfig<'a> {
123 pub fn new(
125 project: Option<&'a MeasurementsConfig>,
126 global: Option<&'a MeasurementsConfig>,
127 ) -> Self {
128 CombinedMeasurementsConfig { project, global }
129 }
130
131 pub fn builtin_measurement_keys(
136 &'a self,
137 ) -> impl Iterator<Item = &'a BuiltinMeasurementKey> + 'a {
138 let project = self
139 .project
140 .map(|p| p.builtin_measurements.as_slice())
141 .unwrap_or_default();
142
143 let global = self
144 .global
145 .map(|g| g.builtin_measurements.as_slice())
146 .unwrap_or_default();
147
148 project
149 .iter()
150 .chain(global.iter().filter(|key| !project.contains(key)))
151 }
152
153 pub fn max_custom_measurements(&'a self) -> Option<usize> {
156 match (&self.project, &self.global) {
157 (None, None) => None,
158 (None, Some(global)) => Some(global.max_custom_measurements),
159 (Some(project), None) => Some(project.max_custom_measurements),
160 (Some(project), Some(global)) => Some(std::cmp::min(
161 project.max_custom_measurements,
162 global.max_custom_measurements,
163 )),
164 }
165 }
166}
167
168#[derive(Debug, Default, Clone, Serialize, Deserialize)]
173pub struct PerformanceScoreWeightedComponent {
174 pub measurement: String,
177 pub weight: FiniteF64,
179 pub p10: FiniteF64,
181 pub p50: FiniteF64,
183 #[serde(default)]
186 pub optional: bool,
187}
188
189#[derive(Debug, Default, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct PerformanceScoreProfile {
196 pub name: Option<String>,
198 #[serde(default, skip_serializing_if = "Vec::is_empty")]
200 pub score_components: Vec<PerformanceScoreWeightedComponent>,
201 pub condition: Option<RuleCondition>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub version: Option<String>,
206}
207
208#[derive(Debug, Default, Clone, Serialize, Deserialize)]
213pub struct PerformanceScoreConfig {
214 #[serde(default, skip_serializing_if = "Vec::is_empty")]
216 pub profiles: Vec<PerformanceScoreProfile>,
217}
218
219#[derive(Clone, Default, Debug, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct ModelCosts {
241 pub version: u16,
243
244 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
246 pub models: HashMap<String, ModelCostV2>,
247}
248
249impl ModelCosts {
250 const SUPPORTED_VERSION: u16 = 2;
251
252 pub fn is_empty(&self) -> bool {
254 self.models.is_empty() || !self.is_enabled()
255 }
256
257 pub fn is_enabled(&self) -> bool {
259 self.version == Self::SUPPORTED_VERSION
260 }
261
262 pub fn cost_per_token(&self, model_id: &str) -> Option<ModelCostV2> {
264 if self.is_enabled() {
265 self.models.get(model_id).copied()
266 } else {
267 None
268 }
269 }
270}
271
272#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
274#[serde(rename_all = "camelCase")]
275pub struct ModelCostV2 {
276 pub input_per_token: f64,
278 pub output_per_token: f64,
280 pub output_reasoning_per_token: f64,
282 pub input_cached_per_token: f64,
284}
285
286#[cfg(test)]
287mod tests {
288 use chrono::{TimeZone, Utc};
289 use insta::{assert_debug_snapshot, assert_json_snapshot};
290 use itertools::Itertools;
291 use relay_base_schema::events::EventType;
292 use relay_base_schema::metrics::DurationUnit;
293 use relay_base_schema::spans::SpanStatus;
294 use relay_event_schema::protocol::{
295 ClientSdkInfo, Context, ContextInner, Contexts, DebugImage, DebugMeta, EventId, Exception,
296 Frame, Geo, IpAddr, LenientString, Level, LogEntry, PairList, RawStacktrace, ReplayContext,
297 Request, Span, Stacktrace, TagEntry, Tags, TraceContext, User, Values,
298 };
299 use relay_protocol::{
300 Annotated, Error, ErrorKind, FromValue, Object, SerializableAnnotated, Value,
301 assert_annotated_snapshot, get_path, get_value,
302 };
303 use serde_json::json;
304 use similar_asserts::assert_eq;
305 use uuid::Uuid;
306
307 use crate::stacktrace::normalize_non_raw_frame;
308 use crate::validation::validate_event;
309 use crate::{EventValidationConfig, GeoIpLookup, NormalizationConfig, normalize_event};
310
311 use super::*;
312
313 #[test]
315 fn test_model_cost_version_sent_as_number() {
316 let original_v2 = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#;
318 let deserialized_v2: ModelCosts = serde_json::from_str(original_v2).unwrap();
319 assert_debug_snapshot!(
320 deserialized_v2,
321 @r###"
322 ModelCosts {
323 version: 2,
324 models: {
325 "gpt-4": ModelCostV2 {
326 input_per_token: 0.03,
327 output_per_token: 0.06,
328 output_reasoning_per_token: 0.12,
329 input_cached_per_token: 0.015,
330 },
331 },
332 }
333 "###,
334 );
335
336 let original_unknown = r#"{"version":99,"models":{}}"#;
338 let deserialized_unknown: ModelCosts = serde_json::from_str(original_unknown).unwrap();
339 assert_eq!(deserialized_unknown.version, 99);
340 assert!(!deserialized_unknown.is_enabled());
341 }
342
343 #[test]
344 fn test_model_cost_config_v2() {
345 let original = r#"{"version":2,"models":{"gpt-4":{"inputPerToken":0.03,"outputPerToken":0.06,"outputReasoningPerToken":0.12,"inputCachedPerToken":0.015}}}"#;
346 let deserialized: ModelCosts = serde_json::from_str(original).unwrap();
347 assert_debug_snapshot!(deserialized, @r###"
348 ModelCosts {
349 version: 2,
350 models: {
351 "gpt-4": ModelCostV2 {
352 input_per_token: 0.03,
353 output_per_token: 0.06,
354 output_reasoning_per_token: 0.12,
355 input_cached_per_token: 0.015,
356 },
357 },
358 }
359 "###);
360
361 let serialized = serde_json::to_string(&deserialized).unwrap();
362 assert_eq!(&serialized, original);
363 }
364
365 #[test]
366 fn test_model_cost_functionality_v2() {
367 let mut models_map = HashMap::new();
369 models_map.insert(
370 "gpt-4".to_owned(),
371 ModelCostV2 {
372 input_per_token: 0.03,
373 output_per_token: 0.06,
374 output_reasoning_per_token: 0.12,
375 input_cached_per_token: 0.015,
376 },
377 );
378 let v2_config = ModelCosts {
379 version: 2,
380 models: models_map,
381 };
382 assert!(v2_config.is_enabled());
383 let cost = v2_config.cost_per_token("gpt-4").unwrap();
384 assert_eq!(
385 cost,
386 ModelCostV2 {
387 input_per_token: 0.03,
388 output_per_token: 0.06,
389 output_reasoning_per_token: 0.12,
390 input_cached_per_token: 0.015,
391 }
392 );
393 }
394
395 #[test]
396 fn test_model_cost_unknown_version() {
397 let unknown_version_json = r#"{"version":3,"models":{"some-model":{"inputPerToken":0.01,"outputPerToken":0.02,"outputReasoningPerToken":0.03,"inputCachedPerToken":0.005}}}"#;
399 let deserialized: ModelCosts = serde_json::from_str(unknown_version_json).unwrap();
400 assert_eq!(deserialized.version, 3);
401 assert!(!deserialized.is_enabled());
402 assert_eq!(deserialized.cost_per_token("some-model"), None);
403
404 let version_zero_json = r#"{"version":0,"models":{}}"#;
406 let deserialized: ModelCosts = serde_json::from_str(version_zero_json).unwrap();
407 assert_eq!(deserialized.version, 0);
408 assert!(!deserialized.is_enabled());
409 }
410
411 #[test]
412 fn test_merge_builtin_measurement_keys() {
413 let foo = BuiltinMeasurementKey::new("foo", MetricUnit::Duration(DurationUnit::Hour));
414 let bar = BuiltinMeasurementKey::new("bar", MetricUnit::Duration(DurationUnit::Day));
415 let baz = BuiltinMeasurementKey::new("baz", MetricUnit::Duration(DurationUnit::Week));
416
417 let proj = MeasurementsConfig {
418 builtin_measurements: vec![foo.clone(), bar.clone()],
419 max_custom_measurements: 4,
420 };
421
422 let glob = MeasurementsConfig {
423 builtin_measurements: vec![baz.clone(), bar.clone()],
425 max_custom_measurements: 4,
426 };
427 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
428
429 let keys = dynamic_config.builtin_measurement_keys().collect_vec();
430
431 assert_eq!(keys, vec![&foo, &bar, &baz]);
432 }
433
434 #[test]
435 fn test_max_custom_measurement() {
436 let dynamic_config = CombinedMeasurementsConfig::new(None, None);
438 assert!(dynamic_config.max_custom_measurements().is_none());
439
440 let proj = MeasurementsConfig {
441 builtin_measurements: vec![],
442 max_custom_measurements: 3,
443 };
444
445 let glob = MeasurementsConfig {
446 builtin_measurements: vec![],
447 max_custom_measurements: 4,
448 };
449
450 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), None);
452 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
453
454 let dynamic_config = CombinedMeasurementsConfig::new(None, Some(&glob));
456 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 4);
457
458 let dynamic_config = CombinedMeasurementsConfig::new(Some(&proj), Some(&glob));
460 assert_eq!(dynamic_config.max_custom_measurements().unwrap(), 3);
461 }
462
463 #[test]
464 fn test_geo_from_ip_address() {
465 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
466
467 let json = r#"{
468 "user": {
469 "ip_address": "2.125.160.216"
470 }
471 }"#;
472 let mut event = Annotated::<Event>::from_json(json).unwrap();
473
474 normalize_event(
475 &mut event,
476 &NormalizationConfig {
477 geoip_lookup: Some(&lookup),
478 ..Default::default()
479 },
480 );
481
482 let expected = Annotated::new(Geo {
483 country_code: Annotated::new("GB".to_owned()),
484 city: Annotated::new("Boxford".to_owned()),
485 subdivision: Annotated::new("England".to_owned()),
486 region: Annotated::new("United Kingdom".to_owned()),
487 ..Geo::default()
488 });
489 assert_eq!(get_value!(event.user!).geo, expected);
490 }
491
492 #[test]
493 fn test_user_ip_from_remote_addr() {
494 let mut event = Annotated::new(Event {
495 request: Annotated::from(Request {
496 env: Annotated::new({
497 let mut map = Object::new();
498 map.insert(
499 "REMOTE_ADDR".to_owned(),
500 Annotated::new(Value::String("2.125.160.216".to_owned())),
501 );
502 map
503 }),
504 ..Request::default()
505 }),
506 platform: Annotated::new("javascript".to_owned()),
507 ..Event::default()
508 });
509
510 normalize_event(&mut event, &NormalizationConfig::default());
511
512 let ip_addr = get_value!(event.user.ip_address!);
513 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
514 }
515
516 #[test]
517 fn test_user_ip_from_invalid_remote_addr() {
518 let mut event = Annotated::new(Event {
519 request: Annotated::from(Request {
520 env: Annotated::new({
521 let mut map = Object::new();
522 map.insert(
523 "REMOTE_ADDR".to_owned(),
524 Annotated::new(Value::String("whoops".to_owned())),
525 );
526 map
527 }),
528 ..Request::default()
529 }),
530 platform: Annotated::new("javascript".to_owned()),
531 ..Event::default()
532 });
533
534 normalize_event(&mut event, &NormalizationConfig::default());
535
536 assert_eq!(Annotated::empty(), event.value().unwrap().user);
537 }
538
539 #[test]
540 fn test_user_ip_from_client_ip_without_auto() {
541 let mut event = Annotated::new(Event {
542 platform: Annotated::new("javascript".to_owned()),
543 ..Default::default()
544 });
545
546 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
547
548 normalize_event(
549 &mut event,
550 &NormalizationConfig {
551 client_ip: Some(&ip_address),
552 infer_ip_address: true,
553 ..Default::default()
554 },
555 );
556
557 let ip_addr = get_value!(event.user.ip_address!);
558 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
559 }
560
561 #[test]
562 fn test_user_ip_from_client_ip_with_auto() {
563 let mut event = Annotated::new(Event {
564 user: Annotated::new(User {
565 ip_address: Annotated::new(IpAddr::auto()),
566 ..Default::default()
567 }),
568 ..Default::default()
569 });
570
571 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
572
573 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
574 normalize_event(
575 &mut event,
576 &NormalizationConfig {
577 client_ip: Some(&ip_address),
578 geoip_lookup: Some(&geo),
579 infer_ip_address: true,
580 ..Default::default()
581 },
582 );
583
584 let user = get_value!(event.user!);
585 let ip_addr = user.ip_address.value().expect("ip address missing");
586
587 assert_eq!(ip_addr, &IpAddr("2.125.160.216".to_owned()));
588 assert!(user.geo.value().is_some());
589 }
590
591 #[test]
592 fn test_user_ip_from_client_ip_without_appropriate_platform() {
593 let mut event = Annotated::new(Event::default());
594
595 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
596 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
597 normalize_event(
598 &mut event,
599 &NormalizationConfig {
600 client_ip: Some(&ip_address),
601 geoip_lookup: Some(&geo),
602 ..Default::default()
603 },
604 );
605
606 let user = get_value!(event.user!);
607 assert!(user.ip_address.value().is_none());
608 assert!(user.geo.value().is_some());
609 }
610
611 #[test]
612 fn test_geo_present_if_ip_inferring_disabled() {
613 let mut event = Annotated::new(Event {
614 user: Annotated::new(User {
615 ip_address: Annotated::new(IpAddr::auto()),
616 ..Default::default()
617 }),
618 ..Default::default()
619 });
620
621 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
622 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
623
624 normalize_event(
625 &mut event,
626 &NormalizationConfig {
627 client_ip: Some(&ip_address),
628 geoip_lookup: Some(&geo),
629 infer_ip_address: false,
630 ..Default::default()
631 },
632 );
633
634 let user = get_value!(event.user!);
635 assert!(user.ip_address.value().unwrap().is_auto());
636 assert!(user.geo.value().is_some());
637 }
638
639 #[test]
640 fn test_geo_and_ip_present_if_ip_inferring_enabled() {
641 let mut event = Annotated::new(Event {
642 user: Annotated::new(User {
643 ip_address: Annotated::new(IpAddr::auto()),
644 ..Default::default()
645 }),
646 ..Default::default()
647 });
648
649 let ip_address = IpAddr::parse("2.125.160.216").unwrap();
650 let geo = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
651
652 normalize_event(
653 &mut event,
654 &NormalizationConfig {
655 client_ip: Some(&ip_address),
656 geoip_lookup: Some(&geo),
657 infer_ip_address: true,
658 ..Default::default()
659 },
660 );
661
662 let user = get_value!(event.user!);
663 assert_eq!(
664 user.ip_address.value(),
665 Some(&IpAddr::parse("2.125.160.216").unwrap())
666 );
667 assert!(user.geo.value().is_some());
668 }
669
670 #[test]
671 fn test_event_level_defaulted() {
672 let mut event = Annotated::new(Event::default());
673 normalize_event(&mut event, &NormalizationConfig::default());
674 assert_eq!(get_value!(event.level), Some(&Level::Error));
675 }
676
677 #[test]
678 fn test_transaction_level_untouched() {
679 let mut event = Annotated::new(Event {
680 ty: Annotated::new(EventType::Transaction),
681 timestamp: Annotated::new(Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into()),
682 start_timestamp: Annotated::new(
683 Utc.with_ymd_and_hms(1987, 6, 5, 4, 3, 2).unwrap().into(),
684 ),
685 contexts: {
686 let mut contexts = Contexts::new();
687 contexts.add(TraceContext {
688 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
689 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
690 op: Annotated::new("http.server".to_owned()),
691 ..Default::default()
692 });
693 Annotated::new(contexts)
694 },
695 ..Event::default()
696 });
697 normalize_event(&mut event, &NormalizationConfig::default());
698 assert_eq!(get_value!(event.level), Some(&Level::Info));
699 }
700
701 #[test]
702 fn test_environment_tag_is_moved() {
703 let mut event = Annotated::new(Event {
704 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
705 Annotated::new("environment".to_owned()),
706 Annotated::new("despacito".to_owned()),
707 ))]))),
708 ..Event::default()
709 });
710
711 normalize_event(&mut event, &NormalizationConfig::default());
712
713 let event = event.value().unwrap();
714
715 assert_eq!(event.environment.as_str(), Some("despacito"));
716 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
717 }
718
719 #[test]
720 fn test_empty_environment_is_removed_and_overwritten_with_tag() {
721 let mut event = Annotated::new(Event {
722 tags: Annotated::new(Tags(PairList(vec![Annotated::new(TagEntry(
723 Annotated::new("environment".to_owned()),
724 Annotated::new("despacito".to_owned()),
725 ))]))),
726 environment: Annotated::new("".to_owned()),
727 ..Event::default()
728 });
729
730 normalize_event(&mut event, &NormalizationConfig::default());
731
732 let event = event.value().unwrap();
733
734 assert_eq!(event.environment.as_str(), Some("despacito"));
735 assert_eq!(event.tags.value(), Some(&Tags(vec![].into())));
736 }
737
738 #[test]
739 fn test_empty_environment_is_removed() {
740 let mut event = Annotated::new(Event {
741 environment: Annotated::new("".to_owned()),
742 ..Event::default()
743 });
744
745 normalize_event(&mut event, &NormalizationConfig::default());
746 assert_eq!(get_value!(event.environment), None);
747 }
748 #[test]
749 fn test_replay_id_added_from_dsc() {
750 let replay_id = Uuid::new_v4();
751 let mut event = Annotated::new(Event {
752 contexts: Annotated::new(Contexts(Object::new())),
753 ..Event::default()
754 });
755 normalize_event(
756 &mut event,
757 &NormalizationConfig {
758 replay_id: Some(replay_id),
759 ..Default::default()
760 },
761 );
762
763 let event = event.value().unwrap();
764
765 assert_eq!(event.contexts, {
766 let mut contexts = Contexts::new();
767 contexts.add(ReplayContext {
768 replay_id: Annotated::new(EventId(replay_id)),
769 other: Object::default(),
770 });
771 Annotated::new(contexts)
772 })
773 }
774
775 #[test]
776 fn test_none_environment_errors() {
777 let mut event = Annotated::new(Event {
778 environment: Annotated::new("none".to_owned()),
779 ..Event::default()
780 });
781
782 normalize_event(&mut event, &NormalizationConfig::default());
783
784 let environment = get_path!(event.environment!);
785 let expected_original = &Value::String("none".to_owned());
786
787 assert_eq!(
788 environment.meta().iter_errors().collect::<Vec<&Error>>(),
789 vec![&Error::new(ErrorKind::InvalidData)],
790 );
791 assert_eq!(
792 environment.meta().original_value().unwrap(),
793 expected_original
794 );
795 assert_eq!(environment.value(), None);
796 }
797
798 #[test]
799 fn test_invalid_release_removed() {
800 let mut event = Annotated::new(Event {
801 release: Annotated::new(LenientString("Latest".to_owned())),
802 ..Event::default()
803 });
804
805 normalize_event(&mut event, &NormalizationConfig::default());
806
807 let release = get_path!(event.release!);
808 let expected_original = &Value::String("Latest".to_owned());
809
810 assert_eq!(
811 release.meta().iter_errors().collect::<Vec<&Error>>(),
812 vec![&Error::new(ErrorKind::InvalidData)],
813 );
814 assert_eq!(release.meta().original_value().unwrap(), expected_original);
815 assert_eq!(release.value(), None);
816 }
817
818 #[test]
819 fn test_top_level_keys_moved_into_tags() {
820 let mut event = Annotated::new(Event {
821 server_name: Annotated::new("foo".to_owned()),
822 site: Annotated::new("foo".to_owned()),
823 tags: Annotated::new(Tags(PairList(vec![
824 Annotated::new(TagEntry(
825 Annotated::new("site".to_owned()),
826 Annotated::new("old".to_owned()),
827 )),
828 Annotated::new(TagEntry(
829 Annotated::new("server_name".to_owned()),
830 Annotated::new("old".to_owned()),
831 )),
832 ]))),
833 ..Event::default()
834 });
835
836 normalize_event(&mut event, &NormalizationConfig::default());
837
838 assert_eq!(get_value!(event.site), None);
839 assert_eq!(get_value!(event.server_name), None);
840
841 assert_eq!(
842 get_value!(event.tags!),
843 &Tags(PairList(vec![
844 Annotated::new(TagEntry(
845 Annotated::new("site".to_owned()),
846 Annotated::new("foo".to_owned()),
847 )),
848 Annotated::new(TagEntry(
849 Annotated::new("server_name".to_owned()),
850 Annotated::new("foo".to_owned()),
851 )),
852 ]))
853 );
854 }
855
856 #[test]
857 fn test_internal_tags_removed() {
858 let mut event = Annotated::new(Event {
859 tags: Annotated::new(Tags(PairList(vec![
860 Annotated::new(TagEntry(
861 Annotated::new("release".to_owned()),
862 Annotated::new("foo".to_owned()),
863 )),
864 Annotated::new(TagEntry(
865 Annotated::new("dist".to_owned()),
866 Annotated::new("foo".to_owned()),
867 )),
868 Annotated::new(TagEntry(
869 Annotated::new("user".to_owned()),
870 Annotated::new("foo".to_owned()),
871 )),
872 Annotated::new(TagEntry(
873 Annotated::new("filename".to_owned()),
874 Annotated::new("foo".to_owned()),
875 )),
876 Annotated::new(TagEntry(
877 Annotated::new("function".to_owned()),
878 Annotated::new("foo".to_owned()),
879 )),
880 Annotated::new(TagEntry(
881 Annotated::new("something".to_owned()),
882 Annotated::new("else".to_owned()),
883 )),
884 ]))),
885 ..Event::default()
886 });
887
888 normalize_event(&mut event, &NormalizationConfig::default());
889
890 assert_eq!(get_value!(event.tags!).len(), 1);
891 }
892
893 #[test]
894 fn test_empty_tags_removed() {
895 let mut event = Annotated::new(Event {
896 tags: Annotated::new(Tags(PairList(vec![
897 Annotated::new(TagEntry(
898 Annotated::new("".to_owned()),
899 Annotated::new("foo".to_owned()),
900 )),
901 Annotated::new(TagEntry(
902 Annotated::new("foo".to_owned()),
903 Annotated::new("".to_owned()),
904 )),
905 Annotated::new(TagEntry(
906 Annotated::new("something".to_owned()),
907 Annotated::new("else".to_owned()),
908 )),
909 ]))),
910 ..Event::default()
911 });
912
913 normalize_event(&mut event, &NormalizationConfig::default());
914
915 assert_eq!(
916 get_value!(event.tags!),
917 &Tags(PairList(vec![
918 Annotated::new(TagEntry(
919 Annotated::from_error(Error::nonempty(), None),
920 Annotated::new("foo".to_owned()),
921 )),
922 Annotated::new(TagEntry(
923 Annotated::new("foo".to_owned()),
924 Annotated::from_error(Error::nonempty(), None),
925 )),
926 Annotated::new(TagEntry(
927 Annotated::new("something".to_owned()),
928 Annotated::new("else".to_owned()),
929 )),
930 ]))
931 );
932 }
933
934 #[test]
935 fn test_tags_deduplicated() {
936 let mut event = Annotated::new(Event {
937 tags: Annotated::new(Tags(PairList(vec![
938 Annotated::new(TagEntry(
939 Annotated::new("foo".to_owned()),
940 Annotated::new("1".to_owned()),
941 )),
942 Annotated::new(TagEntry(
943 Annotated::new("bar".to_owned()),
944 Annotated::new("1".to_owned()),
945 )),
946 Annotated::new(TagEntry(
947 Annotated::new("foo".to_owned()),
948 Annotated::new("2".to_owned()),
949 )),
950 Annotated::new(TagEntry(
951 Annotated::new("bar".to_owned()),
952 Annotated::new("2".to_owned()),
953 )),
954 Annotated::new(TagEntry(
955 Annotated::new("foo".to_owned()),
956 Annotated::new("3".to_owned()),
957 )),
958 ]))),
959 ..Event::default()
960 });
961
962 normalize_event(&mut event, &NormalizationConfig::default());
963
964 assert_eq!(
966 get_value!(event.tags!),
967 &Tags(PairList(vec![
968 Annotated::new(TagEntry(
969 Annotated::new("foo".to_owned()),
970 Annotated::new("1".to_owned()),
971 )),
972 Annotated::new(TagEntry(
973 Annotated::new("bar".to_owned()),
974 Annotated::new("1".to_owned()),
975 )),
976 ]))
977 );
978 }
979
980 #[test]
981 fn test_transaction_status_defaulted_to_unknown() {
982 let mut object = Object::new();
983 let trace_context = TraceContext {
984 status: Annotated::empty(),
986 ..TraceContext::default()
987 };
988 object.insert(
989 "trace".to_owned(),
990 Annotated::new(ContextInner(Context::Trace(Box::new(trace_context)))),
991 );
992
993 let mut event = Annotated::new(Event {
994 contexts: Annotated::new(Contexts(object)),
995 ..Event::default()
996 });
997 normalize_event(&mut event, &NormalizationConfig::default());
998
999 let event = event.value().unwrap();
1000
1001 let event_trace_context = event.context::<TraceContext>().unwrap();
1002 assert_eq!(
1003 event_trace_context.status,
1004 Annotated::new(SpanStatus::Unknown)
1005 )
1006 }
1007
1008 #[test]
1009 fn test_unknown_debug_image() {
1010 let mut event = Annotated::new(Event {
1011 debug_meta: Annotated::new(DebugMeta {
1012 images: Annotated::new(vec![Annotated::new(DebugImage::Other(Object::default()))]),
1013 ..DebugMeta::default()
1014 }),
1015 ..Event::default()
1016 });
1017
1018 normalize_event(&mut event, &NormalizationConfig::default());
1019
1020 assert_eq!(
1021 get_path!(event.debug_meta!),
1022 &Annotated::new(DebugMeta {
1023 images: Annotated::new(vec![Annotated::from_error(
1024 Error::invalid("unsupported debug image type"),
1025 Some(Value::Object(Object::default())),
1026 )]),
1027 ..DebugMeta::default()
1028 })
1029 );
1030 }
1031
1032 #[test]
1033 fn test_context_line_default() {
1034 let mut frame = Annotated::new(Frame {
1035 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1036 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1037 ..Frame::default()
1038 });
1039
1040 normalize_non_raw_frame(&mut frame);
1041
1042 let frame = frame.value().unwrap();
1043 assert_eq!(frame.context_line.as_str(), Some(""));
1044 }
1045
1046 #[test]
1047 fn test_context_line_retain() {
1048 let mut frame = Annotated::new(Frame {
1049 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1050 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1051 context_line: Annotated::new("some line".to_owned()),
1052 ..Frame::default()
1053 });
1054
1055 normalize_non_raw_frame(&mut frame);
1056
1057 let frame = frame.value().unwrap();
1058 assert_eq!(frame.context_line.as_str(), Some("some line"));
1059 }
1060
1061 #[test]
1062 fn test_frame_null_context_lines() {
1063 let mut frame = Annotated::new(Frame {
1064 pre_context: Annotated::new(vec![Annotated::default(), Annotated::new("".to_owned())]),
1065 post_context: Annotated::new(vec![Annotated::new("".to_owned()), Annotated::default()]),
1066 ..Frame::default()
1067 });
1068
1069 normalize_non_raw_frame(&mut frame);
1070
1071 assert_eq!(
1072 *get_value!(frame.pre_context!),
1073 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1074 );
1075 assert_eq!(
1076 *get_value!(frame.post_context!),
1077 vec![Annotated::new("".to_owned()), Annotated::new("".to_owned())],
1078 );
1079 }
1080
1081 #[test]
1082 fn test_too_long_tags() {
1083 let mut event = Annotated::new(Event {
1084 tags: Annotated::new(Tags(PairList(
1085 vec![Annotated::new(TagEntry(
1086 Annotated::new("foobar".to_owned()),
1087 Annotated::new("...........................................................................................................................................................................................................".to_owned()),
1088 )), Annotated::new(TagEntry(
1089 Annotated::new("foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo".to_owned()),
1090 Annotated::new("bar".to_owned()),
1091 ))]),
1092 )),
1093 ..Event::default()
1094 });
1095
1096 normalize_event(&mut event, &NormalizationConfig::default());
1097
1098 assert_eq!(
1099 get_value!(event.tags!),
1100 &Tags(PairList(vec![
1101 Annotated::new(TagEntry(
1102 Annotated::new("foobar".to_owned()),
1103 Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None),
1104 )),
1105 Annotated::new(TagEntry(
1106 Annotated::from_error(Error::new(ErrorKind::ValueTooLong), None),
1107 Annotated::new("bar".to_owned()),
1108 )),
1109 ]))
1110 );
1111 }
1112
1113 #[test]
1114 fn test_too_long_distribution() {
1115 let json = r#"{
1116 "event_id": "52df9022835246eeb317dbd739ccd059",
1117 "fingerprint": [
1118 "{{ default }}"
1119 ],
1120 "platform": "other",
1121 "dist": "52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059"
1122}"#;
1123
1124 let mut event = Annotated::<Event>::from_json(json).unwrap();
1125
1126 normalize_event(&mut event, &NormalizationConfig::default());
1127
1128 let dist = &event.value().unwrap().dist;
1129 let result = &Annotated::<String>::from_error(
1130 Error::new(ErrorKind::ValueTooLong),
1131 Some(Value::String("52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059-52df9022835246eeb317dbd739ccd059".to_owned()))
1132 );
1133 assert_eq!(dist, result);
1134 }
1135
1136 #[test]
1137 fn test_regression_backfills_abs_path_even_when_moving_stacktrace() {
1138 let mut event = Annotated::new(Event {
1139 exceptions: Annotated::new(Values::new(vec![Annotated::new(Exception {
1140 ty: Annotated::new("FooDivisionError".to_owned()),
1141 value: Annotated::new("hi".to_owned().into()),
1142 ..Exception::default()
1143 })])),
1144 stacktrace: Annotated::new(
1145 RawStacktrace {
1146 frames: Annotated::new(vec![Annotated::new(Frame {
1147 module: Annotated::new("MyModule".to_owned()),
1148 filename: Annotated::new("MyFilename".into()),
1149 function: Annotated::new("Void FooBar()".to_owned()),
1150 ..Frame::default()
1151 })]),
1152 ..RawStacktrace::default()
1153 }
1154 .into(),
1155 ),
1156 ..Event::default()
1157 });
1158
1159 normalize_event(&mut event, &NormalizationConfig::default());
1160
1161 assert_eq!(
1162 get_value!(event.exceptions.values[0].stacktrace!),
1163 &Stacktrace(RawStacktrace {
1164 frames: Annotated::new(vec![Annotated::new(Frame {
1165 module: Annotated::new("MyModule".to_owned()),
1166 filename: Annotated::new("MyFilename".into()),
1167 abs_path: Annotated::new("MyFilename".into()),
1168 function: Annotated::new("Void FooBar()".to_owned()),
1169 ..Frame::default()
1170 })]),
1171 ..RawStacktrace::default()
1172 })
1173 );
1174 }
1175
1176 #[test]
1177 fn test_parses_sdk_info_from_header() {
1178 let mut event = Annotated::new(Event::default());
1179
1180 normalize_event(
1181 &mut event,
1182 &NormalizationConfig {
1183 client: Some("_fooBar/0.0.0".to_owned()),
1184 ..Default::default()
1185 },
1186 );
1187
1188 assert_eq!(
1189 get_path!(event.client_sdk!),
1190 &Annotated::new(ClientSdkInfo {
1191 name: Annotated::new("_fooBar".to_owned()),
1192 version: Annotated::new("0.0.0".to_owned()),
1193 ..ClientSdkInfo::default()
1194 })
1195 );
1196 }
1197
1198 #[test]
1199 fn test_discards_received() {
1200 let mut event = Annotated::new(Event {
1201 received: FromValue::from_value(Annotated::new(Value::U64(696_969_696_969))),
1202 ..Default::default()
1203 });
1204
1205 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1206 normalize_event(&mut event, &NormalizationConfig::default());
1207
1208 assert_eq!(get_value!(event.received!), get_value!(event.timestamp!));
1209 }
1210
1211 #[test]
1212 fn test_grouping_config() {
1213 let mut event = Annotated::new(Event {
1214 logentry: Annotated::from(LogEntry {
1215 message: Annotated::new("Hello World!".to_owned().into()),
1216 ..Default::default()
1217 }),
1218 ..Default::default()
1219 });
1220
1221 validate_event(&mut event, &EventValidationConfig::default()).unwrap();
1222 normalize_event(
1223 &mut event,
1224 &NormalizationConfig {
1225 grouping_config: Some(json!({
1226 "id": "legacy:1234-12-12".to_owned(),
1227 })),
1228 ..Default::default()
1229 },
1230 );
1231
1232 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1233 ".event_id" => "[event-id]",
1234 ".received" => "[received]",
1235 ".timestamp" => "[timestamp]"
1236 }, @r###"
1237 {
1238 "event_id": "[event-id]",
1239 "level": "error",
1240 "type": "default",
1241 "logentry": {
1242 "formatted": "Hello World!",
1243 },
1244 "logger": "",
1245 "platform": "other",
1246 "timestamp": "[timestamp]",
1247 "received": "[received]",
1248 "grouping_config": {
1249 "id": "legacy:1234-12-12",
1250 },
1251 }
1252 "###);
1253 }
1254
1255 #[test]
1256 fn test_logentry_error() {
1257 let json = r#"
1258{
1259 "event_id": "74ad1301f4df489ead37d757295442b1",
1260 "timestamp": 1668148328.308933,
1261 "received": 1668148328.308933,
1262 "level": "error",
1263 "platform": "python",
1264 "logentry": {
1265 "params": [
1266 "bogus"
1267 ],
1268 "formatted": 42
1269 }
1270}
1271"#;
1272 let mut event = Annotated::<Event>::from_json(json).unwrap();
1273
1274 normalize_event(&mut event, &NormalizationConfig::default());
1275
1276 assert_json_snapshot!(SerializableAnnotated(&event), {".received" => "[received]"}, @r###"
1277 {
1278 "event_id": "74ad1301f4df489ead37d757295442b1",
1279 "level": "error",
1280 "type": "default",
1281 "logentry": null,
1282 "logger": "",
1283 "platform": "python",
1284 "timestamp": 1668148328.308933,
1285 "received": "[received]",
1286 "_meta": {
1287 "logentry": {
1288 "": {
1289 "err": [
1290 [
1291 "invalid_data",
1292 {
1293 "reason": "no message present"
1294 }
1295 ]
1296 ],
1297 "val": {
1298 "formatted": null,
1299 "message": null,
1300 "params": [
1301 "bogus"
1302 ]
1303 }
1304 }
1305 }
1306 }
1307 }
1308 "###)
1309 }
1310
1311 #[test]
1312 fn test_future_timestamp() {
1313 let mut event = Annotated::new(Event {
1314 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 2, 0).unwrap().into()),
1315 ..Default::default()
1316 });
1317
1318 let received_at = Some(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap());
1319 let max_secs_in_past = Some(30 * 24 * 3600);
1320 let max_secs_in_future = Some(60);
1321
1322 validate_event(
1323 &mut event,
1324 &EventValidationConfig {
1325 received_at,
1326 max_secs_in_past,
1327 max_secs_in_future,
1328 is_validated: false,
1329 ..Default::default()
1330 },
1331 )
1332 .unwrap();
1333 normalize_event(&mut event, &NormalizationConfig::default());
1334
1335 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1336 ".event_id" => "[event-id]",
1337 }, @r###"
1338 {
1339 "event_id": "[event-id]",
1340 "level": "error",
1341 "type": "default",
1342 "logger": "",
1343 "platform": "other",
1344 "timestamp": 946857600.0,
1345 "received": 946857600.0,
1346 "_meta": {
1347 "timestamp": {
1348 "": Meta(Some(MetaInner(
1349 err: [
1350 [
1351 "future_timestamp",
1352 {
1353 "sdk_time": "2000-01-03T00:02:00+00:00",
1354 "server_time": "2000-01-03T00:00:00+00:00",
1355 },
1356 ],
1357 ],
1358 ))),
1359 },
1360 },
1361 }
1362 "###);
1363 }
1364
1365 #[test]
1366 fn test_past_timestamp() {
1367 let mut event = Annotated::new(Event {
1368 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 3, 0, 0, 0).unwrap().into()),
1369 ..Default::default()
1370 });
1371
1372 let received_at = Some(Utc.with_ymd_and_hms(2000, 3, 3, 0, 0, 0).unwrap());
1373 let max_secs_in_past = Some(30 * 24 * 3600);
1374 let max_secs_in_future = Some(60);
1375
1376 validate_event(
1377 &mut event,
1378 &EventValidationConfig {
1379 received_at,
1380 max_secs_in_past,
1381 max_secs_in_future,
1382 is_validated: false,
1383 ..Default::default()
1384 },
1385 )
1386 .unwrap();
1387 normalize_event(&mut event, &NormalizationConfig::default());
1388
1389 insta::assert_ron_snapshot!(SerializableAnnotated(&event), {
1390 ".event_id" => "[event-id]",
1391 }, @r###"
1392 {
1393 "event_id": "[event-id]",
1394 "level": "error",
1395 "type": "default",
1396 "logger": "",
1397 "platform": "other",
1398 "timestamp": 952041600.0,
1399 "received": 952041600.0,
1400 "_meta": {
1401 "timestamp": {
1402 "": Meta(Some(MetaInner(
1403 err: [
1404 [
1405 "past_timestamp",
1406 {
1407 "sdk_time": "2000-01-03T00:00:00+00:00",
1408 "server_time": "2000-03-03T00:00:00+00:00",
1409 },
1410 ],
1411 ],
1412 ))),
1413 },
1414 },
1415 }
1416 "###);
1417 }
1418
1419 #[test]
1420 fn test_normalize_logger_empty() {
1421 let mut event = Event::from_value(
1422 serde_json::json!({
1423 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1424 "platform": "java",
1425 "logger": "",
1426 })
1427 .into(),
1428 );
1429
1430 normalize_event(&mut event, &NormalizationConfig::default());
1431 assert_annotated_snapshot!(event);
1432 }
1433
1434 #[test]
1435 fn test_normalize_logger_trimmed() {
1436 let mut event = Event::from_value(
1437 serde_json::json!({
1438 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1439 "platform": "java",
1440 "logger": " \t \t ",
1441 })
1442 .into(),
1443 );
1444
1445 normalize_event(&mut event, &NormalizationConfig::default());
1446 assert_annotated_snapshot!(event);
1447 }
1448
1449 #[test]
1450 fn test_normalize_logger_short_no_trimming() {
1451 let mut event = Event::from_value(
1452 serde_json::json!({
1453 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1454 "platform": "java",
1455 "logger": "my.short-logger.isnt_trimmed",
1456 })
1457 .into(),
1458 );
1459
1460 normalize_event(&mut event, &NormalizationConfig::default());
1461 assert_annotated_snapshot!(event);
1462 }
1463
1464 #[test]
1465 fn test_normalize_logger_exact_length() {
1466 let mut event = Event::from_value(
1467 serde_json::json!({
1468 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1469 "platform": "java",
1470 "logger": "this_is-exactly-the_max_len.012345678901234567890123456789012345",
1471 })
1472 .into(),
1473 );
1474
1475 normalize_event(&mut event, &NormalizationConfig::default());
1476 assert_annotated_snapshot!(event);
1477 }
1478
1479 #[test]
1480 fn test_normalize_logger_too_long_single_word() {
1481 let mut event = Event::from_value(
1482 serde_json::json!({
1483 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1484 "platform": "java",
1485 "logger": "this_is-way_too_long-and_we_only_have_one_word-so_we_cant_smart_trim",
1486 })
1487 .into(),
1488 );
1489
1490 normalize_event(&mut event, &NormalizationConfig::default());
1491 assert_annotated_snapshot!(event);
1492 }
1493
1494 #[test]
1495 fn test_normalize_logger_word_trimmed_at_max() {
1496 let mut event = Event::from_value(
1497 serde_json::json!({
1498 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1499 "platform": "java",
1500 "logger": "already_out.out.in.this_part-is-kept.this_right_here-is_an-extremely_long_word",
1501 })
1502 .into(),
1503 );
1504
1505 normalize_event(&mut event, &NormalizationConfig::default());
1506 assert_annotated_snapshot!(event);
1507 }
1508
1509 #[test]
1510 fn test_normalize_logger_word_trimmed_before_max() {
1511 let mut event = Event::from_value(
1514 serde_json::json!({
1515 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1516 "platform": "java",
1517 "logger": "super_out.this_is_already_out_too.this_part-is-kept.this_right_here-is_a-very_long_word",
1518 })
1519 .into(),
1520 );
1521
1522 normalize_event(&mut event, &NormalizationConfig::default());
1523 assert_annotated_snapshot!(event);
1524 }
1525
1526 #[test]
1527 fn test_normalize_logger_word_leading_dots() {
1528 let mut event = Event::from_value(
1529 serde_json::json!({
1530 "event_id": "7637af36578e4e4592692e28a1d6e2ca",
1531 "platform": "java",
1532 "logger": "io.this-tests-the-smart-trimming-and-word-removal-around-dot.words",
1533 })
1534 .into(),
1535 );
1536
1537 normalize_event(&mut event, &NormalizationConfig::default());
1538 assert_annotated_snapshot!(event);
1539 }
1540
1541 #[test]
1542 fn test_normalization_is_idempotent() {
1543 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1545 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1546 let mut event = Annotated::new(Event {
1547 ty: Annotated::new(EventType::Transaction),
1548 transaction: Annotated::new("/".to_owned()),
1549 timestamp: Annotated::new(end.into()),
1550 start_timestamp: Annotated::new(start.into()),
1551 contexts: {
1552 let mut contexts = Contexts::new();
1553 contexts.add(TraceContext {
1554 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1555 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1556 op: Annotated::new("http.server".to_owned()),
1557 ..Default::default()
1558 });
1559 Annotated::new(contexts)
1560 },
1561 spans: Annotated::new(vec![Annotated::new(Span {
1562 timestamp: Annotated::new(
1563 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1564 ),
1565 start_timestamp: Annotated::new(
1566 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1567 ),
1568 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1569 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1570
1571 ..Default::default()
1572 })]),
1573 ..Default::default()
1574 });
1575
1576 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1577 relay_event_schema::processor::apply(event, |e, _m| {
1578 e.received = Annotated::empty();
1579 Ok(())
1580 })
1581 .unwrap();
1582 event
1583 }
1584
1585 normalize_event(&mut event, &NormalizationConfig::default());
1586 let first = remove_received_from_event(&mut event.clone())
1587 .to_json()
1588 .unwrap();
1589 normalize_event(&mut event, &NormalizationConfig::default());
1592 let second = remove_received_from_event(&mut event.clone())
1593 .to_json()
1594 .unwrap();
1595 assert_eq!(&first, &second, "idempotency check failed");
1596
1597 normalize_event(&mut event, &NormalizationConfig::default());
1598 let third = remove_received_from_event(&mut event.clone())
1599 .to_json()
1600 .unwrap();
1601 assert_eq!(&second, &third, "idempotency check failed");
1602 }
1603
1604 #[test]
1611 fn test_full_normalization_is_idempotent() {
1612 let start = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
1613 let end = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap();
1614 let mut event = Annotated::new(Event {
1615 ty: Annotated::new(EventType::Transaction),
1616 transaction: Annotated::new("/".to_owned()),
1617 timestamp: Annotated::new(end.into()),
1618 start_timestamp: Annotated::new(start.into()),
1619 contexts: {
1620 let mut contexts = Contexts::new();
1621 contexts.add(TraceContext {
1622 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1623 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1624 op: Annotated::new("http.server".to_owned()),
1625 ..Default::default()
1626 });
1627 Annotated::new(contexts)
1628 },
1629 spans: Annotated::new(vec![Annotated::new(Span {
1630 timestamp: Annotated::new(
1631 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 10).unwrap().into(),
1632 ),
1633 start_timestamp: Annotated::new(
1634 Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into(),
1635 ),
1636 trace_id: Annotated::new("4c79f60c11214eb38604f4ae0781bfb2".parse().unwrap()),
1637 span_id: Annotated::new("fa90fdead5f74053".parse().unwrap()),
1638
1639 ..Default::default()
1640 })]),
1641 ..Default::default()
1642 });
1643
1644 fn remove_received_from_event(event: &mut Annotated<Event>) -> &mut Annotated<Event> {
1645 relay_event_schema::processor::apply(event, |e, _m| {
1646 e.received = Annotated::empty();
1647 Ok(())
1648 })
1649 .unwrap();
1650 event
1651 }
1652
1653 let full_normalization_config = NormalizationConfig {
1654 is_renormalize: false,
1655 enable_trimming: true,
1656 remove_other: true,
1657 emit_event_errors: true,
1658 ..Default::default()
1659 };
1660
1661 normalize_event(&mut event, &full_normalization_config);
1662 let first = remove_received_from_event(&mut event.clone())
1663 .to_json()
1664 .unwrap();
1665 normalize_event(&mut event, &full_normalization_config);
1668 let second = remove_received_from_event(&mut event.clone())
1669 .to_json()
1670 .unwrap();
1671 assert_eq!(&first, &second, "idempotency check failed");
1672
1673 normalize_event(&mut event, &full_normalization_config);
1674 let third = remove_received_from_event(&mut event.clone())
1675 .to_json()
1676 .unwrap();
1677 assert_eq!(&second, &third, "idempotency check failed");
1678 }
1679
1680 #[test]
1681 fn test_normalize_validates_spans() {
1682 let event = Annotated::<Event>::from_json(
1683 r#"
1684 {
1685 "type": "transaction",
1686 "transaction": "/",
1687 "timestamp": 946684810.0,
1688 "start_timestamp": 946684800.0,
1689 "contexts": {
1690 "trace": {
1691 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1692 "span_id": "fa90fdead5f74053",
1693 "op": "http.server",
1694 "type": "trace"
1695 }
1696 },
1697 "spans": []
1698 }
1699 "#,
1700 )
1701 .unwrap();
1702
1703 for span in [
1705 r#"null"#,
1706 r#"{
1707 "timestamp": 946684810.0,
1708 "start_timestamp": 946684900.0,
1709 "span_id": "fa90fdead5f74053",
1710 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1711 }"#,
1712 r#"{
1713 "timestamp": 946684810.0,
1714 "span_id": "fa90fdead5f74053",
1715 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1716 }"#,
1717 r#"{
1718 "timestamp": 946684810.0,
1719 "start_timestamp": 946684800.0,
1720 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2"
1721 }"#,
1722 r#"{
1723 "timestamp": 946684810.0,
1724 "start_timestamp": 946684800.0,
1725 "span_id": "fa90fdead5f74053"
1726 }"#,
1727 ] {
1728 let mut modified_event = event.clone();
1729 let event_ref = modified_event.value_mut().as_mut().unwrap();
1730 event_ref
1731 .spans
1732 .set_value(Some(vec![Annotated::<Span>::from_json(span).unwrap()]));
1733
1734 let res = validate_event(&mut modified_event, &EventValidationConfig::default());
1735
1736 assert!(res.is_err(), "{span:?}");
1737 }
1738 }
1739
1740 #[test]
1741 fn test_normalization_respects_is_renormalize() {
1742 let mut event = Annotated::<Event>::from_json(
1743 r#"
1744 {
1745 "type": "default",
1746 "tags": [["environment", "some_environment"]]
1747 }
1748 "#,
1749 )
1750 .unwrap();
1751
1752 normalize_event(
1753 &mut event,
1754 &NormalizationConfig {
1755 is_renormalize: true,
1756 ..Default::default()
1757 },
1758 );
1759
1760 assert_debug_snapshot!(event.value().unwrap().tags, @r###"
1761 Tags(
1762 PairList(
1763 [
1764 TagEntry(
1765 "environment",
1766 "some_environment",
1767 ),
1768 ],
1769 ),
1770 )
1771 "###);
1772 }
1773
1774 #[test]
1775 fn test_geo_in_normalize() {
1776 let mut event = Annotated::<Event>::from_json(
1777 r#"
1778 {
1779 "type": "transaction",
1780 "transaction": "/foo/",
1781 "timestamp": 946684810.0,
1782 "start_timestamp": 946684800.0,
1783 "contexts": {
1784 "trace": {
1785 "trace_id": "4c79f60c11214eb38604f4ae0781bfb2",
1786 "span_id": "fa90fdead5f74053",
1787 "op": "http.server",
1788 "type": "trace"
1789 }
1790 },
1791 "transaction_info": {
1792 "source": "url"
1793 },
1794 "user": {
1795 "ip_address": "2.125.160.216"
1796 }
1797 }
1798 "#,
1799 )
1800 .unwrap();
1801
1802 let lookup = GeoIpLookup::open("tests/fixtures/GeoIP2-Enterprise-Test.mmdb").unwrap();
1803
1804 let user_geo = event.value().unwrap().user.value().unwrap().geo.value();
1806 assert!(user_geo.is_none());
1807
1808 normalize_event(
1809 &mut event,
1810 &NormalizationConfig {
1811 geoip_lookup: Some(&lookup),
1812 ..Default::default()
1813 },
1814 );
1815
1816 let user_geo = event
1818 .value()
1819 .unwrap()
1820 .user
1821 .value()
1822 .unwrap()
1823 .geo
1824 .value()
1825 .unwrap();
1826
1827 assert_eq!(user_geo.country_code.value().unwrap(), "GB");
1828 assert_eq!(user_geo.city.value().unwrap(), "Boxford");
1829 }
1830
1831 #[test]
1832 fn test_normalize_app_start_spans_only_for_react_native_3_to_4_4() {
1833 let mut event = Event {
1834 spans: Annotated::new(vec![Annotated::new(Span {
1835 op: Annotated::new("app_start_cold".to_owned()),
1836 ..Default::default()
1837 })]),
1838 client_sdk: Annotated::new(ClientSdkInfo {
1839 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1840 version: Annotated::new("4.5.0".to_owned()),
1841 ..Default::default()
1842 }),
1843 ..Default::default()
1844 };
1845 normalize_app_start_spans(&mut event);
1846 assert_debug_snapshot!(event.spans, @r###"
1847 [
1848 Span {
1849 timestamp: ~,
1850 start_timestamp: ~,
1851 exclusive_time: ~,
1852 op: "app_start_cold",
1853 span_id: ~,
1854 parent_span_id: ~,
1855 trace_id: ~,
1856 segment_id: ~,
1857 is_segment: ~,
1858 is_remote: ~,
1859 status: ~,
1860 description: ~,
1861 tags: ~,
1862 origin: ~,
1863 profile_id: ~,
1864 data: ~,
1865 links: ~,
1866 sentry_tags: ~,
1867 received: ~,
1868 measurements: ~,
1869 platform: ~,
1870 was_transaction: ~,
1871 kind: ~,
1872 _performance_issues_spans: ~,
1873 other: {},
1874 },
1875 ]
1876 "###);
1877 }
1878
1879 #[test]
1880 fn test_normalize_app_start_cold_spans_for_react_native() {
1881 let mut event = Event {
1882 spans: Annotated::new(vec![Annotated::new(Span {
1883 op: Annotated::new("app_start_cold".to_owned()),
1884 ..Default::default()
1885 })]),
1886 client_sdk: Annotated::new(ClientSdkInfo {
1887 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1888 version: Annotated::new("4.4.0".to_owned()),
1889 ..Default::default()
1890 }),
1891 ..Default::default()
1892 };
1893 normalize_app_start_spans(&mut event);
1894 assert_debug_snapshot!(event.spans, @r###"
1895 [
1896 Span {
1897 timestamp: ~,
1898 start_timestamp: ~,
1899 exclusive_time: ~,
1900 op: "app.start.cold",
1901 span_id: ~,
1902 parent_span_id: ~,
1903 trace_id: ~,
1904 segment_id: ~,
1905 is_segment: ~,
1906 is_remote: ~,
1907 status: ~,
1908 description: ~,
1909 tags: ~,
1910 origin: ~,
1911 profile_id: ~,
1912 data: ~,
1913 links: ~,
1914 sentry_tags: ~,
1915 received: ~,
1916 measurements: ~,
1917 platform: ~,
1918 was_transaction: ~,
1919 kind: ~,
1920 _performance_issues_spans: ~,
1921 other: {},
1922 },
1923 ]
1924 "###);
1925 }
1926
1927 #[test]
1928 fn test_normalize_app_start_warm_spans_for_react_native() {
1929 let mut event = Event {
1930 spans: Annotated::new(vec![Annotated::new(Span {
1931 op: Annotated::new("app_start_warm".to_owned()),
1932 ..Default::default()
1933 })]),
1934 client_sdk: Annotated::new(ClientSdkInfo {
1935 name: Annotated::new("sentry.javascript.react-native".to_owned()),
1936 version: Annotated::new("4.4.0".to_owned()),
1937 ..Default::default()
1938 }),
1939 ..Default::default()
1940 };
1941 normalize_app_start_spans(&mut event);
1942 assert_debug_snapshot!(event.spans, @r###"
1943 [
1944 Span {
1945 timestamp: ~,
1946 start_timestamp: ~,
1947 exclusive_time: ~,
1948 op: "app.start.warm",
1949 span_id: ~,
1950 parent_span_id: ~,
1951 trace_id: ~,
1952 segment_id: ~,
1953 is_segment: ~,
1954 is_remote: ~,
1955 status: ~,
1956 description: ~,
1957 tags: ~,
1958 origin: ~,
1959 profile_id: ~,
1960 data: ~,
1961 links: ~,
1962 sentry_tags: ~,
1963 received: ~,
1964 measurements: ~,
1965 platform: ~,
1966 was_transaction: ~,
1967 kind: ~,
1968 _performance_issues_spans: ~,
1969 other: {},
1970 },
1971 ]
1972 "###);
1973 }
1974}