relay_event_schema/protocol/
mechanism.rs

1use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Object, Value};
2
3use crate::processor::ProcessValue;
4
5/// POSIX signal with optional extended data.
6///
7/// Error codes set by Linux system calls and some library functions as specified in ISO C99,
8/// POSIX.1-2001, and POSIX.1-2008. See
9/// [`errno(3)`](https://man7.org/linux/man-pages/man3/errno.3.html) for more information.
10#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
11pub struct CError {
12    /// The error code as specified by ISO C99, POSIX.1-2001 or POSIX.1-2008.
13    pub number: Annotated<i64>,
14
15    /// Optional name of the errno constant.
16    pub name: Annotated<String>,
17}
18
19/// Mach exception information.
20#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
21pub struct MachException {
22    /// The mach exception type.
23    #[metastructure(field = "exception")]
24    pub ty: Annotated<i64>,
25
26    /// The mach exception code.
27    pub code: Annotated<u64>,
28
29    /// The mach exception subcode.
30    pub subcode: Annotated<u64>,
31
32    /// Optional name of the mach exception.
33    pub name: Annotated<String>,
34}
35
36/// NSError informaiton.
37#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
38pub struct NsError {
39    /// The error code.
40    pub code: Annotated<i64>,
41
42    /// A string containing the error domain.
43    pub domain: Annotated<String>,
44}
45
46/// POSIX signal with optional extended data.
47///
48/// On Apple systems, signals also carry a code in addition to the signal number describing the
49/// signal in more detail. On Linux, this code does not exist.
50#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
51pub struct PosixSignal {
52    /// The POSIX signal number.
53    pub number: Annotated<i64>,
54
55    /// An optional signal code present on Apple systems.
56    pub code: Annotated<i64>,
57
58    /// Optional name of the errno constant.
59    pub name: Annotated<String>,
60
61    /// Optional name of the errno constant.
62    pub code_name: Annotated<String>,
63}
64
65/// Operating system or runtime meta information to an exception mechanism.
66///
67/// The mechanism metadata usually carries error codes reported by the runtime or operating system,
68/// along with a platform-dependent interpretation of these codes. SDKs can safely omit code names
69/// and descriptions for well-known error codes, as it will be filled out by Sentry. For
70/// proprietary or vendor-specific error codes, adding these values will give additional
71/// information to the user.
72#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
73pub struct MechanismMeta {
74    /// Optional ISO C standard error code.
75    pub errno: Annotated<CError>,
76
77    /// Information on the POSIX signal.
78    pub signal: Annotated<PosixSignal>,
79
80    /// A Mach Exception on Apple systems comprising a code triple and optional descriptions.
81    pub mach_exception: Annotated<MachException>,
82
83    /// An NSError on Apple systems comprising code and signal.
84    pub ns_error: Annotated<NsError>,
85
86    /// Additional arbitrary fields for forwards compatibility.
87    #[metastructure(additional_properties)]
88    pub other: Object<Value>,
89}
90
91/// The mechanism by which an exception was generated and handled.
92///
93/// The exception mechanism is an optional field residing in the [exception](#typedef-Exception).
94/// It carries additional information about the way the exception was created on the target system.
95/// This includes general exception values obtained from the operating system or runtime APIs, as
96/// well as mechanism-specific values.
97#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
98pub struct Mechanism {
99    /// Mechanism type (required).
100    ///
101    /// Required unique identifier of this mechanism determining rendering and processing of the
102    /// mechanism data.
103    ///
104    /// In the Python SDK this is merely the name of the framework integration that produced the
105    /// exception, while for native it is e.g. `"minidump"` or `"applecrashreport"`.
106    #[metastructure(field = "type", required = true, nonempty = true, max_chars = 128)]
107    pub ty: Annotated<String>,
108
109    /// If this is set then the exception is not a real exception but some
110    /// form of synthetic error for instance from a signal handler, a hard
111    /// segfault or similar where type and value are not useful for grouping
112    /// or display purposes.
113    pub synthetic: Annotated<bool>,
114
115    /// Optional human-readable description of the error mechanism.
116    ///
117    /// May include a possible hint on how to solve this error.
118    #[metastructure(pii = "true", max_chars = 8192, max_chars_allowance = 200)]
119    pub description: Annotated<String>,
120
121    /// Link to online resources describing this error.
122    #[metastructure(
123        required = false,
124        nonempty = true,
125        max_chars = 256,
126        max_chars_allowance = 40
127    )]
128    pub help_link: Annotated<String>,
129
130    /// Flag indicating whether this exception was handled.
131    ///
132    /// This is a best-effort guess at whether the exception was handled by user code or not. For
133    /// example:
134    ///
135    /// - Exceptions leading to a 500 Internal Server Error or to a hard process crash are
136    ///   `handled=false`, as the SDK typically has an integration that automatically captures the
137    ///   error.
138    ///
139    /// - Exceptions captured using `capture_exception` (called from user code) are `handled=true`
140    ///   as the user explicitly captured the exception (and therefore kind of handled it)
141    pub handled: Annotated<bool>,
142
143    /// An optional string value describing the source of the exception.
144    ///
145    /// For chained exceptions, this should contain the platform-specific name of the property or
146    /// attribute (on the parent exception) that this exception was acquired from. In the case of
147    /// an array, it should include the zero-based array index as well.
148    ///
149    /// - Python Examples: `"__context__"`, `"__cause__"`, `"exceptions[0]"`, `"exceptions[1]"`
150    ///
151    /// - .NET Examples:  `"InnerException"`, `"InnerExceptions[0]"`, `"InnerExceptions[1]"`
152    ///
153    /// - JavaScript Examples: `"cause"`, `"errors[0]"`, `"errors[1]"`
154    #[metastructure(
155        required = false,
156        nonempty = true,
157        max_chars = 128,
158        deny_chars = " \t\r\n"
159    )]
160    pub source: Annotated<String>,
161
162    /// An optional boolean value, set `true` when the exception is the platform-specific exception
163    /// group type.  Defaults to `false`.
164    ///
165    /// For example, exceptions of type `ExceptionGroup` (Python), `AggregateException` (.NET), and
166    /// `AggregateError` (JavaScript) should have `"is_exception_group": true`.  Other exceptions
167    /// can omit this field.
168    pub is_exception_group: Annotated<bool>,
169
170    /// An optional numeric value providing an ID for the exception relative to this specific event.
171    /// It is referenced by the `parent_id` to reconstruct the logical tree of exceptions in an
172    /// exception group.
173    ///
174    /// This should contain an unsigned integer value starting with `0` for the last exception in
175    /// the exception values list, then `1` for the previous exception, etc.
176    pub exception_id: Annotated<u64>,
177
178    /// An optional numeric value pointing at the `exception_id` that is the direct parent of this
179    /// exception, used to reconstruct the logical tree of exceptions in an exception group.
180    ///
181    /// The last exception in the exception values list should omit this field, because it is the
182    /// root exception and thus has no parent.
183    pub parent_id: Annotated<u64>,
184
185    /// Arbitrary extra data that might help the user understand the error thrown by this mechanism.
186    #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
187    #[metastructure(skip_serialization = "empty")]
188    pub data: Annotated<Object<Value>>,
189
190    /// Operating system or runtime meta information.
191    #[metastructure(skip_serialization = "empty")]
192    pub meta: Annotated<MechanismMeta>,
193
194    /// Additional arbitrary fields for forwards compatibility.
195    #[metastructure(additional_properties)]
196    pub other: Object<Value>,
197}
198
199impl FromValue for Mechanism {
200    fn from_value(annotated: Annotated<Value>) -> Annotated<Self> {
201        #[derive(Debug, FromValue)]
202        struct NewMechanism {
203            #[metastructure(field = "type", required = true)]
204            pub ty: Annotated<String>,
205            pub synthetic: Annotated<bool>,
206            pub description: Annotated<String>,
207            pub help_link: Annotated<String>,
208            pub handled: Annotated<bool>,
209            pub source: Annotated<String>,
210            pub is_exception_group: Annotated<bool>,
211            pub exception_id: Annotated<u64>,
212            pub parent_id: Annotated<u64>,
213            pub data: Annotated<Object<Value>>,
214            pub meta: Annotated<MechanismMeta>,
215            #[metastructure(additional_properties)]
216            pub other: Object<Value>,
217        }
218
219        #[derive(Debug, FromValue)]
220        struct LegacyPosixSignal {
221            pub signal: Annotated<i64>,
222            pub code: Annotated<i64>,
223            pub name: Annotated<String>,
224            pub code_name: Annotated<String>,
225        }
226
227        #[derive(Debug, FromValue)]
228        struct LegacyMachException {
229            pub exception: Annotated<i64>,
230            pub code: Annotated<u64>,
231            pub subcode: Annotated<u64>,
232            pub exception_name: Annotated<String>,
233        }
234
235        #[derive(Debug, FromValue)]
236        struct LegacyMechanism {
237            posix_signal: Annotated<LegacyPosixSignal>,
238            mach_exception: Annotated<LegacyMachException>,
239            #[metastructure(additional_properties)]
240            pub other: Object<Value>,
241        }
242
243        match annotated {
244            Annotated(Some(Value::Object(object)), meta) => {
245                if object.is_empty() {
246                    Annotated(None, meta)
247                } else if object.contains_key("type") {
248                    let annotated = Annotated(Some(Value::Object(object)), meta);
249                    NewMechanism::from_value(annotated).map_value(|mechanism| Mechanism {
250                        ty: mechanism.ty,
251                        synthetic: mechanism.synthetic,
252                        description: mechanism.description,
253                        help_link: mechanism.help_link,
254                        handled: mechanism.handled,
255                        source: mechanism.source,
256                        is_exception_group: mechanism.is_exception_group,
257                        exception_id: mechanism.exception_id,
258                        parent_id: mechanism.parent_id,
259                        data: mechanism.data,
260                        meta: mechanism.meta,
261                        other: mechanism.other,
262                    })
263                } else {
264                    let annotated = Annotated(Some(Value::Object(object)), meta);
265                    LegacyMechanism::from_value(annotated).map_value(|legacy| Mechanism {
266                        ty: Annotated::new("generic".to_string()),
267                        synthetic: Annotated::empty(),
268                        description: Annotated::empty(),
269                        help_link: Annotated::empty(),
270                        handled: Annotated::empty(),
271                        source: Annotated::empty(),
272                        is_exception_group: Annotated::empty(),
273                        exception_id: Annotated::empty(),
274                        parent_id: Annotated::empty(),
275                        data: Annotated::new(legacy.other),
276                        meta: Annotated::new(MechanismMeta {
277                            errno: Annotated::empty(),
278                            signal: legacy.posix_signal.map_value(|legacy| PosixSignal {
279                                number: legacy.signal,
280                                code: legacy.code,
281                                name: legacy.name,
282                                code_name: legacy.code_name,
283                            }),
284                            mach_exception: legacy.mach_exception.map_value(|legacy| {
285                                MachException {
286                                    ty: legacy.exception,
287                                    code: legacy.code,
288                                    subcode: legacy.subcode,
289                                    name: legacy.exception_name,
290                                }
291                            }),
292                            ns_error: Annotated::empty(),
293                            other: Object::default(),
294                        }),
295                        other: Object::default(),
296                    })
297                }
298            }
299            Annotated(Some(value), mut meta) => {
300                meta.add_error(Error::expected("exception mechanism"));
301                meta.set_original_value(Some(value));
302                Annotated(None, meta)
303            }
304            Annotated(None, meta) => Annotated(None, meta),
305        }
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use relay_protocol::Map;
312    use similar_asserts::assert_eq;
313
314    use super::*;
315
316    #[test]
317    fn test_mechanism_roundtrip() {
318        let json = r#"{
319  "type": "mytype",
320  "description": "mydescription",
321  "help_link": "https://developer.apple.com/library/content/qa/qa1367/_index.html",
322  "handled": false,
323  "source": "errors[0]",
324  "is_exception_group": false,
325  "exception_id": 1,
326  "parent_id": 0,
327  "data": {
328    "relevant_address": "0x1"
329  },
330  "meta": {
331    "errno": {
332      "number": 2,
333      "name": "ENOENT"
334    },
335    "signal": {
336      "number": 11,
337      "code": 0,
338      "name": "SIGSEGV",
339      "code_name": "SEGV_NOOP"
340    },
341    "mach_exception": {
342      "exception": 1,
343      "code": 1,
344      "subcode": 8,
345      "name": "EXC_BAD_ACCESS"
346    },
347    "ns_error": {
348      "code": -42,
349      "domain": "SqlException"
350    },
351    "other": "value"
352  },
353  "other": "value"
354}"#;
355        let mechanism = Annotated::new(Mechanism {
356            ty: Annotated::new("mytype".to_string()),
357            synthetic: Annotated::empty(),
358            description: Annotated::new("mydescription".to_string()),
359            help_link: Annotated::new(
360                "https://developer.apple.com/library/content/qa/qa1367/_index.html".to_string(),
361            ),
362            handled: Annotated::new(false),
363            source: Annotated::new("errors[0]".to_string()),
364            is_exception_group: Annotated::new(false),
365            exception_id: Annotated::new(1),
366            parent_id: Annotated::new(0),
367            data: {
368                let mut map = Map::new();
369                map.insert(
370                    "relevant_address".to_string(),
371                    Annotated::new(Value::String("0x1".to_string())),
372                );
373                Annotated::new(map)
374            },
375            meta: Annotated::new(MechanismMeta {
376                errno: Annotated::new(CError {
377                    number: Annotated::new(2),
378                    name: Annotated::new("ENOENT".to_string()),
379                }),
380                mach_exception: Annotated::new(MachException {
381                    ty: Annotated::new(1),
382                    code: Annotated::new(1),
383                    subcode: Annotated::new(8),
384                    name: Annotated::new("EXC_BAD_ACCESS".to_string()),
385                }),
386                signal: Annotated::new(PosixSignal {
387                    number: Annotated::new(11),
388                    code: Annotated::new(0),
389                    name: Annotated::new("SIGSEGV".to_string()),
390                    code_name: Annotated::new("SEGV_NOOP".to_string()),
391                }),
392                ns_error: Annotated::new(NsError {
393                    code: Annotated::new(-42),
394                    domain: Annotated::new("SqlException".to_string()),
395                }),
396                other: {
397                    let mut map = Object::new();
398                    map.insert(
399                        "other".to_string(),
400                        Annotated::new(Value::String("value".to_string())),
401                    );
402                    map
403                },
404            }),
405            other: {
406                let mut map = Object::new();
407                map.insert(
408                    "other".to_string(),
409                    Annotated::new(Value::String("value".to_string())),
410                );
411                map
412            },
413        });
414
415        assert_eq!(mechanism, Annotated::from_json(json).unwrap());
416        assert_eq!(json, mechanism.to_json_pretty().unwrap());
417    }
418
419    #[test]
420    fn test_mechanism_default_values() {
421        let json = r#"{"type":"mytype"}"#;
422        let mechanism = Annotated::new(Mechanism {
423            ty: Annotated::new("mytype".to_string()),
424            ..Default::default()
425        });
426
427        assert_eq!(mechanism, Annotated::from_json(json).unwrap());
428        assert_eq!(json, mechanism.to_json().unwrap());
429    }
430
431    #[test]
432    fn test_mechanism_empty() {
433        let mechanism = Annotated::<Mechanism>::empty();
434        assert_eq!(mechanism, Annotated::from_json("{}").unwrap());
435    }
436
437    #[test]
438    fn test_mechanism_legacy_conversion() {
439        let input = r#"{
440  "posix_signal": {
441    "name": "SIGSEGV",
442    "code_name": "SEGV_NOOP",
443    "signal": 11,
444    "code": 0
445  },
446  "relevant_address": "0x1",
447  "mach_exception": {
448    "exception": 1,
449    "exception_name": "EXC_BAD_ACCESS",
450    "subcode": 8,
451    "code": 1
452  }
453}"#;
454
455        let output = r#"{
456  "type": "generic",
457  "data": {
458    "relevant_address": "0x1"
459  },
460  "meta": {
461    "signal": {
462      "number": 11,
463      "code": 0,
464      "name": "SIGSEGV",
465      "code_name": "SEGV_NOOP"
466    },
467    "mach_exception": {
468      "exception": 1,
469      "code": 1,
470      "subcode": 8,
471      "name": "EXC_BAD_ACCESS"
472    }
473  }
474}"#;
475        let mechanism = Annotated::new(Mechanism {
476            ty: Annotated::new("generic".to_string()),
477            synthetic: Annotated::empty(),
478            description: Annotated::empty(),
479            help_link: Annotated::empty(),
480            handled: Annotated::empty(),
481            source: Annotated::empty(),
482            is_exception_group: Annotated::empty(),
483            exception_id: Annotated::empty(),
484            parent_id: Annotated::empty(),
485            data: {
486                let mut map = Map::new();
487                map.insert(
488                    "relevant_address".to_string(),
489                    Annotated::new(Value::String("0x1".to_string())),
490                );
491                Annotated::new(map)
492            },
493            meta: Annotated::new(MechanismMeta {
494                errno: Annotated::empty(),
495                mach_exception: Annotated::new(MachException {
496                    ty: Annotated::new(1),
497                    code: Annotated::new(1),
498                    subcode: Annotated::new(8),
499                    name: Annotated::new("EXC_BAD_ACCESS".to_string()),
500                }),
501                signal: Annotated::new(PosixSignal {
502                    number: Annotated::new(11),
503                    code: Annotated::new(0),
504                    name: Annotated::new("SIGSEGV".to_string()),
505                    code_name: Annotated::new("SEGV_NOOP".to_string()),
506                }),
507                ns_error: Annotated::empty(),
508                other: Object::default(),
509            }),
510            other: Object::default(),
511        });
512
513        assert_eq!(mechanism, Annotated::from_json(input).unwrap());
514        assert_eq!(output, mechanism.to_json_pretty().unwrap());
515    }
516}