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, ExpectCt, ExpectStaple, Fingerprint,
16 GpuContext, Hpkp, LenientString, Level, LogEntry, Measurements, Metrics, MonitorContext,
17 OsContext, ProfileContext, RelayInfo, Request, ResponseContext, RuntimeContext, Span, SpanId,
18 Stacktrace, Tags, TemplateInfo, Thread, 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(pii = "true", legacy_alias = "sentry.interfaces.Hpkp")]
449 #[metastructure(omit_from_schema)] pub hpkp: Annotated<Hpkp>,
451
452 #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectCT")]
454 #[metastructure(omit_from_schema)] pub expectct: Annotated<ExpectCt>,
456
457 #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectStaple")]
459 #[metastructure(omit_from_schema)] pub expectstaple: Annotated<ExpectStaple>,
461
462 #[metastructure(max_bytes = 819200)]
464 #[metastructure(omit_from_schema)] pub spans: Annotated<Array<Span>>,
466
467 #[metastructure(skip_serialization = "empty")]
472 #[metastructure(omit_from_schema)] pub measurements: Annotated<Measurements>,
474
475 #[metastructure(skip_serialization = "empty")]
477 #[metastructure(omit_from_schema)] pub breakdowns: Annotated<Breakdowns>,
479
480 #[metastructure(omit_from_schema)] pub scraping_attempts: Annotated<Value>,
484
485 #[metastructure(omit_from_schema)]
489 pub _metrics: Annotated<Metrics>,
490
491 #[metastructure(omit_from_schema)]
493 pub _dsc: Annotated<Value>,
494
495 #[metastructure(
500 field = "_performance_issues_spans",
501 skip_serialization = "empty",
502 trim = false
503 )]
504 pub performance_issues_spans: Annotated<bool>,
505
506 #[metastructure(additional_properties, pii = "true")]
508 pub other: Object<Value>,
509}
510
511impl Event {
512 pub fn tag_value(&self, tag_key: &str) -> Option<&str> {
517 if let Some(tags) = self.tags.value() {
518 tags.get(tag_key)
519 } else {
520 None
521 }
522 }
523
524 pub fn has_module(&self, module_name: &str) -> bool {
526 self.modules
527 .value()
528 .map(|m| m.contains_key(module_name))
529 .unwrap_or(false)
530 }
531
532 pub fn sdk_name(&self) -> &str {
536 if let Some(client_sdk) = self.client_sdk.value()
537 && let Some(name) = client_sdk.name.as_str()
538 {
539 return name;
540 }
541
542 "unknown"
543 }
544
545 pub fn sdk_version(&self) -> &str {
549 if let Some(client_sdk) = self.client_sdk.value()
550 && let Some(version) = client_sdk.version.as_str()
551 {
552 return version;
553 }
554
555 "unknown"
556 }
557
558 pub fn user_agent(&self) -> Option<&str> {
563 let headers = self.request.value()?.headers.value()?;
564
565 for item in headers.iter() {
566 if let Some((o_k, v)) = item.value()
567 && let Some(k) = o_k.as_str()
568 && k.eq_ignore_ascii_case("user-agent")
569 {
570 return v.as_str();
571 }
572 }
573
574 None
575 }
576
577 pub fn extra_at(&self, path: &str) -> Option<&Value> {
582 let mut path = path.split('.');
583
584 let mut value = &self.extra.value()?.get(path.next()?)?.value()?.0;
586
587 for key in path {
589 if let Value::Object(object) = value {
590 value = object.get(key)?.value()?;
591 } else {
592 return None;
593 }
594 }
595
596 Some(value)
597 }
598
599 pub fn parse_release(&self) -> Option<ParsedRelease<'_>> {
601 sentry_release_parser::Release::parse(self.release.as_str()?).ok()
602 }
603
604 pub fn measurement(&self, name: &str) -> Option<FiniteF64> {
608 let annotated = self.measurements.value()?.get(name)?;
609 Some(*annotated.value()?.value.value()?)
610 }
611
612 pub fn breakdown(&self, breakdown: &str, measurement: &str) -> Option<FiniteF64> {
614 let breakdown = self.breakdowns.value()?.get(breakdown)?.value()?;
615 Some(*breakdown.get(measurement)?.value()?.value.value()?)
616 }
617
618 pub fn context<C: DefaultContext>(&self) -> Option<&C> {
620 self.contexts.value()?.get()
621 }
622
623 pub fn context_mut<C: DefaultContext>(&mut self) -> Option<&mut C> {
625 self.contexts.value_mut().as_mut()?.get_mut()
626 }
627}
628
629fn or_none(string: &Annotated<impl AsRef<str>>) -> Option<&str> {
630 match string.as_str() {
631 None | Some("") => None,
632 Some(other) => Some(other),
633 }
634}
635
636impl Getter for Event {
637 fn get_value(&self, path: &str) -> Option<Val<'_>> {
638 Some(match path.strip_prefix("event.")? {
639 "level" => self.level.value()?.name().into(),
641 "release" => self.release.as_str()?.into(),
642 "dist" => self.dist.as_str()?.into(),
643 "environment" => self.environment.as_str()?.into(),
644 "transaction" => self.transaction.as_str()?.into(),
645 "logger" => self.logger.as_str()?.into(),
646 "platform" => self.platform.as_str().unwrap_or("other").into(),
647
648 "logentry.formatted" => self.logentry.value()?.formatted.value()?.as_ref().into(),
650 "logentry.message" => self.logentry.value()?.message.value()?.as_ref().into(),
651 "user.email" => or_none(&self.user.value()?.email)?.into(),
652 "user.id" => or_none(&self.user.value()?.id)?.into(),
653 "user.ip_address" => self.user.value()?.ip_address.as_str()?.into(),
654 "user.name" => self.user.value()?.name.as_str()?.into(),
655 "user.segment" => or_none(&self.user.value()?.segment)?.into(),
656 "user.geo.city" => self.user.value()?.geo.value()?.city.as_str()?.into(),
657 "user.geo.country_code" => self
658 .user
659 .value()?
660 .geo
661 .value()?
662 .country_code
663 .as_str()?
664 .into(),
665 "user.geo.region" => self.user.value()?.geo.value()?.region.as_str()?.into(),
666 "user.geo.subdivision" => self.user.value()?.geo.value()?.subdivision.as_str()?.into(),
667 "request.method" => self.request.value()?.method.as_str()?.into(),
668 "request.url" => self.request.value()?.url.as_str()?.into(),
669 "transaction.source" => self
670 .transaction_info
671 .value()?
672 .source
673 .value()?
674 .as_str()
675 .into(),
676 "sdk.name" => self.client_sdk.value()?.name.as_str()?.into(),
677 "sdk.version" => self.client_sdk.value()?.version.as_str()?.into(),
678
679 "sentry_user" => self.user.value()?.sentry_user.as_str()?.into(),
681
682 "contexts.app.in_foreground" => {
684 self.context::<AppContext>()?.in_foreground.value()?.into()
685 }
686 "contexts.app.device_app_hash" => self
687 .context::<AppContext>()?
688 .device_app_hash
689 .as_str()?
690 .into(),
691 "contexts.device.arch" => self.context::<DeviceContext>()?.arch.as_str()?.into(),
692 "contexts.device.battery_level" => self
693 .context::<DeviceContext>()?
694 .battery_level
695 .value()?
696 .into(),
697 "contexts.device.brand" => self.context::<DeviceContext>()?.brand.as_str()?.into(),
698 "contexts.device.charging" => self.context::<DeviceContext>()?.charging.value()?.into(),
699 "contexts.device.family" => self.context::<DeviceContext>()?.family.as_str()?.into(),
700 "contexts.device.model" => self.context::<DeviceContext>()?.model.as_str()?.into(),
701 "contexts.device.locale" => self.context::<DeviceContext>()?.locale.as_str()?.into(),
702 "contexts.device.online" => self.context::<DeviceContext>()?.online.value()?.into(),
703 "contexts.device.orientation" => self
704 .context::<DeviceContext>()?
705 .orientation
706 .as_str()?
707 .into(),
708 "contexts.device.name" => self.context::<DeviceContext>()?.name.as_str()?.into(),
709 "contexts.device.screen_density" => self
710 .context::<DeviceContext>()?
711 .screen_density
712 .value()?
713 .into(),
714 "contexts.device.screen_dpi" => {
715 self.context::<DeviceContext>()?.screen_dpi.value()?.into()
716 }
717 "contexts.device.screen_width_pixels" => self
718 .context::<DeviceContext>()?
719 .screen_width_pixels
720 .value()?
721 .into(),
722 "contexts.device.screen_height_pixels" => self
723 .context::<DeviceContext>()?
724 .screen_height_pixels
725 .value()?
726 .into(),
727 "contexts.device.simulator" => {
728 self.context::<DeviceContext>()?.simulator.value()?.into()
729 }
730 "contexts.gpu.vendor_name" => {
731 self.context::<GpuContext>()?.vendor_name.as_str()?.into()
732 }
733 "contexts.gpu.name" => self.context::<GpuContext>()?.name.as_str()?.into(),
734 "contexts.monitor.id" => self.context::<MonitorContext>()?.get("id")?.value()?.into(),
735 "contexts.monitor.slug" => self
736 .context::<MonitorContext>()?
737 .get("slug")?
738 .value()?
739 .into(),
740 "contexts.os" => self.context::<OsContext>()?.os.as_str()?.into(),
741 "contexts.os.build" => self.context::<OsContext>()?.build.as_str()?.into(),
742 "contexts.os.kernel_version" => {
743 self.context::<OsContext>()?.kernel_version.as_str()?.into()
744 }
745 "contexts.os.name" => self.context::<OsContext>()?.name.as_str()?.into(),
746 "contexts.os.version" => self.context::<OsContext>()?.version.as_str()?.into(),
747 "contexts.os.rooted" => self.context::<OsContext>()?.rooted.value()?.into(),
748 "contexts.browser" => self.context::<BrowserContext>()?.browser.as_str()?.into(),
749 "contexts.browser.name" => self.context::<BrowserContext>()?.name.as_str()?.into(),
750 "contexts.browser.version" => {
751 self.context::<BrowserContext>()?.version.as_str()?.into()
752 }
753 "contexts.profile.profile_id" => {
754 (&self.context::<ProfileContext>()?.profile_id.value()?.0).into()
755 }
756 "contexts.device.uuid" => self.context::<DeviceContext>()?.uuid.value()?.into(),
757 "contexts.trace.status" => self
758 .context::<TraceContext>()?
759 .status
760 .value()?
761 .as_str()
762 .into(),
763 "contexts.trace.op" => self.context::<TraceContext>()?.op.as_str()?.into(),
764 "contexts.response.status_code" => self
765 .context::<ResponseContext>()?
766 .status_code
767 .value()?
768 .into(),
769 "contexts.unreal.crash_type" => match self.contexts.value()?.get_key("unreal")? {
770 super::Context::Other(context) => context.get("crash_type")?.value()?.into(),
771 _ => return None,
772 },
773 "contexts.runtime" => self.context::<RuntimeContext>()?.runtime.as_str()?.into(),
774 "contexts.runtime.name" => self.context::<RuntimeContext>()?.name.as_str()?.into(),
775
776 "duration" => {
778 let start = self.start_timestamp.value()?;
779 let end = self.timestamp.value()?;
780 if start <= end && self.ty.value() == Some(&EventType::Transaction) {
781 time::chrono_to_positive_millis(*end - *start).into()
782 } else {
783 return None;
784 }
785 }
786
787 path => {
789 if let Some(rest) = path.strip_prefix("release.") {
790 let release = self.parse_release()?;
791 match rest {
792 "build" => release.build_hash()?.into(),
793 "package" => release.package()?.into(),
794 "version.short" => release.version()?.raw_short().into(),
795 _ => return None,
796 }
797 } else if let Some(rest) = path.strip_prefix("measurements.") {
798 let name = rest.strip_suffix(".value")?;
799 self.measurement(name)?.into()
800 } else if let Some(rest) = path.strip_prefix("breakdowns.") {
801 let (breakdown, measurement) = rest.split_once('.')?;
802 self.breakdown(breakdown, measurement)?.into()
803 } else if let Some(rest) = path.strip_prefix("extra.") {
804 self.extra_at(rest)?.into()
805 } else if let Some(rest) = path.strip_prefix("tags.") {
806 self.tags.value()?.get(rest)?.into()
807 } else {
808 let rest = path.strip_prefix("request.headers.")?;
809 self.request
810 .value()?
811 .headers
812 .value()?
813 .get_header(rest)?
814 .into()
815 }
816 }
817 })
818 }
819
820 fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
821 Some(match path.strip_prefix("event.")? {
822 "exception.values" => {
823 GetterIter::new_annotated(self.exceptions.value()?.values.value()?)
824 }
825 _ => return None,
826 })
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use chrono::{TimeZone, Utc};
833 use relay_protocol::{ErrorKind, HexId, Map, Meta};
834 use similar_asserts::assert_eq;
835 use std::collections::BTreeMap;
836 use uuid::uuid;
837
838 use super::*;
839 use crate::protocol::{
840 Headers, IpAddr, JsonLenientString, PairList, TagEntry, TransactionSource,
841 };
842
843 #[test]
844 fn test_event_roundtrip() {
845 let json = r#"{
847 "event_id": "52df9022835246eeb317dbd739ccd059",
848 "level": "debug",
849 "fingerprint": [
850 "myprint"
851 ],
852 "culprit": "myculprit",
853 "transaction": "mytransaction",
854 "logentry": {
855 "formatted": "mymessage"
856 },
857 "logger": "mylogger",
858 "modules": {
859 "mymodule": "1.0.0"
860 },
861 "platform": "myplatform",
862 "timestamp": 946684800.0,
863 "server_name": "myhost",
864 "release": "myrelease",
865 "dist": "mydist",
866 "environment": "myenv",
867 "tags": [
868 [
869 "tag",
870 "value"
871 ]
872 ],
873 "extra": {
874 "extra": "value"
875 },
876 "other": "value",
877 "_meta": {
878 "event_id": {
879 "": {
880 "err": [
881 "invalid_data"
882 ]
883 }
884 }
885 }
886}"#;
887
888 let event = Annotated::new(Event {
889 id: Annotated(
890 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
891 Meta::from_error(ErrorKind::InvalidData),
892 ),
893 level: Annotated::new(Level::Debug),
894 fingerprint: Annotated::new(vec!["myprint".to_owned()].into()),
895 culprit: Annotated::new("myculprit".to_owned()),
896 transaction: Annotated::new("mytransaction".to_owned()),
897 logentry: Annotated::new(LogEntry {
898 formatted: Annotated::new("mymessage".to_owned().into()),
899 ..Default::default()
900 }),
901 logger: Annotated::new("mylogger".to_owned()),
902 modules: {
903 let mut map = Map::new();
904 map.insert("mymodule".to_owned(), Annotated::new("1.0.0".to_owned()));
905 Annotated::new(map)
906 },
907 platform: Annotated::new("myplatform".to_owned()),
908 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
909 server_name: Annotated::new("myhost".to_owned()),
910 release: Annotated::new("myrelease".to_owned().into()),
911 dist: Annotated::new("mydist".to_owned()),
912 environment: Annotated::new("myenv".to_owned()),
913 tags: {
914 let items = vec![Annotated::new(TagEntry(
915 Annotated::new("tag".to_owned()),
916 Annotated::new("value".to_owned()),
917 ))];
918 Annotated::new(Tags(items.into()))
919 },
920 extra: {
921 let mut map = Map::new();
922 map.insert(
923 "extra".to_owned(),
924 Annotated::new(ExtraValue(Value::String("value".to_owned()))),
925 );
926 Annotated::new(map)
927 },
928 other: {
929 let mut map = Map::new();
930 map.insert(
931 "other".to_owned(),
932 Annotated::new(Value::String("value".to_owned())),
933 );
934 map
935 },
936 ..Default::default()
937 });
938
939 assert_eq!(event, Annotated::from_json(json).unwrap());
940 assert_eq!(json, event.to_json_pretty().unwrap());
941 }
942
943 #[test]
944 fn test_event_default_values() {
945 let json = "{}";
946 let event = Annotated::new(Event::default());
947
948 assert_eq!(event, Annotated::from_json(json).unwrap());
949 assert_eq!(json, event.to_json_pretty().unwrap());
950 }
951
952 #[test]
953 fn test_event_default_values_with_meta() {
954 let json = r#"{
955 "event_id": "52df9022835246eeb317dbd739ccd059",
956 "fingerprint": [
957 "{{ default }}"
958 ],
959 "platform": "other",
960 "_meta": {
961 "event_id": {
962 "": {
963 "err": [
964 "invalid_data"
965 ]
966 }
967 },
968 "fingerprint": {
969 "": {
970 "err": [
971 "invalid_data"
972 ]
973 }
974 },
975 "platform": {
976 "": {
977 "err": [
978 "invalid_data"
979 ]
980 }
981 }
982 }
983}"#;
984
985 let event = Annotated::new(Event {
986 id: Annotated(
987 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
988 Meta::from_error(ErrorKind::InvalidData),
989 ),
990 fingerprint: Annotated(
991 Some(vec!["{{ default }}".to_owned()].into()),
992 Meta::from_error(ErrorKind::InvalidData),
993 ),
994 platform: Annotated(
995 Some("other".to_owned()),
996 Meta::from_error(ErrorKind::InvalidData),
997 ),
998 ..Default::default()
999 });
1000
1001 assert_eq!(event, Annotated::<Event>::from_json(json).unwrap());
1002 assert_eq!(json, event.to_json_pretty().unwrap());
1003 }
1004
1005 #[test]
1006 fn test_event_type() {
1007 assert_eq!(
1008 EventType::Default,
1009 *Annotated::<EventType>::from_json("\"default\"")
1010 .unwrap()
1011 .value()
1012 .unwrap()
1013 );
1014 }
1015
1016 #[test]
1017 fn test_fingerprint_empty_string() {
1018 let json = r#"{"fingerprint":[""]}"#;
1019 let event = Annotated::new(Event {
1020 fingerprint: Annotated::new(vec!["".to_owned()].into()),
1021 ..Default::default()
1022 });
1023
1024 assert_eq!(json, event.to_json().unwrap());
1025 assert_eq!(event, Annotated::from_json(json).unwrap());
1026 }
1027
1028 #[test]
1029 fn test_fingerprint_null_values() {
1030 let input = r#"{"fingerprint":[null]}"#;
1031 let output = r#"{}"#;
1032 let event = Annotated::new(Event {
1033 fingerprint: Annotated::new(vec![].into()),
1034 ..Default::default()
1035 });
1036
1037 assert_eq!(event, Annotated::from_json(input).unwrap());
1038 assert_eq!(output, event.to_json().unwrap());
1039 }
1040
1041 #[test]
1042 fn test_empty_threads() {
1043 let input = r#"{"threads": {}}"#;
1044 let output = r#"{}"#;
1045
1046 let event = Annotated::new(Event::default());
1047
1048 assert_eq!(event, Annotated::from_json(input).unwrap());
1049 assert_eq!(output, event.to_json().unwrap());
1050 }
1051
1052 #[test]
1053 fn test_lenient_release() {
1054 let input = r#"{"release":42}"#;
1055 let output = r#"{"release":"42"}"#;
1056 let event = Annotated::new(Event {
1057 release: Annotated::new("42".to_owned().into()),
1058 ..Default::default()
1059 });
1060
1061 assert_eq!(event, Annotated::from_json(input).unwrap());
1062 assert_eq!(output, event.to_json().unwrap());
1063 }
1064
1065 #[test]
1066 fn test_extra_at() {
1067 let json = serde_json::json!({
1068 "extra": {
1069 "a": "string1",
1070 "b": 42,
1071 "c": {
1072 "d": "string2",
1073 "e": null,
1074 },
1075 },
1076 });
1077
1078 let event = Event::from_value(json.into());
1079 let event = event.value().unwrap();
1080
1081 assert_eq!(
1082 Some(&Value::String("string1".to_owned())),
1083 event.extra_at("a")
1084 );
1085 assert_eq!(Some(&Value::I64(42)), event.extra_at("b"));
1086 assert!(matches!(event.extra_at("c"), Some(&Value::Object(_))));
1087 assert_eq!(None, event.extra_at("d"));
1088 assert_eq!(
1089 Some(&Value::String("string2".to_owned())),
1090 event.extra_at("c.d")
1091 );
1092 assert_eq!(None, event.extra_at("c.e"));
1093 assert_eq!(None, event.extra_at("c.f"));
1094 }
1095
1096 #[test]
1097 fn test_scrape_attempts() {
1098 let json = serde_json::json!({
1099 "scraping_attempts": [
1100 {"status": "not_attempted", "url": "http://example.com/embedded.js"},
1101 {"status": "not_attempted", "url": "http://example.com/embedded.js.map"},
1102 ]
1103 });
1104
1105 let event = Event::from_value(json.into());
1106 assert!(!event.value().unwrap().scraping_attempts.meta().has_errors());
1107 }
1108
1109 #[test]
1110 fn test_field_value_provider_event_filled() {
1111 let event = Event {
1112 level: Annotated::new(Level::Info),
1113 release: Annotated::new(LenientString("1.1.1".to_owned())),
1114 environment: Annotated::new("prod".to_owned()),
1115 user: Annotated::new(User {
1116 ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
1117 id: Annotated::new(LenientString("user-id".into())),
1118 segment: Annotated::new("user-seg".into()),
1119 sentry_user: Annotated::new("id:user-id".into()),
1120 ..Default::default()
1121 }),
1122 client_sdk: Annotated::new(ClientSdkInfo {
1123 name: Annotated::new("sentry-javascript".into()),
1124 version: Annotated::new("1.87.0".into()),
1125 ..Default::default()
1126 }),
1127 exceptions: Annotated::new(Values {
1128 values: Annotated::new(vec![Annotated::new(Exception {
1129 value: Annotated::new(JsonLenientString::from(
1130 "canvas.contentDocument".to_owned(),
1131 )),
1132 ..Default::default()
1133 })]),
1134 ..Default::default()
1135 }),
1136 logentry: Annotated::new(LogEntry {
1137 formatted: Annotated::new("formatted".to_owned().into()),
1138 message: Annotated::new("message".to_owned().into()),
1139 ..Default::default()
1140 }),
1141 request: Annotated::new(Request {
1142 headers: Annotated::new(Headers(PairList(vec![Annotated::new((
1143 Annotated::new("user-agent".into()),
1144 Annotated::new("Slurp".into()),
1145 ))]))),
1146 url: Annotated::new("https://sentry.io".into()),
1147 ..Default::default()
1148 }),
1149 transaction: Annotated::new("some-transaction".into()),
1150 transaction_info: Annotated::new(TransactionInfo {
1151 source: Annotated::new(TransactionSource::Route),
1152 ..Default::default()
1153 }),
1154 tags: {
1155 let items = vec![Annotated::new(TagEntry(
1156 Annotated::new("custom".to_owned()),
1157 Annotated::new("custom-value".to_owned()),
1158 ))];
1159 Annotated::new(Tags(items.into()))
1160 },
1161 contexts: Annotated::new({
1162 let mut contexts = Contexts::new();
1163 contexts.add(DeviceContext {
1164 name: Annotated::new("iphone".to_owned()),
1165 family: Annotated::new("iphone-fam".to_owned()),
1166 model: Annotated::new("iphone7,3".to_owned()),
1167 screen_dpi: Annotated::new(560),
1168 screen_width_pixels: Annotated::new(1920),
1169 screen_height_pixels: Annotated::new(1080),
1170 locale: Annotated::new("US".into()),
1171 uuid: Annotated::new(uuid!("abadcade-feed-dead-beef-baddadfeeded")),
1172 charging: Annotated::new(true),
1173 ..DeviceContext::default()
1174 });
1175 contexts.add(OsContext {
1176 name: Annotated::new("iOS".to_owned()),
1177 version: Annotated::new("11.4.2".to_owned()),
1178 kernel_version: Annotated::new("17.4.0".to_owned()),
1179 ..OsContext::default()
1180 });
1181 contexts.add(ProfileContext {
1182 profile_id: Annotated::new(EventId(uuid!(
1183 "abadcade-feed-dead-beef-8addadfeedaa"
1184 ))),
1185 ..ProfileContext::default()
1186 });
1187 let mut monitor_context_fields = BTreeMap::new();
1188 monitor_context_fields.insert(
1189 "id".to_owned(),
1190 Annotated::new(Value::String("123".to_owned())),
1191 );
1192 monitor_context_fields.insert(
1193 "slug".to_owned(),
1194 Annotated::new(Value::String("my_monitor".to_owned())),
1195 );
1196 contexts.add(MonitorContext(monitor_context_fields));
1197 contexts
1198 }),
1199 ..Default::default()
1200 };
1201
1202 assert_eq!(Some(Val::String("info")), event.get_value("event.level"));
1203
1204 assert_eq!(Some(Val::String("1.1.1")), event.get_value("event.release"));
1205 assert_eq!(
1206 Some(Val::String("prod")),
1207 event.get_value("event.environment")
1208 );
1209 assert_eq!(
1210 Some(Val::String("user-id")),
1211 event.get_value("event.user.id")
1212 );
1213 assert_eq!(
1214 Some(Val::String("id:user-id")),
1215 event.get_value("event.sentry_user")
1216 );
1217 assert_eq!(
1218 Some(Val::String("user-seg")),
1219 event.get_value("event.user.segment")
1220 );
1221 assert_eq!(
1222 Some(Val::String("some-transaction")),
1223 event.get_value("event.transaction")
1224 );
1225 assert_eq!(
1226 Some(Val::String("iphone")),
1227 event.get_value("event.contexts.device.name")
1228 );
1229 assert_eq!(
1230 Some(Val::String("iphone-fam")),
1231 event.get_value("event.contexts.device.family")
1232 );
1233 assert_eq!(
1234 Some(Val::String("iOS")),
1235 event.get_value("event.contexts.os.name")
1236 );
1237 assert_eq!(
1238 Some(Val::String("11.4.2")),
1239 event.get_value("event.contexts.os.version")
1240 );
1241 assert_eq!(
1242 Some(Val::String("custom-value")),
1243 event.get_value("event.tags.custom")
1244 );
1245 assert_eq!(None, event.get_value("event.tags.doesntexist"));
1246 assert_eq!(
1247 Some(Val::String("sentry-javascript")),
1248 event.get_value("event.sdk.name")
1249 );
1250 assert_eq!(
1251 Some(Val::String("1.87.0")),
1252 event.get_value("event.sdk.version")
1253 );
1254 assert_eq!(
1255 Some(Val::String("17.4.0")),
1256 event.get_value("event.contexts.os.kernel_version")
1257 );
1258 assert_eq!(
1259 Some(Val::I64(560)),
1260 event.get_value("event.contexts.device.screen_dpi")
1261 );
1262 assert_eq!(
1263 Some(Val::Bool(true)),
1264 event.get_value("event.contexts.device.charging")
1265 );
1266 assert_eq!(
1267 Some(Val::U64(1920)),
1268 event.get_value("event.contexts.device.screen_width_pixels")
1269 );
1270 assert_eq!(
1271 Some(Val::U64(1080)),
1272 event.get_value("event.contexts.device.screen_height_pixels")
1273 );
1274 assert_eq!(
1275 Some(Val::String("US")),
1276 event.get_value("event.contexts.device.locale")
1277 );
1278 assert_eq!(
1279 Some(Val::HexId(HexId(
1280 uuid!("abadcade-feed-dead-beef-baddadfeeded").as_bytes()
1281 ))),
1282 event.get_value("event.contexts.device.uuid")
1283 );
1284 assert_eq!(
1285 Some(Val::String("https://sentry.io")),
1286 event.get_value("event.request.url")
1287 );
1288 assert_eq!(
1289 Some(Val::HexId(HexId(
1290 uuid!("abadcade-feed-dead-beef-8addadfeedaa").as_bytes()
1291 ))),
1292 event.get_value("event.contexts.profile.profile_id")
1293 );
1294 assert_eq!(
1295 Some(Val::String("route")),
1296 event.get_value("event.transaction.source")
1297 );
1298
1299 let mut exceptions = event.get_iter("event.exception.values").unwrap();
1300 let exception = exceptions.next().unwrap();
1301 assert_eq!(
1302 Some(Val::String("canvas.contentDocument")),
1303 exception.get_value("value")
1304 );
1305 assert!(exceptions.next().is_none());
1306
1307 assert_eq!(
1308 Some(Val::String("formatted")),
1309 event.get_value("event.logentry.formatted")
1310 );
1311 assert_eq!(
1312 Some(Val::String("message")),
1313 event.get_value("event.logentry.message")
1314 );
1315 assert_eq!(
1316 Some(Val::String("123")),
1317 event.get_value("event.contexts.monitor.id")
1318 );
1319 assert_eq!(
1320 Some(Val::String("my_monitor")),
1321 event.get_value("event.contexts.monitor.slug")
1322 );
1323 }
1324
1325 #[test]
1326 fn test_field_value_provider_event_empty() {
1327 let event = Event::default();
1328
1329 assert_eq!(None, event.get_value("event.release"));
1330 assert_eq!(None, event.get_value("event.environment"));
1331 assert_eq!(None, event.get_value("event.user.id"));
1332 assert_eq!(None, event.get_value("event.user.segment"));
1333
1334 let event = Event {
1336 user: Annotated::new(User {
1337 ..Default::default()
1338 }),
1339 ..Default::default()
1340 };
1341
1342 assert_eq!(None, event.get_value("event.user.id"));
1343 assert_eq!(None, event.get_value("event.user.segment"));
1344 assert_eq!(None, event.get_value("event.transaction"));
1345 }
1346}