relay_event_schema/protocol/
exception.rs1use relay_protocol::{Annotated, Empty, FromValue, Getter, IntoValue, Object, Val, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::{JsonLenientString, Mechanism, RawStacktrace, Stacktrace, ThreadId};
5
6#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
30#[metastructure(process_func = "process_exception", value_type = "Exception")]
31pub struct Exception {
32 #[metastructure(field = "type", max_chars = 256, max_chars_allowance = 20)]
37 pub ty: Annotated<String>,
38
39 #[metastructure(max_chars = 8192, max_chars_allowance = 200, pii = "true")]
43 pub value: Annotated<JsonLenientString>,
44
45 #[metastructure(max_chars = 256, max_chars_allowance = 20)]
47 pub module: Annotated<String>,
48
49 #[metastructure(
51 legacy_alias = "sentry.interfaces.Stacktrace",
52 skip_serialization = "empty"
53 )]
54 pub stacktrace: Annotated<Stacktrace>,
55
56 #[metastructure(skip_serialization = "empty", omit_from_schema)]
58 pub raw_stacktrace: Annotated<RawStacktrace>,
59
60 #[metastructure(max_chars = 128)]
62 pub thread_id: Annotated<ThreadId>,
63
64 pub mechanism: Annotated<Mechanism>,
66
67 #[metastructure(additional_properties)]
69 pub other: Object<Value>,
70}
71
72impl Getter for Exception {
73 fn get_value(&self, path: &str) -> Option<Val<'_>> {
74 Some(match path {
75 "ty" => self.ty.as_str()?.into(),
76 "value" => self.value.as_str()?.into(),
77 _ => return None,
78 })
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use relay_protocol::Map;
85 use similar_asserts::assert_eq;
86
87 use super::*;
88
89 #[test]
90 fn test_exception_roundtrip() {
91 let json = r#"{
93 "type": "mytype",
94 "value": "myvalue",
95 "module": "mymodule",
96 "thread_id": 42,
97 "other": "value"
98}"#;
99 let exception = Annotated::new(Exception {
100 ty: Annotated::new("mytype".to_string()),
101 value: Annotated::new("myvalue".to_string().into()),
102 module: Annotated::new("mymodule".to_string()),
103 thread_id: Annotated::new(ThreadId::Int(42)),
104 other: {
105 let mut map = Map::new();
106 map.insert(
107 "other".to_string(),
108 Annotated::new(Value::String("value".to_string())),
109 );
110 map
111 },
112 ..Default::default()
113 });
114
115 assert_eq!(exception, Annotated::from_json(json).unwrap());
116 assert_eq!(json, exception.to_json_pretty().unwrap());
117 }
118
119 #[test]
120 fn test_exception_default_values() {
121 let json = r#"{"type":"mytype"}"#;
122 let exception = Annotated::new(Exception {
123 ty: Annotated::new("mytype".to_string()),
124 ..Default::default()
125 });
126
127 assert_eq!(exception, Annotated::from_json(json).unwrap());
128 assert_eq!(json, exception.to_json().unwrap());
129 }
130
131 #[test]
132 fn test_exception_empty_fields() {
133 let json = r#"{"type":"","value":""}"#;
134 let exception = Annotated::new(Exception {
135 ty: Annotated::new("".to_string()),
136 value: Annotated::new("".to_string().into()),
137 ..Default::default()
138 });
139
140 assert_eq!(exception, Annotated::from_json(json).unwrap());
141 assert_eq!(json, exception.to_json().unwrap());
142 }
143
144 #[test]
145 fn test_coerces_object_value_to_string() {
146 let input = r#"{"value":{"unauthorized":true}}"#;
147 let output = r#"{"value":"{\"unauthorized\":true}"}"#;
148
149 let exception = Annotated::new(Exception {
150 value: Annotated::new(r#"{"unauthorized":true}"#.to_string().into()),
151 ..Default::default()
152 });
153
154 assert_eq!(exception, Annotated::from_json(input).unwrap());
155 assert_eq!(output, exception.to_json().unwrap());
156 }
157
158 #[test]
159 fn test_explicit_none() {
160 let json = r#"{
161 "value": null,
162 "type": "ZeroDivisionError"
163}"#;
164
165 let exception = Annotated::new(Exception {
166 ty: Annotated::new("ZeroDivisionError".to_string()),
167 ..Default::default()
168 });
169
170 assert_eq!(exception, Annotated::from_json(json).unwrap());
171 assert_eq!(
172 r#"{"type":"ZeroDivisionError"}"#,
173 exception.to_json().unwrap()
174 );
175 }
176}