1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use relay_protocol::{Annotated, Empty, FromValue, Getter, IntoValue, Object, Val, Value};

use crate::processor::ProcessValue;
use crate::protocol::{JsonLenientString, Mechanism, RawStacktrace, Stacktrace, ThreadId};

/// A single exception.
///
/// 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:
///
/// ```python
/// try:
///     raise Exception("random boring invariant was not met!")
/// except Exception as e:
///     raise ValueError("something went wrong, help!") from e
/// ```
///
/// `Exception` would be described first in the values list, followed by a description of `ValueError`:
///
/// ```json
/// {
///   "exception": {
///     "values": [
///       {"type": "Exception": "value": "random boring invariant was not met!"},
///       {"type": "ValueError", "value": "something went wrong, help!"},
///     ]
///   }
/// }
/// ```
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
#[metastructure(process_func = "process_exception", value_type = "Exception")]
pub struct Exception {
    /// Exception type, e.g. `ValueError`.
    ///
    /// At least one of `type` or `value` is required, otherwise the exception is discarded.
    // (note: requirement checked in checked in StoreNormalizeProcessor)
    #[metastructure(field = "type", max_chars = 256, max_chars_allowance = 20)]
    pub ty: Annotated<String>,

    /// Human readable display value.
    ///
    /// At least one of `type` or `value` is required, otherwise the exception is discarded.
    #[metastructure(max_chars = 8192, max_chars_allowance = 200, pii = "true")]
    pub value: Annotated<JsonLenientString>,

    /// The optional module, or package which the exception type lives in.
    #[metastructure(max_chars = 256, max_chars_allowance = 20)]
    pub module: Annotated<String>,

    /// Stack trace containing frames of this exception.
    #[metastructure(
        legacy_alias = "sentry.interfaces.Stacktrace",
        skip_serialization = "empty"
    )]
    pub stacktrace: Annotated<Stacktrace>,

    /// Optional unprocessed stack trace.
    #[metastructure(skip_serialization = "empty", omit_from_schema)]
    pub raw_stacktrace: Annotated<RawStacktrace>,

    /// An optional value that refers to a [thread](#typedef-Thread).
    #[metastructure(max_chars = 128)]
    pub thread_id: Annotated<ThreadId>,

    /// Mechanism by which this exception was generated and handled.
    pub mechanism: Annotated<Mechanism>,

    /// Additional arbitrary fields for forwards compatibility.
    #[metastructure(additional_properties)]
    pub other: Object<Value>,
}

impl Getter for Exception {
    fn get_value(&self, path: &str) -> Option<Val<'_>> {
        Some(match path {
            "ty" => self.ty.as_str()?.into(),
            "value" => self.value.as_str()?.into(),
            _ => return None,
        })
    }
}

#[cfg(test)]
mod tests {
    use relay_protocol::Map;
    use similar_asserts::assert_eq;

    use super::*;

    #[test]
    fn test_exception_roundtrip() {
        // stack traces and mechanism are tested separately
        let json = r#"{
  "type": "mytype",
  "value": "myvalue",
  "module": "mymodule",
  "thread_id": 42,
  "other": "value"
}"#;
        let exception = Annotated::new(Exception {
            ty: Annotated::new("mytype".to_string()),
            value: Annotated::new("myvalue".to_string().into()),
            module: Annotated::new("mymodule".to_string()),
            thread_id: Annotated::new(ThreadId::Int(42)),
            other: {
                let mut map = Map::new();
                map.insert(
                    "other".to_string(),
                    Annotated::new(Value::String("value".to_string())),
                );
                map
            },
            ..Default::default()
        });

        assert_eq!(exception, Annotated::from_json(json).unwrap());
        assert_eq!(json, exception.to_json_pretty().unwrap());
    }

    #[test]
    fn test_exception_default_values() {
        let json = r#"{"type":"mytype"}"#;
        let exception = Annotated::new(Exception {
            ty: Annotated::new("mytype".to_string()),
            ..Default::default()
        });

        assert_eq!(exception, Annotated::from_json(json).unwrap());
        assert_eq!(json, exception.to_json().unwrap());
    }

    #[test]
    fn test_exception_empty_fields() {
        let json = r#"{"type":"","value":""}"#;
        let exception = Annotated::new(Exception {
            ty: Annotated::new("".to_string()),
            value: Annotated::new("".to_string().into()),
            ..Default::default()
        });

        assert_eq!(exception, Annotated::from_json(json).unwrap());
        assert_eq!(json, exception.to_json().unwrap());
    }

    #[test]
    fn test_coerces_object_value_to_string() {
        let input = r#"{"value":{"unauthorized":true}}"#;
        let output = r#"{"value":"{\"unauthorized\":true}"}"#;

        let exception = Annotated::new(Exception {
            value: Annotated::new(r#"{"unauthorized":true}"#.to_string().into()),
            ..Default::default()
        });

        assert_eq!(exception, Annotated::from_json(input).unwrap());
        assert_eq!(output, exception.to_json().unwrap());
    }

    #[test]
    fn test_explicit_none() {
        let json = r#"{
  "value": null,
  "type": "ZeroDivisionError"
}"#;

        let exception = Annotated::new(Exception {
            ty: Annotated::new("ZeroDivisionError".to_string()),
            ..Default::default()
        });

        assert_eq!(exception, Annotated::from_json(json).unwrap());
        assert_eq!(
            r#"{"type":"ZeroDivisionError"}"#,
            exception.to_json().unwrap()
        );
    }
}