relay_event_schema/protocol/
exception.rs

1use 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/// A single exception.
7///
8/// Multiple values inside of an [event](#typedef-Event) represent chained exceptions and should be sorted oldest to newest. For example, consider this Python code snippet:
9///
10/// ```python
11/// try:
12///     raise Exception("random boring invariant was not met!")
13/// except Exception as e:
14///     raise ValueError("something went wrong, help!") from e
15/// ```
16///
17/// `Exception` would be described first in the values list, followed by a description of `ValueError`:
18///
19/// ```json
20/// {
21///   "exception": {
22///     "values": [
23///       {"type": "Exception": "value": "random boring invariant was not met!"},
24///       {"type": "ValueError", "value": "something went wrong, help!"},
25///     ]
26///   }
27/// }
28/// ```
29#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
30#[metastructure(process_func = "process_exception", value_type = "Exception")]
31pub struct Exception {
32    /// Exception type, e.g. `ValueError`.
33    ///
34    /// At least one of `type` or `value` is required, otherwise the exception is discarded.
35    // (note: requirement checked in checked in StoreNormalizeProcessor)
36    #[metastructure(field = "type", max_chars = 256, max_chars_allowance = 20)]
37    pub ty: Annotated<String>,
38
39    /// Human readable display value.
40    ///
41    /// At least one of `type` or `value` is required, otherwise the exception is discarded.
42    #[metastructure(max_chars = 8192, max_chars_allowance = 200, pii = "true")]
43    pub value: Annotated<JsonLenientString>,
44
45    /// The optional module, or package which the exception type lives in.
46    #[metastructure(max_chars = 256, max_chars_allowance = 20)]
47    pub module: Annotated<String>,
48
49    /// Stack trace containing frames of this exception.
50    #[metastructure(
51        legacy_alias = "sentry.interfaces.Stacktrace",
52        skip_serialization = "empty"
53    )]
54    pub stacktrace: Annotated<Stacktrace>,
55
56    /// Optional unprocessed stack trace.
57    #[metastructure(skip_serialization = "empty", omit_from_schema)]
58    pub raw_stacktrace: Annotated<RawStacktrace>,
59
60    /// An optional value that refers to a [thread](#typedef-Thread).
61    #[metastructure(max_chars = 128)]
62    pub thread_id: Annotated<ThreadId>,
63
64    /// Mechanism by which this exception was generated and handled.
65    pub mechanism: Annotated<Mechanism>,
66
67    /// Additional arbitrary fields for forwards compatibility.
68    #[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        // stack traces and mechanism are tested separately
92        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}