Skip to content

Commit cec392d

Browse files
committed
feat: Added support for multi variant exception format
1 parent 8846c23 commit cec392d

File tree

2 files changed

+175
-18
lines changed

2 files changed

+175
-18
lines changed

src/protocol/v7.rs

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,133 @@ use chrono::{DateTime, Utc};
77
use url_serde;
88
use url::Url;
99
use serde::de::{Deserialize, Deserializer, Error as DeError};
10-
use serde::ser::{Error as SerError, SerializeMap, Serializer};
10+
use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer};
1111
use serde_json::{from_value, to_value, Value};
1212

1313
/// Represents a log entry message.
14+
///
15+
/// A log message is similar to the `message` attribute on the event itself but
16+
/// can additionally hold optional parameters.
1417
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
1518
pub struct LogEntry {
19+
/// The log message with parameters replaced by `%s`
1620
pub message: String,
17-
#[serde(skip_serializing_if = "Vec::is_empty")] pub params: Vec<Value>,
21+
/// Positional parameters to be inserted into the log entry.
22+
#[serde(skip_serializing_if = "Vec::is_empty")]
23+
pub params: Vec<Value>,
1824
}
1925

2026
/// Represents a frame.
2127
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
2228
pub struct Frame {
23-
pub filename: String,
29+
/// The name of the function is known.
30+
///
31+
/// Note that this might include the name of a class as well if that makes
32+
/// sense for the language.
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub function: Option<String>,
35+
/// The potentially mangled name of the symbol as it appears in an executable.
36+
///
37+
/// This is different from a function name by generally being the mangled
38+
/// name that appears natively in the binary. This is relevant for languages
39+
/// like Swift, C++ or Rust.
40+
#[serde(skip_serializing_if = "Option::is_none")]
41+
pub symbol: Option<String>,
42+
/// The name of the module the frame is contained in.
43+
///
44+
/// Note that this might also include a class name if that is something the
45+
/// language natively considers to be part of the stack (for instance in Java).
46+
#[serde(skip_serializing_if = "Option::is_none")]
47+
pub module: Option<String>,
48+
/// The name of the package that contains the frame.
49+
///
50+
/// For instance this can be a dylib for native languages, the name of the jar
51+
/// or .NET assembly.
52+
#[serde(skip_serializing_if = "Option::is_none")]
53+
pub package: Option<String>,
54+
/// Location information about where the error originated.
55+
#[serde(flatten)]
56+
pub location: FileLocation,
57+
/// Embedded sourcecode in the frame.
58+
#[serde(flatten)]
59+
pub source: EmbeddedSources,
60+
/// In-app indicator.
61+
#[serde(skip_serializing_if = "Option::is_none")]
62+
pub in_app: Option<bool>,
63+
/// Optional local variables.
64+
#[serde(skip_serializing_if = "HashMap::is_empty")]
65+
pub vars: HashMap<String, Value>,
66+
/// Optional instruction information for native languages.
67+
#[serde(flatten)]
68+
pub instruction_info: InstructionInfo,
69+
}
70+
71+
/// Represents location information.
72+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
73+
pub struct FileLocation {
74+
/// The filename (basename only).
75+
#[serde(skip_serializing_if = "Option::is_none")]
76+
pub filename: Option<String>,
77+
/// If known the absolute path.
78+
#[serde(skip_serializing_if = "Option::is_none")]
2479
pub abs_path: Option<String>,
25-
pub function: String,
26-
pub lineno: Option<u32>,
27-
pub context_line: Option<String>,
28-
pub pre_context: Option<Vec<String>>,
29-
pub post_context: Option<Vec<String>>,
80+
/// The line number if known.
81+
#[serde(rename = "lineno", skip_serializing_if = "Option::is_none")]
82+
pub line: Option<u64>,
83+
/// The column number if known.
84+
#[serde(rename = "colno", skip_serializing_if = "Option::is_none")]
85+
pub column: Option<u64>,
86+
}
87+
88+
/// Represents instruction information.
89+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
90+
pub struct InstructionInfo {
91+
/// If known the location of the image.
92+
#[serde(skip_serializing_if = "Option::is_none")]
93+
pub image_addr: Option<u64>,
94+
/// If known the location of the instruction.
95+
#[serde(skip_serializing_if = "Option::is_none")]
96+
pub instruction_addr: Option<u64>,
97+
/// If known the location of symbol.
98+
#[serde(skip_serializing_if = "Option::is_none")]
99+
pub symbol_addr: Option<u64>,
100+
}
101+
102+
/// Represents contextual information in a frame.
103+
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
104+
pub struct EmbeddedSources {
105+
/// The sources of the lines leading up to the current line.
106+
#[serde(rename = "pre_context")]
107+
pub pre_lines: Option<Vec<String>>,
108+
/// The current line as source.
109+
#[serde(rename = "context_line")]
110+
pub current_line: Option<String>,
111+
/// The sources of the lines after the current line.
112+
#[serde(rename = "post_context")]
113+
pub post_lines: Option<Vec<String>>,
30114
}
31115

32116
/// Represents a stacktrace.
33117
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
34118
pub struct Stacktrace {
119+
/// The list of frames in the stacktrace.
35120
pub frames: Vec<Frame>,
36-
}
37-
38-
/// Represents a list of exceptions.
39-
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)]
40-
pub struct Exception {
41-
pub values: Vec<SingleException>,
121+
/// Optionally a segment of frames removed (`start`, `end`)
122+
#[serde(skip_serializing_if = "Option::is_none")]
123+
pub frames_omitted: Option<(u64, u64)>,
42124
}
43125

44126
/// Represents a single exception
45127
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
46-
pub struct SingleException {
47-
#[serde(rename = "type")] pub ty: String,
48-
pub value: String,
128+
pub struct Exception {
129+
/// The type of the exception
130+
#[serde(rename = "type")]
131+
pub ty: String,
132+
/// The optional value of the exception
133+
#[serde(skip_serializing_if = "Option::is_none")]
134+
pub value: Option<String>,
135+
/// Optionally the stacktrace.
136+
#[serde(skip_serializing_if = "Option::is_none")]
49137
pub stacktrace: Option<Stacktrace>,
50138
}
51139

@@ -163,7 +251,9 @@ pub struct Event {
163251
deserialize_with = "deserialize_context")]
164252
pub contexts: HashMap<String, Context>,
165253
#[serde(skip_serializing_if = "Vec::is_empty")] pub breadcrumbs: Vec<Breadcrumb>,
166-
#[serde(skip_serializing_if = "Option::is_none")] pub exception: Option<Exception>,
254+
#[serde(skip_serializing_if = "Vec::is_empty", serialize_with = "serialize_exceptions",
255+
deserialize_with = "deserialize_exceptions", rename = "exception")]
256+
pub exceptions: Vec<Exception>,
167257
#[serde(skip_serializing_if = "HashMap::is_empty")] pub tags: HashMap<String, String>,
168258
#[serde(skip_serializing_if = "HashMap::is_empty")] pub extra: HashMap<String, Value>,
169259
#[serde(flatten)] pub other: HashMap<String, Value>,
@@ -329,3 +419,32 @@ where
329419

330420
map.end()
331421
}
422+
423+
fn deserialize_exceptions<'de, D>(deserializer: D) -> Result<Vec<Exception>, D::Error>
424+
where
425+
D: Deserializer<'de>,
426+
{
427+
#[derive(Deserialize)]
428+
#[serde(untagged)]
429+
enum Repr {
430+
Qualified { values: Vec<Exception> },
431+
Unqualified(Vec<Exception>),
432+
Single(Exception),
433+
}
434+
Repr::deserialize(deserializer).map(|x| match x {
435+
Repr::Qualified { values } => values,
436+
Repr::Unqualified(values) => values,
437+
Repr::Single(exc) => vec![exc],
438+
})
439+
}
440+
441+
fn serialize_exceptions<S>(value: &Vec<Exception>, serializer: S) -> Result<S::Ok, S::Error>
442+
where
443+
S: Serializer,
444+
{
445+
#[derive(Serialize)]
446+
struct Helper<'a> {
447+
values: &'a [Exception],
448+
}
449+
Helper { values: &value }.serialize(serializer)
450+
}

tests/test_protocol_v7.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,41 @@ fn test_basic_event() {
3131
\"os\"}}}"
3232
);
3333
}
34+
35+
#[test]
36+
fn test_canonical_exception() {
37+
let mut event: v7::Event = Default::default();
38+
event.exceptions.push(v7::Exception {
39+
ty: "ZeroDivisionError".into(),
40+
..Default::default()
41+
});
42+
let json = serde_json::to_string(&event).unwrap();
43+
assert_eq!(json, "{\"exception\":{\"values\":[{\"type\":\"ZeroDivisionError\"}]}}");
44+
45+
let event2: v7::Event = serde_json::from_str(&json).unwrap();
46+
assert_eq!(event, event2);
47+
}
48+
49+
#[test]
50+
fn test_single_exception_inline() {
51+
let json = "{\"exception\":{\"type\":\"ZeroDivisionError\"}}";
52+
let event: v7::Event = serde_json::from_str(&json).unwrap();
53+
let mut ref_event: v7::Event = Default::default();
54+
ref_event.exceptions.push(v7::Exception {
55+
ty: "ZeroDivisionError".into(),
56+
..Default::default()
57+
});
58+
assert_eq!(event, ref_event);
59+
}
60+
61+
#[test]
62+
fn test_multi_exception_list() {
63+
let json = "{\"exception\":[{\"type\":\"ZeroDivisionError\"}]}";
64+
let event: v7::Event = serde_json::from_str(&json).unwrap();
65+
let mut ref_event: v7::Event = Default::default();
66+
ref_event.exceptions.push(v7::Exception {
67+
ty: "ZeroDivisionError".into(),
68+
..Default::default()
69+
});
70+
assert_eq!(event, ref_event);
71+
}

0 commit comments

Comments
 (0)