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