1use std::fmt;
2use std::str::FromStr;
3
4use relay_common::time;
5use relay_protocol::{
6 Annotated, Array, Empty, FromValue, Getter, GetterIter, IntoValue, Object, Val, Value,
7};
8use sentry_release_parser::Release as ParsedRelease;
9use uuid::Uuid;
10
11use crate::processor::ProcessValue;
12use crate::protocol::{
13 AppContext, Breadcrumb, Breakdowns, BrowserContext, ClientSdkInfo, Contexts, Csp, DebugMeta,
14 DefaultContext, DeviceContext, EventType, Exception, ExpectCt, ExpectStaple, Fingerprint,
15 GpuContext, Hpkp, LenientString, Level, LogEntry, Measurements, Metrics, MonitorContext,
16 OsContext, ProfileContext, RelayInfo, Request, ResponseContext, RuntimeContext, Span, SpanId,
17 Stacktrace, Tags, TemplateInfo, Thread, Timestamp, TraceContext, TransactionInfo, User, Values,
18};
19
20#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct EventId(pub Uuid);
23
24impl EventId {
25 #[inline]
27 pub fn new() -> Self {
28 Self(Uuid::new_v4())
29 }
30
31 #[inline]
33 pub fn is_nil(&self) -> bool {
34 self.0.is_nil()
35 }
36}
37
38impl Default for EventId {
39 #[inline]
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45relay_protocol::derive_string_meta_structure!(EventId, "event id");
46
47impl ProcessValue for EventId {}
48
49impl fmt::Display for EventId {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}", self.0.as_simple())
52 }
53}
54
55impl FromStr for EventId {
56 type Err = <Uuid as FromStr>::Err;
57
58 fn from_str(uuid_str: &str) -> Result<Self, Self::Err> {
59 uuid_str.parse().map(EventId)
60 }
61}
62
63relay_common::impl_str_serde!(EventId, "an event identifier");
64
65impl TryFrom<&SpanId> for EventId {
66 type Error = <EventId as FromStr>::Err;
67
68 fn try_from(value: &SpanId) -> Result<Self, Self::Error> {
69 let s = format!("0000000000000000{value}");
71 s.parse()
72 }
73}
74
75#[derive(Debug, FromValue, IntoValue, ProcessValue, Empty, Clone, PartialEq)]
76pub struct ExtraValue(#[metastructure(max_depth = 7, max_bytes = 16_384)] pub Value);
77
78impl<T: Into<Value>> From<T> for ExtraValue {
79 fn from(value: T) -> ExtraValue {
80 ExtraValue(value.into())
81 }
82}
83
84#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
86pub struct EventProcessingError {
87 #[metastructure(field = "type", required = true)]
89 pub ty: Annotated<String>,
90
91 pub name: Annotated<String>,
93
94 pub value: Annotated<Value>,
96
97 #[metastructure(additional_properties, pii = "maybe")]
99 pub other: Object<Value>,
100}
101
102#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
107pub struct GroupingConfig {
108 #[metastructure(max_chars = 128)]
110 pub id: Annotated<String>,
111 pub enhancements: Annotated<String>,
113}
114
115#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
117#[metastructure(process_func = "process_event", value_type = "Event")]
118pub struct Event {
119 #[metastructure(field = "event_id")]
137 pub id: Annotated<EventId>,
138
139 pub level: Annotated<Level>,
147
148 pub version: Annotated<String>,
150
151 #[metastructure(field = "type")]
176 pub ty: Annotated<EventType>,
177
178 #[metastructure(skip_serialization = "empty")]
189 pub fingerprint: Annotated<Fingerprint>,
190
191 #[metastructure(max_chars = 200, pii = "maybe")]
195 pub culprit: Annotated<String>,
196
197 #[metastructure(max_chars = 200, trim_whitespace = true)]
202 pub transaction: Annotated<String>,
203
204 #[metastructure(skip_serialization = "null")]
206 pub transaction_info: Annotated<TransactionInfo>,
207
208 pub time_spent: Annotated<u64>,
210
211 #[metastructure(legacy_alias = "sentry.interfaces.Message", legacy_alias = "message")]
213 #[metastructure(skip_serialization = "empty")]
214 pub logentry: Annotated<LogEntry>,
215
216 #[metastructure(
218 max_chars = 64, deny_chars = "\r\n",
220 )]
221 pub logger: Annotated<String>,
222
223 #[metastructure(skip_serialization = "empty_deep", max_depth = 7, max_bytes = 8192)]
237 pub modules: Annotated<Object<String>>,
238
239 pub platform: Annotated<String>,
249
250 pub timestamp: Annotated<Timestamp>,
276
277 #[metastructure(omit_from_schema)] pub start_timestamp: Annotated<Timestamp>,
280
281 pub received: Annotated<Timestamp>,
283
284 #[metastructure(pii = "true", max_chars = 256, max_chars_allowance = 20)]
288 pub server_name: Annotated<String>,
289
290 #[metastructure(
295 max_chars = 200, required = false,
298 trim_whitespace = true,
299 nonempty = true,
300 skip_serialization = "empty"
301 )]
302 pub release: Annotated<LenientString>,
303
304 #[metastructure(
312 allow_chars = "a-zA-Z0-9_.-",
313 trim_whitespace = true,
314 required = false,
315 nonempty = true
316 )]
317 pub dist: Annotated<String>,
318
319 #[metastructure(
325 max_chars = 64,
326 nonempty = true,
328 required = false,
329 trim_whitespace = true
330 )]
331 pub environment: Annotated<String>,
332
333 #[metastructure(max_chars = 256, max_chars_allowance = 20)]
335 #[metastructure(omit_from_schema)] pub site: Annotated<String>,
337
338 #[metastructure(legacy_alias = "sentry.interfaces.User")]
340 #[metastructure(skip_serialization = "empty")]
341 pub user: Annotated<User>,
342
343 #[metastructure(legacy_alias = "sentry.interfaces.Http")]
345 #[metastructure(skip_serialization = "empty")]
346 pub request: Annotated<Request>,
347
348 #[metastructure(legacy_alias = "sentry.interfaces.Contexts")]
350 pub contexts: Annotated<Contexts>,
351
352 #[metastructure(legacy_alias = "sentry.interfaces.Breadcrumbs")]
354 #[metastructure(skip_serialization = "empty")]
355 pub breadcrumbs: Annotated<Values<Breadcrumb>>,
356
357 #[metastructure(legacy_alias = "sentry.interfaces.Exception")]
359 #[metastructure(field = "exception")]
360 #[metastructure(skip_serialization = "empty")]
361 pub exceptions: Annotated<Values<Exception>>,
362
363 #[metastructure(skip_serialization = "empty")]
367 #[metastructure(legacy_alias = "sentry.interfaces.Stacktrace")]
368 pub stacktrace: Annotated<Stacktrace>,
369
370 #[metastructure(legacy_alias = "sentry.interfaces.Template")]
374 #[metastructure(omit_from_schema)]
375 pub template: Annotated<TemplateInfo>,
376
377 #[metastructure(skip_serialization = "empty")]
379 pub threads: Annotated<Values<Thread>>,
380
381 #[metastructure(skip_serialization = "empty", pii = "maybe")]
385 pub tags: Annotated<Tags>,
386
387 #[metastructure(max_depth = 7, max_bytes = 262_144)]
397 #[metastructure(pii = "true", skip_serialization = "empty")]
398 pub extra: Annotated<Object<ExtraValue>>,
399
400 #[metastructure(skip_serialization = "empty")]
402 pub debug_meta: Annotated<DebugMeta>,
403
404 #[metastructure(field = "sdk")]
406 #[metastructure(skip_serialization = "empty")]
407 pub client_sdk: Annotated<ClientSdkInfo>,
408
409 #[metastructure(max_depth = 5, max_bytes = 2048)]
411 #[metastructure(skip_serialization = "empty", omit_from_schema)]
412 pub ingest_path: Annotated<Array<RelayInfo>>,
413
414 #[metastructure(skip_serialization = "empty_deep")]
417 pub errors: Annotated<Array<EventProcessingError>>,
418
419 #[metastructure(omit_from_schema)] pub key_id: Annotated<String>,
422
423 #[metastructure(omit_from_schema)] pub project: Annotated<u64>,
426
427 #[metastructure(omit_from_schema)] pub grouping_config: Annotated<Object<Value>>,
430
431 #[metastructure(max_chars = 128)]
433 #[metastructure(omit_from_schema)] pub checksum: Annotated<String>,
435
436 #[metastructure(legacy_alias = "sentry.interfaces.Csp")]
438 #[metastructure(omit_from_schema)] pub csp: Annotated<Csp>,
440
441 #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.Hpkp")]
443 #[metastructure(omit_from_schema)] pub hpkp: Annotated<Hpkp>,
445
446 #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectCT")]
448 #[metastructure(omit_from_schema)] pub expectct: Annotated<ExpectCt>,
450
451 #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectStaple")]
453 #[metastructure(omit_from_schema)] pub expectstaple: Annotated<ExpectStaple>,
455
456 #[metastructure(max_bytes = 819200)]
458 #[metastructure(omit_from_schema)] pub spans: Annotated<Array<Span>>,
460
461 #[metastructure(skip_serialization = "empty")]
466 #[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
468
469 #[metastructure(skip_serialization = "empty")]
471 #[metastructure(omit_from_schema)] pub breakdowns: Annotated<Breakdowns>,
473
474 #[metastructure(omit_from_schema)] pub scraping_attempts: Annotated<Value>,
478
479 #[metastructure(omit_from_schema)]
483 pub _metrics: Annotated<Metrics>,
484
485 #[metastructure(omit_from_schema)]
487 pub _dsc: Annotated<Value>,
488
489 #[metastructure(skip_serialization = "empty", trim = false)]
494 pub _performance_issues_spans: Annotated<bool>,
495
496 #[metastructure(additional_properties, pii = "true")]
498 pub other: Object<Value>,
499}
500
501impl Event {
502 pub fn tag_value(&self, tag_key: &str) -> Option<&str> {
507 if let Some(tags) = self.tags.value() {
508 tags.get(tag_key)
509 } else {
510 None
511 }
512 }
513
514 pub fn has_module(&self, module_name: &str) -> bool {
516 self.modules
517 .value()
518 .map(|m| m.contains_key(module_name))
519 .unwrap_or(false)
520 }
521
522 pub fn sdk_name(&self) -> &str {
526 if let Some(client_sdk) = self.client_sdk.value() {
527 if let Some(name) = client_sdk.name.as_str() {
528 return name;
529 }
530 }
531
532 "unknown"
533 }
534
535 pub fn sdk_version(&self) -> &str {
539 if let Some(client_sdk) = self.client_sdk.value() {
540 if let Some(version) = client_sdk.version.as_str() {
541 return version;
542 }
543 }
544
545 "unknown"
546 }
547
548 pub fn user_agent(&self) -> Option<&str> {
553 let headers = self.request.value()?.headers.value()?;
554
555 for item in headers.iter() {
556 if let Some((o_k, v)) = item.value() {
557 if let Some(k) = o_k.as_str() {
558 if k.eq_ignore_ascii_case("user-agent") {
559 return v.as_str();
560 }
561 }
562 }
563 }
564
565 None
566 }
567
568 pub fn extra_at(&self, path: &str) -> Option<&Value> {
573 let mut path = path.split('.');
574
575 let mut value = &self.extra.value()?.get(path.next()?)?.value()?.0;
577
578 for key in path {
580 if let Value::Object(object) = value {
581 value = object.get(key)?.value()?;
582 } else {
583 return None;
584 }
585 }
586
587 Some(value)
588 }
589
590 pub fn parse_release(&self) -> Option<ParsedRelease<'_>> {
592 sentry_release_parser::Release::parse(self.release.as_str()?).ok()
593 }
594
595 pub fn measurement(&self, name: &str) -> Option<f64> {
599 let annotated = self.measurements.value()?.get(name)?;
600 Some(*annotated.value()?.value.value()?)
601 }
602
603 pub fn breakdown(&self, breakdown: &str, measurement: &str) -> Option<f64> {
605 let breakdown = self.breakdowns.value()?.get(breakdown)?.value()?;
606 Some(*breakdown.get(measurement)?.value()?.value.value()?)
607 }
608
609 pub fn context<C: DefaultContext>(&self) -> Option<&C> {
611 self.contexts.value()?.get()
612 }
613
614 pub fn context_mut<C: DefaultContext>(&mut self) -> Option<&mut C> {
616 self.contexts.value_mut().as_mut()?.get_mut()
617 }
618}
619
620fn or_none(string: &Annotated<impl AsRef<str>>) -> Option<&str> {
621 match string.as_str() {
622 None | Some("") => None,
623 Some(other) => Some(other),
624 }
625}
626
627impl Getter for Event {
628 fn get_value(&self, path: &str) -> Option<Val<'_>> {
629 Some(match path.strip_prefix("event.")? {
630 "level" => self.level.value()?.name().into(),
632 "release" => self.release.as_str()?.into(),
633 "dist" => self.dist.as_str()?.into(),
634 "environment" => self.environment.as_str()?.into(),
635 "transaction" => self.transaction.as_str()?.into(),
636 "logger" => self.logger.as_str()?.into(),
637 "platform" => self.platform.as_str().unwrap_or("other").into(),
638
639 "logentry.formatted" => self.logentry.value()?.formatted.value()?.as_ref().into(),
641 "logentry.message" => self.logentry.value()?.message.value()?.as_ref().into(),
642 "user.email" => or_none(&self.user.value()?.email)?.into(),
643 "user.id" => or_none(&self.user.value()?.id)?.into(),
644 "user.ip_address" => self.user.value()?.ip_address.as_str()?.into(),
645 "user.name" => self.user.value()?.name.as_str()?.into(),
646 "user.segment" => or_none(&self.user.value()?.segment)?.into(),
647 "user.geo.city" => self.user.value()?.geo.value()?.city.as_str()?.into(),
648 "user.geo.country_code" => self
649 .user
650 .value()?
651 .geo
652 .value()?
653 .country_code
654 .as_str()?
655 .into(),
656 "user.geo.region" => self.user.value()?.geo.value()?.region.as_str()?.into(),
657 "user.geo.subdivision" => self.user.value()?.geo.value()?.subdivision.as_str()?.into(),
658 "request.method" => self.request.value()?.method.as_str()?.into(),
659 "request.url" => self.request.value()?.url.as_str()?.into(),
660 "transaction.source" => self
661 .transaction_info
662 .value()?
663 .source
664 .value()?
665 .as_str()
666 .into(),
667 "sdk.name" => self.client_sdk.value()?.name.as_str()?.into(),
668 "sdk.version" => self.client_sdk.value()?.version.as_str()?.into(),
669
670 "sentry_user" => self.user.value()?.sentry_user.as_str()?.into(),
672
673 "contexts.app.in_foreground" => {
675 self.context::<AppContext>()?.in_foreground.value()?.into()
676 }
677 "contexts.app.device_app_hash" => self
678 .context::<AppContext>()?
679 .device_app_hash
680 .as_str()?
681 .into(),
682 "contexts.device.arch" => self.context::<DeviceContext>()?.arch.as_str()?.into(),
683 "contexts.device.battery_level" => self
684 .context::<DeviceContext>()?
685 .battery_level
686 .value()?
687 .into(),
688 "contexts.device.brand" => self.context::<DeviceContext>()?.brand.as_str()?.into(),
689 "contexts.device.charging" => self.context::<DeviceContext>()?.charging.value()?.into(),
690 "contexts.device.family" => self.context::<DeviceContext>()?.family.as_str()?.into(),
691 "contexts.device.model" => self.context::<DeviceContext>()?.model.as_str()?.into(),
692 "contexts.device.locale" => self.context::<DeviceContext>()?.locale.as_str()?.into(),
693 "contexts.device.online" => self.context::<DeviceContext>()?.online.value()?.into(),
694 "contexts.device.orientation" => self
695 .context::<DeviceContext>()?
696 .orientation
697 .as_str()?
698 .into(),
699 "contexts.device.name" => self.context::<DeviceContext>()?.name.as_str()?.into(),
700 "contexts.device.screen_density" => self
701 .context::<DeviceContext>()?
702 .screen_density
703 .value()?
704 .into(),
705 "contexts.device.screen_dpi" => {
706 self.context::<DeviceContext>()?.screen_dpi.value()?.into()
707 }
708 "contexts.device.screen_width_pixels" => self
709 .context::<DeviceContext>()?
710 .screen_width_pixels
711 .value()?
712 .into(),
713 "contexts.device.screen_height_pixels" => self
714 .context::<DeviceContext>()?
715 .screen_height_pixels
716 .value()?
717 .into(),
718 "contexts.device.simulator" => {
719 self.context::<DeviceContext>()?.simulator.value()?.into()
720 }
721 "contexts.gpu.vendor_name" => {
722 self.context::<GpuContext>()?.vendor_name.as_str()?.into()
723 }
724 "contexts.gpu.name" => self.context::<GpuContext>()?.name.as_str()?.into(),
725 "contexts.monitor.id" => self.context::<MonitorContext>()?.get("id")?.value()?.into(),
726 "contexts.monitor.slug" => self
727 .context::<MonitorContext>()?
728 .get("slug")?
729 .value()?
730 .into(),
731 "contexts.os" => self.context::<OsContext>()?.os.as_str()?.into(),
732 "contexts.os.build" => self.context::<OsContext>()?.build.as_str()?.into(),
733 "contexts.os.kernel_version" => {
734 self.context::<OsContext>()?.kernel_version.as_str()?.into()
735 }
736 "contexts.os.name" => self.context::<OsContext>()?.name.as_str()?.into(),
737 "contexts.os.version" => self.context::<OsContext>()?.version.as_str()?.into(),
738 "contexts.os.rooted" => self.context::<OsContext>()?.rooted.value()?.into(),
739 "contexts.browser" => self.context::<BrowserContext>()?.browser.as_str()?.into(),
740 "contexts.browser.name" => self.context::<BrowserContext>()?.name.as_str()?.into(),
741 "contexts.browser.version" => {
742 self.context::<BrowserContext>()?.version.as_str()?.into()
743 }
744 "contexts.profile.profile_id" => self
745 .context::<ProfileContext>()?
746 .profile_id
747 .value()?
748 .0
749 .into(),
750 "contexts.device.uuid" => self.context::<DeviceContext>()?.uuid.value()?.into(),
751 "contexts.trace.status" => self
752 .context::<TraceContext>()?
753 .status
754 .value()?
755 .as_str()
756 .into(),
757 "contexts.trace.op" => self.context::<TraceContext>()?.op.as_str()?.into(),
758 "contexts.response.status_code" => self
759 .context::<ResponseContext>()?
760 .status_code
761 .value()?
762 .into(),
763 "contexts.unreal.crash_type" => match self.contexts.value()?.get_key("unreal")? {
764 super::Context::Other(context) => context.get("crash_type")?.value()?.into(),
765 _ => return None,
766 },
767 "contexts.runtime" => self.context::<RuntimeContext>()?.runtime.as_str()?.into(),
768 "contexts.runtime.name" => self.context::<RuntimeContext>()?.name.as_str()?.into(),
769
770 "duration" => {
772 let start = self.start_timestamp.value()?;
773 let end = self.timestamp.value()?;
774 if start <= end && self.ty.value() == Some(&EventType::Transaction) {
775 time::chrono_to_positive_millis(*end - *start).into()
776 } else {
777 return None;
778 }
779 }
780
781 path => {
783 if let Some(rest) = path.strip_prefix("release.") {
784 let release = self.parse_release()?;
785 match rest {
786 "build" => release.build_hash()?.into(),
787 "package" => release.package()?.into(),
788 "version.short" => release.version()?.raw_short().into(),
789 _ => return None,
790 }
791 } else if let Some(rest) = path.strip_prefix("measurements.") {
792 let name = rest.strip_suffix(".value")?;
793 self.measurement(name)?.into()
794 } else if let Some(rest) = path.strip_prefix("breakdowns.") {
795 let (breakdown, measurement) = rest.split_once('.')?;
796 self.breakdown(breakdown, measurement)?.into()
797 } else if let Some(rest) = path.strip_prefix("extra.") {
798 self.extra_at(rest)?.into()
799 } else if let Some(rest) = path.strip_prefix("tags.") {
800 self.tags.value()?.get(rest)?.into()
801 } else if let Some(rest) = path.strip_prefix("request.headers.") {
802 self.request
803 .value()?
804 .headers
805 .value()?
806 .get_header(rest)?
807 .into()
808 } else {
809 return None;
810 }
811 }
812 })
813 }
814
815 fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
816 Some(match path.strip_prefix("event.")? {
817 "exception.values" => {
818 GetterIter::new_annotated(self.exceptions.value()?.values.value()?)
819 }
820 _ => return None,
821 })
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use chrono::{TimeZone, Utc};
828 use relay_protocol::{ErrorKind, Map, Meta};
829 use similar_asserts::assert_eq;
830 use std::collections::BTreeMap;
831 use uuid::uuid;
832
833 use super::*;
834 use crate::protocol::{
835 Headers, IpAddr, JsonLenientString, PairList, TagEntry, TransactionSource,
836 };
837
838 #[test]
839 fn test_event_roundtrip() {
840 let json = r#"{
842 "event_id": "52df9022835246eeb317dbd739ccd059",
843 "level": "debug",
844 "fingerprint": [
845 "myprint"
846 ],
847 "culprit": "myculprit",
848 "transaction": "mytransaction",
849 "logentry": {
850 "formatted": "mymessage"
851 },
852 "logger": "mylogger",
853 "modules": {
854 "mymodule": "1.0.0"
855 },
856 "platform": "myplatform",
857 "timestamp": 946684800.0,
858 "server_name": "myhost",
859 "release": "myrelease",
860 "dist": "mydist",
861 "environment": "myenv",
862 "tags": [
863 [
864 "tag",
865 "value"
866 ]
867 ],
868 "extra": {
869 "extra": "value"
870 },
871 "other": "value",
872 "_meta": {
873 "event_id": {
874 "": {
875 "err": [
876 "invalid_data"
877 ]
878 }
879 }
880 }
881}"#;
882
883 let event = Annotated::new(Event {
884 id: Annotated(
885 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
886 Meta::from_error(ErrorKind::InvalidData),
887 ),
888 level: Annotated::new(Level::Debug),
889 fingerprint: Annotated::new(vec!["myprint".to_string()].into()),
890 culprit: Annotated::new("myculprit".to_string()),
891 transaction: Annotated::new("mytransaction".to_string()),
892 logentry: Annotated::new(LogEntry {
893 formatted: Annotated::new("mymessage".to_string().into()),
894 ..Default::default()
895 }),
896 logger: Annotated::new("mylogger".to_string()),
897 modules: {
898 let mut map = Map::new();
899 map.insert("mymodule".to_string(), Annotated::new("1.0.0".to_string()));
900 Annotated::new(map)
901 },
902 platform: Annotated::new("myplatform".to_string()),
903 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
904 server_name: Annotated::new("myhost".to_string()),
905 release: Annotated::new("myrelease".to_string().into()),
906 dist: Annotated::new("mydist".to_string()),
907 environment: Annotated::new("myenv".to_string()),
908 tags: {
909 let items = vec![Annotated::new(TagEntry(
910 Annotated::new("tag".to_string()),
911 Annotated::new("value".to_string()),
912 ))];
913 Annotated::new(Tags(items.into()))
914 },
915 extra: {
916 let mut map = Map::new();
917 map.insert(
918 "extra".to_string(),
919 Annotated::new(ExtraValue(Value::String("value".to_string()))),
920 );
921 Annotated::new(map)
922 },
923 other: {
924 let mut map = Map::new();
925 map.insert(
926 "other".to_string(),
927 Annotated::new(Value::String("value".to_string())),
928 );
929 map
930 },
931 ..Default::default()
932 });
933
934 assert_eq!(event, Annotated::from_json(json).unwrap());
935 assert_eq!(json, event.to_json_pretty().unwrap());
936 }
937
938 #[test]
939 fn test_event_default_values() {
940 let json = "{}";
941 let event = Annotated::new(Event::default());
942
943 assert_eq!(event, Annotated::from_json(json).unwrap());
944 assert_eq!(json, event.to_json_pretty().unwrap());
945 }
946
947 #[test]
948 fn test_event_default_values_with_meta() {
949 let json = r#"{
950 "event_id": "52df9022835246eeb317dbd739ccd059",
951 "fingerprint": [
952 "{{ default }}"
953 ],
954 "platform": "other",
955 "_meta": {
956 "event_id": {
957 "": {
958 "err": [
959 "invalid_data"
960 ]
961 }
962 },
963 "fingerprint": {
964 "": {
965 "err": [
966 "invalid_data"
967 ]
968 }
969 },
970 "platform": {
971 "": {
972 "err": [
973 "invalid_data"
974 ]
975 }
976 }
977 }
978}"#;
979
980 let event = Annotated::new(Event {
981 id: Annotated(
982 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
983 Meta::from_error(ErrorKind::InvalidData),
984 ),
985 fingerprint: Annotated(
986 Some(vec!["{{ default }}".to_string()].into()),
987 Meta::from_error(ErrorKind::InvalidData),
988 ),
989 platform: Annotated(
990 Some("other".to_string()),
991 Meta::from_error(ErrorKind::InvalidData),
992 ),
993 ..Default::default()
994 });
995
996 assert_eq!(event, Annotated::<Event>::from_json(json).unwrap());
997 assert_eq!(json, event.to_json_pretty().unwrap());
998 }
999
1000 #[test]
1001 fn test_event_type() {
1002 assert_eq!(
1003 EventType::Default,
1004 *Annotated::<EventType>::from_json("\"default\"")
1005 .unwrap()
1006 .value()
1007 .unwrap()
1008 );
1009 }
1010
1011 #[test]
1012 fn test_fingerprint_empty_string() {
1013 let json = r#"{"fingerprint":[""]}"#;
1014 let event = Annotated::new(Event {
1015 fingerprint: Annotated::new(vec!["".to_string()].into()),
1016 ..Default::default()
1017 });
1018
1019 assert_eq!(json, event.to_json().unwrap());
1020 assert_eq!(event, Annotated::from_json(json).unwrap());
1021 }
1022
1023 #[test]
1024 fn test_fingerprint_null_values() {
1025 let input = r#"{"fingerprint":[null]}"#;
1026 let output = r#"{}"#;
1027 let event = Annotated::new(Event {
1028 fingerprint: Annotated::new(vec![].into()),
1029 ..Default::default()
1030 });
1031
1032 assert_eq!(event, Annotated::from_json(input).unwrap());
1033 assert_eq!(output, event.to_json().unwrap());
1034 }
1035
1036 #[test]
1037 fn test_empty_threads() {
1038 let input = r#"{"threads": {}}"#;
1039 let output = r#"{}"#;
1040
1041 let event = Annotated::new(Event::default());
1042
1043 assert_eq!(event, Annotated::from_json(input).unwrap());
1044 assert_eq!(output, event.to_json().unwrap());
1045 }
1046
1047 #[test]
1048 fn test_lenient_release() {
1049 let input = r#"{"release":42}"#;
1050 let output = r#"{"release":"42"}"#;
1051 let event = Annotated::new(Event {
1052 release: Annotated::new("42".to_string().into()),
1053 ..Default::default()
1054 });
1055
1056 assert_eq!(event, Annotated::from_json(input).unwrap());
1057 assert_eq!(output, event.to_json().unwrap());
1058 }
1059
1060 #[test]
1061 fn test_extra_at() {
1062 let json = serde_json::json!({
1063 "extra": {
1064 "a": "string1",
1065 "b": 42,
1066 "c": {
1067 "d": "string2",
1068 "e": null,
1069 },
1070 },
1071 });
1072
1073 let event = Event::from_value(json.into());
1074 let event = event.value().unwrap();
1075
1076 assert_eq!(
1077 Some(&Value::String("string1".to_owned())),
1078 event.extra_at("a")
1079 );
1080 assert_eq!(Some(&Value::I64(42)), event.extra_at("b"));
1081 assert!(matches!(event.extra_at("c"), Some(&Value::Object(_))));
1082 assert_eq!(None, event.extra_at("d"));
1083 assert_eq!(
1084 Some(&Value::String("string2".to_owned())),
1085 event.extra_at("c.d")
1086 );
1087 assert_eq!(None, event.extra_at("c.e"));
1088 assert_eq!(None, event.extra_at("c.f"));
1089 }
1090
1091 #[test]
1092 fn test_scrape_attempts() {
1093 let json = serde_json::json!({
1094 "scraping_attempts": [
1095 {"status": "not_attempted", "url": "http://example.com/embedded.js"},
1096 {"status": "not_attempted", "url": "http://example.com/embedded.js.map"},
1097 ]
1098 });
1099
1100 let event = Event::from_value(json.into());
1101 assert!(!event.value().unwrap().scraping_attempts.meta().has_errors());
1102 }
1103
1104 #[test]
1105 fn test_field_value_provider_event_filled() {
1106 let event = Event {
1107 level: Annotated::new(Level::Info),
1108 release: Annotated::new(LenientString("1.1.1".to_owned())),
1109 environment: Annotated::new("prod".to_owned()),
1110 user: Annotated::new(User {
1111 ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
1112 id: Annotated::new(LenientString("user-id".into())),
1113 segment: Annotated::new("user-seg".into()),
1114 sentry_user: Annotated::new("id:user-id".into()),
1115 ..Default::default()
1116 }),
1117 client_sdk: Annotated::new(ClientSdkInfo {
1118 name: Annotated::new("sentry-javascript".into()),
1119 version: Annotated::new("1.87.0".into()),
1120 ..Default::default()
1121 }),
1122 exceptions: Annotated::new(Values {
1123 values: Annotated::new(vec![Annotated::new(Exception {
1124 value: Annotated::new(JsonLenientString::from(
1125 "canvas.contentDocument".to_owned(),
1126 )),
1127 ..Default::default()
1128 })]),
1129 ..Default::default()
1130 }),
1131 logentry: Annotated::new(LogEntry {
1132 formatted: Annotated::new("formatted".to_string().into()),
1133 message: Annotated::new("message".to_string().into()),
1134 ..Default::default()
1135 }),
1136 request: Annotated::new(Request {
1137 headers: Annotated::new(Headers(PairList(vec![Annotated::new((
1138 Annotated::new("user-agent".into()),
1139 Annotated::new("Slurp".into()),
1140 ))]))),
1141 url: Annotated::new("https://sentry.io".into()),
1142 ..Default::default()
1143 }),
1144 transaction: Annotated::new("some-transaction".into()),
1145 transaction_info: Annotated::new(TransactionInfo {
1146 source: Annotated::new(TransactionSource::Route),
1147 ..Default::default()
1148 }),
1149 tags: {
1150 let items = vec![Annotated::new(TagEntry(
1151 Annotated::new("custom".to_string()),
1152 Annotated::new("custom-value".to_string()),
1153 ))];
1154 Annotated::new(Tags(items.into()))
1155 },
1156 contexts: Annotated::new({
1157 let mut contexts = Contexts::new();
1158 contexts.add(DeviceContext {
1159 name: Annotated::new("iphone".to_string()),
1160 family: Annotated::new("iphone-fam".to_string()),
1161 model: Annotated::new("iphone7,3".to_string()),
1162 screen_dpi: Annotated::new(560),
1163 screen_width_pixels: Annotated::new(1920),
1164 screen_height_pixels: Annotated::new(1080),
1165 locale: Annotated::new("US".into()),
1166 uuid: Annotated::new(uuid!("abadcade-feed-dead-beef-baddadfeeded")),
1167 charging: Annotated::new(true),
1168 ..DeviceContext::default()
1169 });
1170 contexts.add(OsContext {
1171 name: Annotated::new("iOS".to_string()),
1172 version: Annotated::new("11.4.2".to_string()),
1173 kernel_version: Annotated::new("17.4.0".to_string()),
1174 ..OsContext::default()
1175 });
1176 contexts.add(ProfileContext {
1177 profile_id: Annotated::new(EventId(uuid!(
1178 "abadcade-feed-dead-beef-8addadfeedaa"
1179 ))),
1180 ..ProfileContext::default()
1181 });
1182 let mut monitor_context_fields = BTreeMap::new();
1183 monitor_context_fields.insert(
1184 "id".to_string(),
1185 Annotated::new(Value::String("123".to_string())),
1186 );
1187 monitor_context_fields.insert(
1188 "slug".to_string(),
1189 Annotated::new(Value::String("my_monitor".to_string())),
1190 );
1191 contexts.add(MonitorContext(monitor_context_fields));
1192 contexts
1193 }),
1194 ..Default::default()
1195 };
1196
1197 assert_eq!(Some(Val::String("info")), event.get_value("event.level"));
1198
1199 assert_eq!(Some(Val::String("1.1.1")), event.get_value("event.release"));
1200 assert_eq!(
1201 Some(Val::String("prod")),
1202 event.get_value("event.environment")
1203 );
1204 assert_eq!(
1205 Some(Val::String("user-id")),
1206 event.get_value("event.user.id")
1207 );
1208 assert_eq!(
1209 Some(Val::String("id:user-id")),
1210 event.get_value("event.sentry_user")
1211 );
1212 assert_eq!(
1213 Some(Val::String("user-seg")),
1214 event.get_value("event.user.segment")
1215 );
1216 assert_eq!(
1217 Some(Val::String("some-transaction")),
1218 event.get_value("event.transaction")
1219 );
1220 assert_eq!(
1221 Some(Val::String("iphone")),
1222 event.get_value("event.contexts.device.name")
1223 );
1224 assert_eq!(
1225 Some(Val::String("iphone-fam")),
1226 event.get_value("event.contexts.device.family")
1227 );
1228 assert_eq!(
1229 Some(Val::String("iOS")),
1230 event.get_value("event.contexts.os.name")
1231 );
1232 assert_eq!(
1233 Some(Val::String("11.4.2")),
1234 event.get_value("event.contexts.os.version")
1235 );
1236 assert_eq!(
1237 Some(Val::String("custom-value")),
1238 event.get_value("event.tags.custom")
1239 );
1240 assert_eq!(None, event.get_value("event.tags.doesntexist"));
1241 assert_eq!(
1242 Some(Val::String("sentry-javascript")),
1243 event.get_value("event.sdk.name")
1244 );
1245 assert_eq!(
1246 Some(Val::String("1.87.0")),
1247 event.get_value("event.sdk.version")
1248 );
1249 assert_eq!(
1250 Some(Val::String("17.4.0")),
1251 event.get_value("event.contexts.os.kernel_version")
1252 );
1253 assert_eq!(
1254 Some(Val::I64(560)),
1255 event.get_value("event.contexts.device.screen_dpi")
1256 );
1257 assert_eq!(
1258 Some(Val::Bool(true)),
1259 event.get_value("event.contexts.device.charging")
1260 );
1261 assert_eq!(
1262 Some(Val::U64(1920)),
1263 event.get_value("event.contexts.device.screen_width_pixels")
1264 );
1265 assert_eq!(
1266 Some(Val::U64(1080)),
1267 event.get_value("event.contexts.device.screen_height_pixels")
1268 );
1269 assert_eq!(
1270 Some(Val::String("US")),
1271 event.get_value("event.contexts.device.locale")
1272 );
1273 assert_eq!(
1274 Some(Val::Uuid(uuid!("abadcade-feed-dead-beef-baddadfeeded"))),
1275 event.get_value("event.contexts.device.uuid")
1276 );
1277 assert_eq!(
1278 Some(Val::String("https://sentry.io")),
1279 event.get_value("event.request.url")
1280 );
1281 assert_eq!(
1282 Some(Val::Uuid(uuid!("abadcade-feed-dead-beef-8addadfeedaa"))),
1283 event.get_value("event.contexts.profile.profile_id")
1284 );
1285 assert_eq!(
1286 Some(Val::String("route")),
1287 event.get_value("event.transaction.source")
1288 );
1289
1290 let mut exceptions = event.get_iter("event.exception.values").unwrap();
1291 let exception = exceptions.next().unwrap();
1292 assert_eq!(
1293 Some(Val::String("canvas.contentDocument")),
1294 exception.get_value("value")
1295 );
1296 assert!(exceptions.next().is_none());
1297
1298 assert_eq!(
1299 Some(Val::String("formatted")),
1300 event.get_value("event.logentry.formatted")
1301 );
1302 assert_eq!(
1303 Some(Val::String("message")),
1304 event.get_value("event.logentry.message")
1305 );
1306 assert_eq!(
1307 Some(Val::String("123")),
1308 event.get_value("event.contexts.monitor.id")
1309 );
1310 assert_eq!(
1311 Some(Val::String("my_monitor")),
1312 event.get_value("event.contexts.monitor.slug")
1313 );
1314 }
1315
1316 #[test]
1317 fn test_field_value_provider_event_empty() {
1318 let event = Event::default();
1319
1320 assert_eq!(None, event.get_value("event.release"));
1321 assert_eq!(None, event.get_value("event.environment"));
1322 assert_eq!(None, event.get_value("event.user.id"));
1323 assert_eq!(None, event.get_value("event.user.segment"));
1324
1325 let event = Event {
1327 user: Annotated::new(User {
1328 ..Default::default()
1329 }),
1330 ..Default::default()
1331 };
1332
1333 assert_eq!(None, event.get_value("event.user.id"));
1334 assert_eq!(None, event.get_value("event.user.segment"));
1335 assert_eq!(None, event.get_value("event.transaction"));
1336 }
1337}