relay_event_schema/protocol/
stacktrace.rs

1use std::convert::Infallible;
2use std::fmt;
3use std::ops::{Deref, DerefMut};
4use std::str::FromStr;
5
6use relay_protocol::{
7    Annotated, Array, Empty, ErrorKind, FromValue, IntoValue, Object, SkipSerialization, Value,
8};
9use serde::{Deserialize, Serialize};
10
11use crate::processor::ProcessValue;
12use crate::protocol::{Addr, LockReason, NativeImagePath, RegVal};
13
14/// Holds information about a single stacktrace frame.
15///
16/// Each object should contain **at least** a `filename`, `function` or `instruction_addr`
17/// attribute. All values are optional, but recommended.
18#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
19#[metastructure(process_func = "process_frame", value_type = "Frame")]
20pub struct Frame {
21    /// Name of the frame's function. This might include the name of a class.
22    ///
23    /// This function name may be shortened or demangled. If not, Sentry will demangle and shorten
24    /// it for some platforms. The original function name will be stored in `raw_function`.
25    #[metastructure(max_chars = 512, max_chars_allowance = 20)]
26    #[metastructure(skip_serialization = "empty")]
27    pub function: Annotated<String>,
28
29    /// A raw (but potentially truncated) function value.
30    ///
31    /// The original function name, if the function name is shortened or demangled. Sentry shows the
32    /// raw function when clicking on the shortened one in the UI.
33    ///
34    /// If this has the same value as `function` it's best to be omitted.  This exists because on
35    /// many platforms the function itself contains additional information like overload specifies
36    /// or a lot of generics which can make it exceed the maximum limit we provide for the field.
37    /// In those cases then we cannot reliably trim down the function any more at a later point
38    /// because the more valuable information has been removed.
39    ///
40    /// The logic to be applied is that an intelligently trimmed function name should be stored in
41    /// `function` and the value before trimming is stored in this field instead.  However also this
42    /// field will be capped at 512 characters at the moment which often means that not the entire
43    /// original value can be stored.
44    #[metastructure(max_chars = 512, max_chars_allowance = 20)]
45    #[metastructure(skip_serialization = "empty")]
46    pub raw_function: Annotated<String>,
47
48    /// Potentially mangled name of the symbol as it appears in an executable.
49    ///
50    /// This is different from a function name by generally being the mangled
51    /// name that appears natively in the binary.  This is relevant for languages
52    /// like Swift, C++ or Rust.
53    // XXX(markus): How is this different from just storing the mangled function name in
54    // `function`?
55    #[metastructure(max_chars = 512)]
56    pub symbol: Annotated<String>,
57
58    /// Name of the module the frame is contained in.
59    ///
60    /// Note that this might also include a class name if that is something the
61    /// language natively considers to be part of the stack (for instance in Java).
62    #[metastructure(skip_serialization = "empty", pii = "maybe")]
63    // TODO: Cap? This can be a FS path or a dotted path
64    pub module: Annotated<String>,
65
66    /// Name of the package that contains the frame.
67    ///
68    /// For instance this can be a dylib for native languages, the name of the jar
69    /// or .NET assembly.
70    #[metastructure(skip_serialization = "empty")]
71    // TODO: Cap? This can be a FS path or a dotted path
72    pub package: Annotated<String>,
73
74    /// The source file name (basename only).
75    #[metastructure(max_chars = 256, max_chars_allowance = 40)]
76    #[metastructure(skip_serialization = "empty", pii = "maybe")]
77    pub filename: Annotated<NativeImagePath>,
78
79    /// Absolute path to the source file.
80    #[metastructure(max_chars = 256, max_chars_allowance = 40)]
81    #[metastructure(skip_serialization = "empty", pii = "maybe")]
82    pub abs_path: Annotated<NativeImagePath>,
83
84    /// Line number within the source file, starting at 1.
85    #[metastructure(skip_serialization = "null")]
86    pub lineno: Annotated<u64>,
87
88    /// Column number within the source file, starting at 1.
89    #[metastructure(skip_serialization = "null")]
90    pub colno: Annotated<u64>,
91
92    /// Which platform this frame is from.
93    ///
94    /// This can override the platform for a single frame. Otherwise, the platform of the event is
95    /// assumed. This can be used for multi-platform stack traces, such as in React Native.
96    #[metastructure(skip_serialization = "empty")]
97    pub platform: Annotated<String>,
98
99    /// Source code leading up to `lineno`.
100    #[metastructure(skip_serialization = "empty")]
101    pub pre_context: Annotated<Array<String>>,
102
103    /// Source code of the current line (`lineno`).
104    #[metastructure(skip_serialization = "null")]
105    pub context_line: Annotated<String>,
106
107    /// Source code of the lines after `lineno`.
108    #[metastructure(skip_serialization = "empty")]
109    pub post_context: Annotated<Array<String>>,
110
111    /// Override whether this frame should be considered part of application code, or part of
112    /// libraries/frameworks/dependencies.
113    ///
114    /// Setting this attribute to `false` causes the frame to be hidden/collapsed by default and
115    /// mostly ignored during issue grouping.
116    #[metastructure(skip_serialization = "null")]
117    pub in_app: Annotated<bool>,
118
119    /// Mapping of local variables and expression names that were available in this frame.
120    // XXX: Probably want to trim per-var => new bag size?
121    #[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
122    pub vars: Annotated<FrameVars>,
123
124    /// Auxiliary information about the frame that is platform specific.
125    #[metastructure(omit_from_schema)]
126    #[metastructure(skip_serialization = "empty")]
127    pub data: Annotated<FrameData>,
128
129    /// (C/C++/Native) Start address of the containing code module (image).
130    #[metastructure(skip_serialization = "null")]
131    pub image_addr: Annotated<Addr>,
132
133    /// (C/C++/Native) An optional instruction address for symbolication.
134    ///
135    /// This should be a string with a hexadecimal number that includes a 0x prefix.
136    /// If this is set and a known image is defined in the
137    /// [Debug Meta Interface]({%- link _documentation/development/sdk-dev/event-payloads/debugmeta.md -%}),
138    /// then symbolication can take place.
139    #[metastructure(skip_serialization = "null")]
140    pub instruction_addr: Annotated<Addr>,
141
142    /// Defines the addressing mode for addresses.
143    ///
144    /// This can be:
145    /// - `"abs"` (the default): `instruction_addr` is absolute.
146    /// - `"rel:$idx"`: `instruction_addr` is relative to the `debug_meta.image` identified by its index in the list.
147    /// - `"rel:$uuid"`: `instruction_addr` is relative to the `debug_meta.image` identified by its `debug_id`.
148    ///
149    /// If one of the `"rel:XXX"` variants is given together with `function_id`, the `instruction_addr` is relative
150    /// to the uniquely identified function in the references `debug_meta.image`.
151    #[metastructure(skip_serialization = "empty")]
152    pub addr_mode: Annotated<String>,
153
154    /// (.NET) The function id / index that uniquely identifies a function inside a module.
155    ///
156    /// This is the `MetadataToken` of a .NET `MethodBase`.
157    #[metastructure(skip_serialization = "empty")]
158    pub function_id: Annotated<Addr>,
159
160    /// (C/C++/Native) Start address of the frame's function.
161    ///
162    /// We use the instruction address for symbolication, but this can be used to calculate
163    /// an instruction offset automatically.
164    #[metastructure(skip_serialization = "null")]
165    pub symbol_addr: Annotated<Addr>,
166
167    /// (C/C++/Native) Used for native crashes to indicate how much we can "trust" the instruction_addr
168    #[metastructure(max_chars = 128)]
169    #[metastructure(omit_from_schema)]
170    pub trust: Annotated<String>,
171
172    /// The language of the frame if it overrides the stacktrace language.
173    #[metastructure(max_chars = 128)]
174    #[metastructure(omit_from_schema)]
175    pub lang: Annotated<String>,
176
177    /// Marks this frame as the bottom of a chained stack trace.
178    ///
179    /// Stack traces from asynchronous code consist of several sub traces that are chained together
180    /// into one large list. This flag indicates the root function of a chained stack trace.
181    /// Depending on the runtime and thread, this is either the `main` function or a thread base
182    /// stub.
183    ///
184    /// This field should only be specified when true.
185    #[metastructure(skip_serialization = "null")]
186    pub stack_start: Annotated<bool>,
187
188    /// A possible lock (java monitor object) held by this frame.
189    #[metastructure(skip_serialization = "null")]
190    pub lock: Annotated<LockReason>,
191
192    /// Additional arbitrary fields for forwards compatibility.
193    #[metastructure(additional_properties)]
194    pub other: Object<Value>,
195}
196
197/// Frame local variables.
198#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
199pub struct FrameVars(#[metastructure(skip_serialization = "empty")] pub Object<Value>);
200
201/// Additional frame data information.
202///
203/// This value is set by the server and should not be set by the SDK.
204#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
205pub struct FrameData {
206    /// A reference to the sourcemap used.
207    #[metastructure(max_chars = 256, max_chars_allowance = 40)]
208    sourcemap: Annotated<String>,
209    /// The original function name before it was resolved.
210    #[metastructure(max_chars = 256, max_chars_allowance = 20)]
211    orig_function: Annotated<String>,
212    /// The original minified filename.
213    #[metastructure(max_chars = 256, max_chars_allowance = 40)]
214    orig_filename: Annotated<String>,
215    /// The original line number.
216    orig_lineno: Annotated<u64>,
217    /// The original column number.
218    orig_colno: Annotated<u64>,
219    /// The original value of the in_app flag before grouping enhancers ran.
220    ///
221    /// Because we need to handle more cases the following values are used:
222    ///
223    /// - missing / `null`: information not available
224    /// - `-1`: in_app was set to `null`
225    /// - `0`: in_app was set to `false`
226    /// - `1`: in_app was set to `true`
227    orig_in_app: Annotated<i64>,
228    /// Additional keys not handled by this protocol.
229    #[metastructure(additional_properties)]
230    pub other: Object<Value>,
231}
232
233impl From<Object<Value>> for FrameVars {
234    fn from(value: Object<Value>) -> Self {
235        FrameVars(value)
236    }
237}
238
239impl FromValue for FrameVars {
240    fn from_value(mut value: Annotated<Value>) -> Annotated<FrameVars> {
241        value = value.map_value(|value| {
242            if let Value::Array(value) = value {
243                Value::Object(
244                    value
245                        .into_iter()
246                        .enumerate()
247                        .map(|(i, v)| (i.to_string(), v))
248                        .collect(),
249                )
250            } else {
251                value
252            }
253        });
254
255        FromValue::from_value(value).map_value(FrameVars)
256    }
257}
258
259/// A stack trace of a single thread.
260///
261/// A stack trace contains a list of frames, each with various bits (most optional) describing the
262/// context of that frame. Frames should be sorted from oldest to newest.
263///
264/// For the given example program written in Python:
265///
266/// ```python
267/// def foo():
268///     my_var = 'foo'
269///     raise ValueError()
270///
271/// def main():
272///     foo()
273/// ```
274///
275/// A minimalistic stack trace for the above program in the correct order:
276///
277/// ```json
278/// {
279///   "frames": [
280///     {"function": "main"},
281///     {"function": "foo"}
282///   ]
283/// }
284/// ```
285///
286/// The top frame fully symbolicated with five lines of source context:
287///
288/// ```json
289/// {
290///   "frames": [{
291///     "in_app": true,
292///     "function": "myfunction",
293///     "abs_path": "/real/file/name.py",
294///     "filename": "file/name.py",
295///     "lineno": 3,
296///     "vars": {
297///       "my_var": "'value'"
298///     },
299///     "pre_context": [
300///       "def foo():",
301///       "  my_var = 'foo'",
302///     ],
303///     "context_line": "  raise ValueError()",
304///     "post_context": [
305///       "",
306///       "def main():"
307///     ],
308///   }]
309/// }
310/// ```
311///
312/// A minimal native stack trace with register values. Note that the `package` event attribute must
313/// be "native" for these frames to be symbolicated.
314///
315/// ```json
316/// {
317///   "frames": [
318///     {"instruction_addr": "0x7fff5bf3456c"},
319///     {"instruction_addr": "0x7fff5bf346c0"},
320///   ],
321///   "registers": {
322///     "rip": "0x00007ff6eef54be2",
323///     "rsp": "0x0000003b710cd9e0"
324///   }
325/// }
326/// ```
327#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
328#[metastructure(process_func = "process_raw_stacktrace", value_type = "Stacktrace")]
329pub struct RawStacktrace {
330    /// Required. A non-empty list of stack frames. The list is ordered from caller to callee, or
331    /// oldest to youngest. The last frame is the one creating the exception.
332    #[metastructure(required = true, nonempty = true, skip_serialization = "empty")]
333    pub frames: Annotated<Array<Frame>>,
334
335    /// Register values of the thread (top frame).
336    ///
337    /// A map of register names and their values. The values should contain the actual register
338    /// values of the thread, thus mapping to the last frame in the list.
339    pub registers: Annotated<Object<RegVal>>,
340
341    /// Optional. A flag that indicates if, and how, `instruction_addr` values need to be adjusted
342    /// before they are symbolicated.
343    #[metastructure(skip_serialization = "null")]
344    pub instruction_addr_adjustment: Annotated<InstructionAddrAdjustment>,
345
346    /// The language of the stacktrace.
347    #[metastructure(max_chars = 128)]
348    pub lang: Annotated<String>,
349
350    /// Indicates that this stack trace is a snapshot triggered by an external signal.
351    ///
352    /// If this field is `false`, then the stack trace points to the code that caused this stack
353    /// trace to be created. This can be the location of a raised exception, as well as an exception
354    /// or signal handler.
355    ///
356    /// If this field is `true`, then the stack trace was captured as part of creating an unrelated
357    /// event. For example, a thread other than the crashing thread, or a stack trace computed as a
358    /// result of an external kill signal.
359    pub snapshot: Annotated<bool>,
360
361    /// Additional arbitrary fields for forwards compatibility.
362    #[metastructure(additional_properties)]
363    pub other: Object<Value>,
364}
365
366/// Controls the mechanism by which the `instruction_addr` of a [`Stacktrace`] [`Frame`] is adjusted.
367///
368/// The adjustment tries to transform *return addresses* to *call addresses* for symbolication.
369/// Typically, this adjustment needs to be done for all frames but the first, as the first frame is
370/// usually taken directly from the cpu context of a hardware exception or a suspended thread and
371/// the stack trace is created from that.
372///
373/// When the stack walking implementation truncates frames from the top, `"all"` frames should be
374/// adjusted. In case the stack walking implementation already does the adjustment when producing
375/// stack frames, `"none"` should be used here.
376#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ProcessValue)]
377#[serde(rename_all = "snake_case")]
378pub enum InstructionAddrAdjustment {
379    /// The default. Applies a heuristic based on other event / exception attributes.
380    Auto,
381
382    /// All but the first frame needs to be adjusted. The first frame's address is not a *return address*,
383    /// but points directly to the faulty instruction.
384    AllButFirst,
385
386    /// All frames should be adjusted, for example because the stack walking implementation truncated
387    /// frames from the top of the stack, and all remaining frames' addresses are *return addresses*.
388    All,
389
390    /// The stack walking implementation already provides correct addresses and no adjustment should
391    /// be performed when symbolicating.
392    None,
393
394    /// Any other unknown adjustment strategy.
395    ///
396    /// This exists to ensure forward compatibility.
397    Unknown(String),
398}
399
400impl InstructionAddrAdjustment {
401    /// Returns the string representation of this adjustment.
402    pub fn as_str(&self) -> &str {
403        match self {
404            InstructionAddrAdjustment::Auto => "auto",
405            InstructionAddrAdjustment::AllButFirst => "all_but_first",
406            InstructionAddrAdjustment::All => "all",
407            InstructionAddrAdjustment::None => "none",
408            InstructionAddrAdjustment::Unknown(s) => s,
409        }
410    }
411}
412
413impl FromStr for InstructionAddrAdjustment {
414    type Err = Infallible;
415
416    fn from_str(s: &str) -> Result<Self, Self::Err> {
417        match s {
418            "auto" => Ok(Self::Auto),
419            "all_but_first" => Ok(Self::AllButFirst),
420            "all" => Ok(Self::All),
421            "none" => Ok(Self::None),
422            s => Ok(Self::Unknown(s.to_string())),
423        }
424    }
425}
426
427impl fmt::Display for InstructionAddrAdjustment {
428    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429        f.write_str(self.as_str())
430    }
431}
432
433impl Default for InstructionAddrAdjustment {
434    fn default() -> Self {
435        Self::Auto
436    }
437}
438
439impl Empty for InstructionAddrAdjustment {
440    #[inline]
441    fn is_empty(&self) -> bool {
442        matches!(self, Self::Auto)
443    }
444}
445
446impl FromValue for InstructionAddrAdjustment {
447    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
448        match String::from_value(value) {
449            Annotated(Some(value), mut meta) => match value.parse() {
450                Ok(adjustment) => Annotated(Some(adjustment), meta),
451                Err(_) => {
452                    meta.add_error(ErrorKind::InvalidData);
453                    meta.set_original_value(Some(value));
454                    Annotated(None, meta)
455                }
456            },
457            Annotated(None, meta) => Annotated(None, meta),
458        }
459    }
460}
461
462impl IntoValue for InstructionAddrAdjustment {
463    fn into_value(self) -> Value
464    where
465        Self: Sized,
466    {
467        Value::String(match self {
468            Self::Unknown(s) => s,
469            _ => self.as_str().to_owned(),
470        })
471    }
472
473    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
474    where
475        Self: Sized,
476        S: serde::Serializer,
477    {
478        serde::Serialize::serialize(self.as_str(), s)
479    }
480}
481
482// NOTE: This is not a doc comment because otherwise it will show up in public docs.
483// Newtype to distinguish `raw_stacktrace` attributes from the rest.
484#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
485#[metastructure(process_func = "process_stacktrace")]
486pub struct Stacktrace(pub RawStacktrace);
487
488impl Deref for Stacktrace {
489    type Target = RawStacktrace;
490
491    fn deref(&self) -> &RawStacktrace {
492        &self.0
493    }
494}
495
496impl DerefMut for Stacktrace {
497    fn deref_mut(&mut self) -> &mut RawStacktrace {
498        &mut self.0
499    }
500}
501
502impl From<RawStacktrace> for Stacktrace {
503    fn from(stacktrace: RawStacktrace) -> Stacktrace {
504        Stacktrace(stacktrace)
505    }
506}
507
508impl From<Stacktrace> for RawStacktrace {
509    fn from(stacktrace: Stacktrace) -> RawStacktrace {
510        stacktrace.0
511    }
512}
513
514#[cfg(test)]
515mod tests {
516    use crate::protocol::{LockReasonType, ThreadId};
517    use similar_asserts::assert_eq;
518
519    use super::*;
520
521    #[test]
522    fn test_frame_roundtrip() {
523        let json = r#"{
524  "function": "main@8",
525  "raw_function": "main",
526  "symbol": "_main@8",
527  "module": "app",
528  "package": "/my/app",
529  "filename": "myfile.rs",
530  "abs_path": "/path/to",
531  "lineno": 2,
532  "colno": 42,
533  "platform": "rust",
534  "pre_context": [
535    "fn main() {"
536  ],
537  "context_line": "unimplemented!()",
538  "post_context": [
539    "}"
540  ],
541  "in_app": true,
542  "vars": {
543    "variable": "value"
544  },
545  "data": {
546    "sourcemap": "http://example.com/invalid.map"
547  },
548  "image_addr": "0x400",
549  "instruction_addr": "0x404",
550  "addr_mode": "abs",
551  "symbol_addr": "0x404",
552  "trust": "69",
553  "lang": "rust",
554  "stack_start": true,
555  "lock": {
556    "type": 2,
557    "address": "0x07d7437b",
558    "package_name": "io.sentry.samples",
559    "class_name": "MainActivity",
560    "thread_id": 7
561  },
562  "other": "value"
563}"#;
564        let frame = Annotated::new(Frame {
565            function: Annotated::new("main@8".to_string()),
566            raw_function: Annotated::new("main".to_string()),
567            symbol: Annotated::new("_main@8".to_string()),
568            module: Annotated::new("app".to_string()),
569            package: Annotated::new("/my/app".to_string()),
570            filename: Annotated::new("myfile.rs".into()),
571            abs_path: Annotated::new("/path/to".into()),
572            lineno: Annotated::new(2),
573            colno: Annotated::new(42),
574            platform: Annotated::new("rust".to_string()),
575            pre_context: Annotated::new(vec![Annotated::new("fn main() {".to_string())]),
576            context_line: Annotated::new("unimplemented!()".to_string()),
577            post_context: Annotated::new(vec![Annotated::new("}".to_string())]),
578            in_app: Annotated::new(true),
579            vars: {
580                let mut vars = Object::new();
581                vars.insert(
582                    "variable".to_string(),
583                    Annotated::new(Value::String("value".to_string())),
584                );
585                Annotated::new(vars.into())
586            },
587            data: Annotated::new(FrameData {
588                sourcemap: Annotated::new("http://example.com/invalid.map".to_string()),
589                ..Default::default()
590            }),
591            image_addr: Annotated::new(Addr(0x400)),
592            instruction_addr: Annotated::new(Addr(0x404)),
593            addr_mode: Annotated::new("abs".into()),
594            function_id: Annotated::empty(),
595            symbol_addr: Annotated::new(Addr(0x404)),
596            trust: Annotated::new("69".into()),
597            lang: Annotated::new("rust".into()),
598            stack_start: Annotated::new(true),
599            lock: Annotated::new(LockReason {
600                ty: Annotated::new(LockReasonType::Waiting),
601                address: Annotated::new("0x07d7437b".to_string()),
602                package_name: Annotated::new("io.sentry.samples".to_string()),
603                class_name: Annotated::new("MainActivity".to_string()),
604                thread_id: Annotated::new(ThreadId::Int(7)),
605                other: Default::default(),
606            }),
607            other: {
608                let mut vars = Object::new();
609                vars.insert(
610                    "other".to_string(),
611                    Annotated::new(Value::String("value".to_string())),
612                );
613                vars
614            },
615        });
616
617        assert_eq!(frame, Annotated::from_json(json).unwrap());
618        assert_eq!(json, frame.to_json_pretty().unwrap());
619    }
620
621    #[test]
622    fn test_frame_default_values() {
623        let json = "{}";
624        let frame = Annotated::new(Frame::default());
625
626        assert_eq!(frame, Annotated::from_json(json).unwrap());
627        assert_eq!(json, frame.to_json_pretty().unwrap());
628    }
629
630    #[test]
631    fn test_stacktrace_roundtrip() {
632        let json = r#"{
633  "frames": [
634    {
635      "function": "foobar"
636    }
637  ],
638  "registers": {
639    "cspr": "0x20000000",
640    "lr": "0x18a31aadc",
641    "pc": "0x18a310ea4",
642    "sp": "0x16fd75060"
643  },
644  "instruction_addr_adjustment": "all_but_first",
645  "lang": "rust",
646  "snapshot": false,
647  "other": "value"
648}"#;
649        let stack = Annotated::new(RawStacktrace {
650            frames: Annotated::new(vec![Annotated::new(Frame {
651                function: Annotated::new("foobar".to_string()),
652                ..Default::default()
653            })]),
654            registers: {
655                let mut registers = Object::new();
656                registers.insert("cspr".to_string(), Annotated::new(RegVal(0x2000_0000)));
657                registers.insert("lr".to_string(), Annotated::new(RegVal(0x1_8a31_aadc)));
658                registers.insert("pc".to_string(), Annotated::new(RegVal(0x1_8a31_0ea4)));
659                registers.insert("sp".to_string(), Annotated::new(RegVal(0x1_6fd7_5060)));
660                Annotated::new(registers)
661            },
662            instruction_addr_adjustment: Annotated::new(InstructionAddrAdjustment::AllButFirst),
663            lang: Annotated::new("rust".into()),
664            snapshot: Annotated::new(false),
665            other: {
666                let mut other = Object::new();
667                other.insert(
668                    "other".to_string(),
669                    Annotated::new(Value::String("value".to_string())),
670                );
671                other
672            },
673        });
674
675        assert_eq!(stack, Annotated::from_json(json).unwrap());
676        assert_eq!(json, stack.to_json_pretty().unwrap());
677    }
678
679    #[test]
680    fn test_stacktrace_default_values() {
681        // This needs an empty frame because "frames" is required
682        let json = r#"{
683  "frames": [
684    {}
685  ]
686}"#;
687
688        let stack = Annotated::new(RawStacktrace {
689            frames: Annotated::new(vec![Annotated::new(Frame::default())]),
690            ..Default::default()
691        });
692
693        assert_eq!(stack, Annotated::from_json(json).unwrap());
694        assert_eq!(json, stack.to_json_pretty().unwrap());
695    }
696
697    #[test]
698    fn test_frame_vars_null_preserved() {
699        let json = r#"{
700  "vars": {
701    "despacito": null
702  }
703}"#;
704        let frame = Annotated::new(Frame {
705            vars: Annotated::new({
706                let mut vars = Object::new();
707                vars.insert("despacito".to_string(), Annotated::empty());
708                vars.into()
709            }),
710            ..Default::default()
711        });
712
713        assert_eq!(Annotated::from_json(json).unwrap(), frame);
714        assert_eq!(json, frame.to_json_pretty().unwrap());
715    }
716
717    #[test]
718    fn test_frame_vars_empty_annotated_is_serialized() {
719        let output = r#"{
720  "vars": {
721    "despacito": null,
722    "despacito2": null
723  }
724}"#;
725        let frame = Annotated::new(Frame {
726            vars: Annotated::new({
727                let mut vars = Object::new();
728                vars.insert("despacito".to_string(), Annotated::empty());
729                vars.insert("despacito2".to_string(), Annotated::empty());
730                vars.into()
731            }),
732            ..Default::default()
733        });
734
735        assert_eq!(output, frame.to_json_pretty().unwrap());
736    }
737
738    #[test]
739    fn test_frame_empty_context_lines() {
740        let json = r#"{
741  "pre_context": [
742    ""
743  ],
744  "context_line": "",
745  "post_context": [
746    ""
747  ]
748}"#;
749
750        let frame = Annotated::new(Frame {
751            pre_context: Annotated::new(vec![Annotated::new("".to_string())]),
752            context_line: Annotated::new("".to_string()),
753            post_context: Annotated::new(vec![Annotated::new("".to_string())]),
754            ..Frame::default()
755        });
756
757        assert_eq!(frame, Annotated::from_json(json).unwrap());
758        assert_eq!(json, frame.to_json_pretty().unwrap());
759    }
760
761    #[test]
762    fn test_php_frame_vars() {
763        // Buggy PHP SDKs send us this stuff
764        //
765        // Port of https://github.com/getsentry/sentry/commit/73d9a061dcac3ab8c318a09735601a12e81085dd
766
767        let input = r#"{
768  "vars": ["foo", "bar", "baz", null]
769}"#;
770
771        let output = r#"{
772  "vars": {
773    "0": "foo",
774    "1": "bar",
775    "2": "baz",
776    "3": null
777  }
778}"#;
779
780        let frame = Annotated::new(Frame {
781            vars: Annotated::new({
782                let mut vars = Object::new();
783                vars.insert("0".to_string(), Annotated::new("foo".to_string().into()));
784                vars.insert("1".to_string(), Annotated::new("bar".to_string().into()));
785                vars.insert("2".to_string(), Annotated::new("baz".to_string().into()));
786                vars.insert("3".to_string(), Annotated::empty());
787                vars.into()
788            }),
789            ..Default::default()
790        });
791
792        assert_eq!(frame, Annotated::from_json(input).unwrap());
793        assert_eq!(output, frame.to_json_pretty().unwrap());
794    }
795}