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" => {
745 (&self.context::<ProfileContext>()?.profile_id.value()?.0).into()
746 }
747 "contexts.device.uuid" => self.context::<DeviceContext>()?.uuid.value()?.into(),
748 "contexts.trace.status" => self
749 .context::<TraceContext>()?
750 .status
751 .value()?
752 .as_str()
753 .into(),
754 "contexts.trace.op" => self.context::<TraceContext>()?.op.as_str()?.into(),
755 "contexts.response.status_code" => self
756 .context::<ResponseContext>()?
757 .status_code
758 .value()?
759 .into(),
760 "contexts.unreal.crash_type" => match self.contexts.value()?.get_key("unreal")? {
761 super::Context::Other(context) => context.get("crash_type")?.value()?.into(),
762 _ => return None,
763 },
764 "contexts.runtime" => self.context::<RuntimeContext>()?.runtime.as_str()?.into(),
765 "contexts.runtime.name" => self.context::<RuntimeContext>()?.name.as_str()?.into(),
766
767 "duration" => {
769 let start = self.start_timestamp.value()?;
770 let end = self.timestamp.value()?;
771 if start <= end && self.ty.value() == Some(&EventType::Transaction) {
772 time::chrono_to_positive_millis(*end - *start).into()
773 } else {
774 return None;
775 }
776 }
777
778 path => {
780 if let Some(rest) = path.strip_prefix("release.") {
781 let release = self.parse_release()?;
782 match rest {
783 "build" => release.build_hash()?.into(),
784 "package" => release.package()?.into(),
785 "version.short" => release.version()?.raw_short().into(),
786 _ => return None,
787 }
788 } else if let Some(rest) = path.strip_prefix("measurements.") {
789 let name = rest.strip_suffix(".value")?;
790 self.measurement(name)?.into()
791 } else if let Some(rest) = path.strip_prefix("breakdowns.") {
792 let (breakdown, measurement) = rest.split_once('.')?;
793 self.breakdown(breakdown, measurement)?.into()
794 } else if let Some(rest) = path.strip_prefix("extra.") {
795 self.extra_at(rest)?.into()
796 } else if let Some(rest) = path.strip_prefix("tags.") {
797 self.tags.value()?.get(rest)?.into()
798 } else if let Some(rest) = path.strip_prefix("request.headers.") {
799 self.request
800 .value()?
801 .headers
802 .value()?
803 .get_header(rest)?
804 .into()
805 } else {
806 return None;
807 }
808 }
809 })
810 }
811
812 fn get_iter(&self, path: &str) -> Option<GetterIter<'_>> {
813 Some(match path.strip_prefix("event.")? {
814 "exception.values" => {
815 GetterIter::new_annotated(self.exceptions.value()?.values.value()?)
816 }
817 _ => return None,
818 })
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use chrono::{TimeZone, Utc};
825 use relay_protocol::{ErrorKind, HexId, Map, Meta};
826 use similar_asserts::assert_eq;
827 use std::collections::BTreeMap;
828 use uuid::uuid;
829
830 use super::*;
831 use crate::protocol::{
832 Headers, IpAddr, JsonLenientString, PairList, TagEntry, TransactionSource,
833 };
834
835 #[test]
836 fn test_event_roundtrip() {
837 let json = r#"{
839 "event_id": "52df9022835246eeb317dbd739ccd059",
840 "level": "debug",
841 "fingerprint": [
842 "myprint"
843 ],
844 "culprit": "myculprit",
845 "transaction": "mytransaction",
846 "logentry": {
847 "formatted": "mymessage"
848 },
849 "logger": "mylogger",
850 "modules": {
851 "mymodule": "1.0.0"
852 },
853 "platform": "myplatform",
854 "timestamp": 946684800.0,
855 "server_name": "myhost",
856 "release": "myrelease",
857 "dist": "mydist",
858 "environment": "myenv",
859 "tags": [
860 [
861 "tag",
862 "value"
863 ]
864 ],
865 "extra": {
866 "extra": "value"
867 },
868 "other": "value",
869 "_meta": {
870 "event_id": {
871 "": {
872 "err": [
873 "invalid_data"
874 ]
875 }
876 }
877 }
878}"#;
879
880 let event = Annotated::new(Event {
881 id: Annotated(
882 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
883 Meta::from_error(ErrorKind::InvalidData),
884 ),
885 level: Annotated::new(Level::Debug),
886 fingerprint: Annotated::new(vec!["myprint".to_string()].into()),
887 culprit: Annotated::new("myculprit".to_string()),
888 transaction: Annotated::new("mytransaction".to_string()),
889 logentry: Annotated::new(LogEntry {
890 formatted: Annotated::new("mymessage".to_string().into()),
891 ..Default::default()
892 }),
893 logger: Annotated::new("mylogger".to_string()),
894 modules: {
895 let mut map = Map::new();
896 map.insert("mymodule".to_string(), Annotated::new("1.0.0".to_string()));
897 Annotated::new(map)
898 },
899 platform: Annotated::new("myplatform".to_string()),
900 timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
901 server_name: Annotated::new("myhost".to_string()),
902 release: Annotated::new("myrelease".to_string().into()),
903 dist: Annotated::new("mydist".to_string()),
904 environment: Annotated::new("myenv".to_string()),
905 tags: {
906 let items = vec![Annotated::new(TagEntry(
907 Annotated::new("tag".to_string()),
908 Annotated::new("value".to_string()),
909 ))];
910 Annotated::new(Tags(items.into()))
911 },
912 extra: {
913 let mut map = Map::new();
914 map.insert(
915 "extra".to_string(),
916 Annotated::new(ExtraValue(Value::String("value".to_string()))),
917 );
918 Annotated::new(map)
919 },
920 other: {
921 let mut map = Map::new();
922 map.insert(
923 "other".to_string(),
924 Annotated::new(Value::String("value".to_string())),
925 );
926 map
927 },
928 ..Default::default()
929 });
930
931 assert_eq!(event, Annotated::from_json(json).unwrap());
932 assert_eq!(json, event.to_json_pretty().unwrap());
933 }
934
935 #[test]
936 fn test_event_default_values() {
937 let json = "{}";
938 let event = Annotated::new(Event::default());
939
940 assert_eq!(event, Annotated::from_json(json).unwrap());
941 assert_eq!(json, event.to_json_pretty().unwrap());
942 }
943
944 #[test]
945 fn test_event_default_values_with_meta() {
946 let json = r#"{
947 "event_id": "52df9022835246eeb317dbd739ccd059",
948 "fingerprint": [
949 "{{ default }}"
950 ],
951 "platform": "other",
952 "_meta": {
953 "event_id": {
954 "": {
955 "err": [
956 "invalid_data"
957 ]
958 }
959 },
960 "fingerprint": {
961 "": {
962 "err": [
963 "invalid_data"
964 ]
965 }
966 },
967 "platform": {
968 "": {
969 "err": [
970 "invalid_data"
971 ]
972 }
973 }
974 }
975}"#;
976
977 let event = Annotated::new(Event {
978 id: Annotated(
979 Some("52df9022-8352-46ee-b317-dbd739ccd059".parse().unwrap()),
980 Meta::from_error(ErrorKind::InvalidData),
981 ),
982 fingerprint: Annotated(
983 Some(vec!["{{ default }}".to_string()].into()),
984 Meta::from_error(ErrorKind::InvalidData),
985 ),
986 platform: Annotated(
987 Some("other".to_string()),
988 Meta::from_error(ErrorKind::InvalidData),
989 ),
990 ..Default::default()
991 });
992
993 assert_eq!(event, Annotated::<Event>::from_json(json).unwrap());
994 assert_eq!(json, event.to_json_pretty().unwrap());
995 }
996
997 #[test]
998 fn test_event_type() {
999 assert_eq!(
1000 EventType::Default,
1001 *Annotated::<EventType>::from_json("\"default\"")
1002 .unwrap()
1003 .value()
1004 .unwrap()
1005 );
1006 }
1007
1008 #[test]
1009 fn test_fingerprint_empty_string() {
1010 let json = r#"{"fingerprint":[""]}"#;
1011 let event = Annotated::new(Event {
1012 fingerprint: Annotated::new(vec!["".to_string()].into()),
1013 ..Default::default()
1014 });
1015
1016 assert_eq!(json, event.to_json().unwrap());
1017 assert_eq!(event, Annotated::from_json(json).unwrap());
1018 }
1019
1020 #[test]
1021 fn test_fingerprint_null_values() {
1022 let input = r#"{"fingerprint":[null]}"#;
1023 let output = r#"{}"#;
1024 let event = Annotated::new(Event {
1025 fingerprint: Annotated::new(vec![].into()),
1026 ..Default::default()
1027 });
1028
1029 assert_eq!(event, Annotated::from_json(input).unwrap());
1030 assert_eq!(output, event.to_json().unwrap());
1031 }
1032
1033 #[test]
1034 fn test_empty_threads() {
1035 let input = r#"{"threads": {}}"#;
1036 let output = r#"{}"#;
1037
1038 let event = Annotated::new(Event::default());
1039
1040 assert_eq!(event, Annotated::from_json(input).unwrap());
1041 assert_eq!(output, event.to_json().unwrap());
1042 }
1043
1044 #[test]
1045 fn test_lenient_release() {
1046 let input = r#"{"release":42}"#;
1047 let output = r#"{"release":"42"}"#;
1048 let event = Annotated::new(Event {
1049 release: Annotated::new("42".to_string().into()),
1050 ..Default::default()
1051 });
1052
1053 assert_eq!(event, Annotated::from_json(input).unwrap());
1054 assert_eq!(output, event.to_json().unwrap());
1055 }
1056
1057 #[test]
1058 fn test_extra_at() {
1059 let json = serde_json::json!({
1060 "extra": {
1061 "a": "string1",
1062 "b": 42,
1063 "c": {
1064 "d": "string2",
1065 "e": null,
1066 },
1067 },
1068 });
1069
1070 let event = Event::from_value(json.into());
1071 let event = event.value().unwrap();
1072
1073 assert_eq!(
1074 Some(&Value::String("string1".to_owned())),
1075 event.extra_at("a")
1076 );
1077 assert_eq!(Some(&Value::I64(42)), event.extra_at("b"));
1078 assert!(matches!(event.extra_at("c"), Some(&Value::Object(_))));
1079 assert_eq!(None, event.extra_at("d"));
1080 assert_eq!(
1081 Some(&Value::String("string2".to_owned())),
1082 event.extra_at("c.d")
1083 );
1084 assert_eq!(None, event.extra_at("c.e"));
1085 assert_eq!(None, event.extra_at("c.f"));
1086 }
1087
1088 #[test]
1089 fn test_scrape_attempts() {
1090 let json = serde_json::json!({
1091 "scraping_attempts": [
1092 {"status": "not_attempted", "url": "http://example.com/embedded.js"},
1093 {"status": "not_attempted", "url": "http://example.com/embedded.js.map"},
1094 ]
1095 });
1096
1097 let event = Event::from_value(json.into());
1098 assert!(!event.value().unwrap().scraping_attempts.meta().has_errors());
1099 }
1100
1101 #[test]
1102 fn test_field_value_provider_event_filled() {
1103 let event = Event {
1104 level: Annotated::new(Level::Info),
1105 release: Annotated::new(LenientString("1.1.1".to_owned())),
1106 environment: Annotated::new("prod".to_owned()),
1107 user: Annotated::new(User {
1108 ip_address: Annotated::new(IpAddr("127.0.0.1".to_owned())),
1109 id: Annotated::new(LenientString("user-id".into())),
1110 segment: Annotated::new("user-seg".into()),
1111 sentry_user: Annotated::new("id:user-id".into()),
1112 ..Default::default()
1113 }),
1114 client_sdk: Annotated::new(ClientSdkInfo {
1115 name: Annotated::new("sentry-javascript".into()),
1116 version: Annotated::new("1.87.0".into()),
1117 ..Default::default()
1118 }),
1119 exceptions: Annotated::new(Values {
1120 values: Annotated::new(vec![Annotated::new(Exception {
1121 value: Annotated::new(JsonLenientString::from(
1122 "canvas.contentDocument".to_owned(),
1123 )),
1124 ..Default::default()
1125 })]),
1126 ..Default::default()
1127 }),
1128 logentry: Annotated::new(LogEntry {
1129 formatted: Annotated::new("formatted".to_string().into()),
1130 message: Annotated::new("message".to_string().into()),
1131 ..Default::default()
1132 }),
1133 request: Annotated::new(Request {
1134 headers: Annotated::new(Headers(PairList(vec![Annotated::new((
1135 Annotated::new("user-agent".into()),
1136 Annotated::new("Slurp".into()),
1137 ))]))),
1138 url: Annotated::new("https://sentry.io".into()),
1139 ..Default::default()
1140 }),
1141 transaction: Annotated::new("some-transaction".into()),
1142 transaction_info: Annotated::new(TransactionInfo {
1143 source: Annotated::new(TransactionSource::Route),
1144 ..Default::default()
1145 }),
1146 tags: {
1147 let items = vec![Annotated::new(TagEntry(
1148 Annotated::new("custom".to_string()),
1149 Annotated::new("custom-value".to_string()),
1150 ))];
1151 Annotated::new(Tags(items.into()))
1152 },
1153 contexts: Annotated::new({
1154 let mut contexts = Contexts::new();
1155 contexts.add(DeviceContext {
1156 name: Annotated::new("iphone".to_string()),
1157 family: Annotated::new("iphone-fam".to_string()),
1158 model: Annotated::new("iphone7,3".to_string()),
1159 screen_dpi: Annotated::new(560),
1160 screen_width_pixels: Annotated::new(1920),
1161 screen_height_pixels: Annotated::new(1080),
1162 locale: Annotated::new("US".into()),
1163 uuid: Annotated::new(uuid!("abadcade-feed-dead-beef-baddadfeeded")),
1164 charging: Annotated::new(true),
1165 ..DeviceContext::default()
1166 });
1167 contexts.add(OsContext {
1168 name: Annotated::new("iOS".to_string()),
1169 version: Annotated::new("11.4.2".to_string()),
1170 kernel_version: Annotated::new("17.4.0".to_string()),
1171 ..OsContext::default()
1172 });
1173 contexts.add(ProfileContext {
1174 profile_id: Annotated::new(EventId(uuid!(
1175 "abadcade-feed-dead-beef-8addadfeedaa"
1176 ))),
1177 ..ProfileContext::default()
1178 });
1179 let mut monitor_context_fields = BTreeMap::new();
1180 monitor_context_fields.insert(
1181 "id".to_string(),
1182 Annotated::new(Value::String("123".to_string())),
1183 );
1184 monitor_context_fields.insert(
1185 "slug".to_string(),
1186 Annotated::new(Value::String("my_monitor".to_string())),
1187 );
1188 contexts.add(MonitorContext(monitor_context_fields));
1189 contexts
1190 }),
1191 ..Default::default()
1192 };
1193
1194 assert_eq!(Some(Val::String("info")), event.get_value("event.level"));
1195
1196 assert_eq!(Some(Val::String("1.1.1")), event.get_value("event.release"));
1197 assert_eq!(
1198 Some(Val::String("prod")),
1199 event.get_value("event.environment")
1200 );
1201 assert_eq!(
1202 Some(Val::String("user-id")),
1203 event.get_value("event.user.id")
1204 );
1205 assert_eq!(
1206 Some(Val::String("id:user-id")),
1207 event.get_value("event.sentry_user")
1208 );
1209 assert_eq!(
1210 Some(Val::String("user-seg")),
1211 event.get_value("event.user.segment")
1212 );
1213 assert_eq!(
1214 Some(Val::String("some-transaction")),
1215 event.get_value("event.transaction")
1216 );
1217 assert_eq!(
1218 Some(Val::String("iphone")),
1219 event.get_value("event.contexts.device.name")
1220 );
1221 assert_eq!(
1222 Some(Val::String("iphone-fam")),
1223 event.get_value("event.contexts.device.family")
1224 );
1225 assert_eq!(
1226 Some(Val::String("iOS")),
1227 event.get_value("event.contexts.os.name")
1228 );
1229 assert_eq!(
1230 Some(Val::String("11.4.2")),
1231 event.get_value("event.contexts.os.version")
1232 );
1233 assert_eq!(
1234 Some(Val::String("custom-value")),
1235 event.get_value("event.tags.custom")
1236 );
1237 assert_eq!(None, event.get_value("event.tags.doesntexist"));
1238 assert_eq!(
1239 Some(Val::String("sentry-javascript")),
1240 event.get_value("event.sdk.name")
1241 );
1242 assert_eq!(
1243 Some(Val::String("1.87.0")),
1244 event.get_value("event.sdk.version")
1245 );
1246 assert_eq!(
1247 Some(Val::String("17.4.0")),
1248 event.get_value("event.contexts.os.kernel_version")
1249 );
1250 assert_eq!(
1251 Some(Val::I64(560)),
1252 event.get_value("event.contexts.device.screen_dpi")
1253 );
1254 assert_eq!(
1255 Some(Val::Bool(true)),
1256 event.get_value("event.contexts.device.charging")
1257 );
1258 assert_eq!(
1259 Some(Val::U64(1920)),
1260 event.get_value("event.contexts.device.screen_width_pixels")
1261 );
1262 assert_eq!(
1263 Some(Val::U64(1080)),
1264 event.get_value("event.contexts.device.screen_height_pixels")
1265 );
1266 assert_eq!(
1267 Some(Val::String("US")),
1268 event.get_value("event.contexts.device.locale")
1269 );
1270 assert_eq!(
1271 Some(Val::HexId(HexId(
1272 uuid!("abadcade-feed-dead-beef-baddadfeeded").as_bytes()
1273 ))),
1274 event.get_value("event.contexts.device.uuid")
1275 );
1276 assert_eq!(
1277 Some(Val::String("https://sentry.io")),
1278 event.get_value("event.request.url")
1279 );
1280 assert_eq!(
1281 Some(Val::HexId(HexId(
1282 uuid!("abadcade-feed-dead-beef-8addadfeedaa").as_bytes()
1283 ))),
1284 event.get_value("event.contexts.profile.profile_id")
1285 );
1286 assert_eq!(
1287 Some(Val::String("route")),
1288 event.get_value("event.transaction.source")
1289 );
1290
1291 let mut exceptions = event.get_iter("event.exception.values").unwrap();
1292 let exception = exceptions.next().unwrap();
1293 assert_eq!(
1294 Some(Val::String("canvas.contentDocument")),
1295 exception.get_value("value")
1296 );
1297 assert!(exceptions.next().is_none());
1298
1299 assert_eq!(
1300 Some(Val::String("formatted")),
1301 event.get_value("event.logentry.formatted")
1302 );
1303 assert_eq!(
1304 Some(Val::String("message")),
1305 event.get_value("event.logentry.message")
1306 );
1307 assert_eq!(
1308 Some(Val::String("123")),
1309 event.get_value("event.contexts.monitor.id")
1310 );
1311 assert_eq!(
1312 Some(Val::String("my_monitor")),
1313 event.get_value("event.contexts.monitor.slug")
1314 );
1315 }
1316
1317 #[test]
1318 fn test_field_value_provider_event_empty() {
1319 let event = Event::default();
1320
1321 assert_eq!(None, event.get_value("event.release"));
1322 assert_eq!(None, event.get_value("event.environment"));
1323 assert_eq!(None, event.get_value("event.user.id"));
1324 assert_eq!(None, event.get_value("event.user.segment"));
1325
1326 let event = Event {
1328 user: Annotated::new(User {
1329 ..Default::default()
1330 }),
1331 ..Default::default()
1332 };
1333
1334 assert_eq!(None, event.get_value("event.user.id"));
1335 assert_eq!(None, event.get_value("event.user.segment"));
1336 assert_eq!(None, event.get_value("event.transaction"));
1337 }
1338}