diff --git a/src/protocol/v7.rs b/src/protocol/v7.rs index a44940e3..23100dd8 100644 --- a/src/protocol/v7.rs +++ b/src/protocol/v7.rs @@ -7,45 +7,133 @@ use chrono::{DateTime, Utc}; use url_serde; use url::Url; use serde::de::{Deserialize, Deserializer, Error as DeError}; -use serde::ser::{Error as SerError, SerializeMap, Serializer}; +use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer}; use serde_json::{from_value, to_value, Value}; /// Represents a log entry message. +/// +/// A log message is similar to the `message` attribute on the event itself but +/// can additionally hold optional parameters. #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] pub struct LogEntry { + /// The log message with parameters replaced by `%s` pub message: String, - #[serde(skip_serializing_if = "Vec::is_empty")] pub params: Vec, + /// Positional parameters to be inserted into the log entry. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub params: Vec, } /// Represents a frame. #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] pub struct Frame { - pub filename: String, + /// The name of the function is known. + /// + /// Note that this might include the name of a class as well if that makes + /// sense for the language. + #[serde(skip_serializing_if = "Option::is_none")] + pub function: Option, + /// The potentially mangled name of the symbol as it appears in an executable. + /// + /// This is different from a function name by generally being the mangled + /// name that appears natively in the binary. This is relevant for languages + /// like Swift, C++ or Rust. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol: Option, + /// The name of the module the frame is contained in. + /// + /// Note that this might also include a class name if that is something the + /// language natively considers to be part of the stack (for instance in Java). + #[serde(skip_serializing_if = "Option::is_none")] + pub module: Option, + /// The name of the package that contains the frame. + /// + /// For instance this can be a dylib for native languages, the name of the jar + /// or .NET assembly. + #[serde(skip_serializing_if = "Option::is_none")] + pub package: Option, + /// Location information about where the error originated. + #[serde(flatten)] + pub location: FileLocation, + /// Embedded sourcecode in the frame. + #[serde(flatten)] + pub source: EmbeddedSources, + /// In-app indicator. + #[serde(skip_serializing_if = "Option::is_none")] + pub in_app: Option, + /// Optional local variables. + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub vars: HashMap, + /// Optional instruction information for native languages. + #[serde(flatten)] + pub instruction_info: InstructionInfo, +} + +/// Represents location information. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct FileLocation { + /// The filename (basename only). + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + /// If known the absolute path. + #[serde(skip_serializing_if = "Option::is_none")] pub abs_path: Option, - pub function: String, - pub lineno: Option, - pub context_line: Option, - pub pre_context: Option>, - pub post_context: Option>, + /// The line number if known. + #[serde(rename = "lineno", skip_serializing_if = "Option::is_none")] + pub line: Option, + /// The column number if known. + #[serde(rename = "colno", skip_serializing_if = "Option::is_none")] + pub column: Option, +} + +/// Represents instruction information. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct InstructionInfo { + /// If known the location of the image. + #[serde(skip_serializing_if = "Option::is_none")] + pub image_addr: Option, + /// If known the location of the instruction. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_addr: Option, + /// If known the location of symbol. + #[serde(skip_serializing_if = "Option::is_none")] + pub symbol_addr: Option, +} + +/// Represents contextual information in a frame. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct EmbeddedSources { + /// The sources of the lines leading up to the current line. + #[serde(rename = "pre_context")] + pub pre_lines: Option>, + /// The current line as source. + #[serde(rename = "context_line")] + pub current_line: Option, + /// The sources of the lines after the current line. + #[serde(rename = "post_context")] + pub post_lines: Option>, } /// Represents a stacktrace. #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub struct Stacktrace { + /// The list of frames in the stacktrace. pub frames: Vec, -} - -/// Represents a list of exceptions. -#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] -pub struct Exception { - pub values: Vec, + /// Optionally a segment of frames removed (`start`, `end`) + #[serde(skip_serializing_if = "Option::is_none")] + pub frames_omitted: Option<(u64, u64)>, } /// Represents a single exception #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] -pub struct SingleException { - #[serde(rename = "type")] pub ty: String, - pub value: String, +pub struct Exception { + /// The type of the exception + #[serde(rename = "type")] + pub ty: String, + /// The optional value of the exception + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, + /// Optionally the stacktrace. + #[serde(skip_serializing_if = "Option::is_none")] pub stacktrace: Option, } @@ -163,7 +251,9 @@ pub struct Event { deserialize_with = "deserialize_context")] pub contexts: HashMap, #[serde(skip_serializing_if = "Vec::is_empty")] pub breadcrumbs: Vec, - #[serde(skip_serializing_if = "Option::is_none")] pub exception: Option, + #[serde(skip_serializing_if = "Vec::is_empty", serialize_with = "serialize_exceptions", + deserialize_with = "deserialize_exceptions", rename = "exception")] + pub exceptions: Vec, #[serde(skip_serializing_if = "HashMap::is_empty")] pub tags: HashMap, #[serde(skip_serializing_if = "HashMap::is_empty")] pub extra: HashMap, #[serde(flatten)] pub other: HashMap, @@ -329,3 +419,32 @@ where map.end() } + +fn deserialize_exceptions<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Repr { + Qualified { values: Vec }, + Unqualified(Vec), + Single(Exception), + } + Repr::deserialize(deserializer).map(|x| match x { + Repr::Qualified { values } => values, + Repr::Unqualified(values) => values, + Repr::Single(exc) => vec![exc], + }) +} + +fn serialize_exceptions(value: &Vec, serializer: S) -> Result +where + S: Serializer, +{ + #[derive(Serialize)] + struct Helper<'a> { + values: &'a [Exception], + } + Helper { values: &value }.serialize(serializer) +} diff --git a/tests/test_protocol_v7.rs b/tests/test_protocol_v7.rs index 357fef80..c0949226 100644 --- a/tests/test_protocol_v7.rs +++ b/tests/test_protocol_v7.rs @@ -31,3 +31,41 @@ fn test_basic_event() { \"os\"}}}" ); } + +#[test] +fn test_canonical_exception() { + let mut event: v7::Event = Default::default(); + event.exceptions.push(v7::Exception { + ty: "ZeroDivisionError".into(), + ..Default::default() + }); + let json = serde_json::to_string(&event).unwrap(); + assert_eq!(json, "{\"exception\":{\"values\":[{\"type\":\"ZeroDivisionError\"}]}}"); + + let event2: v7::Event = serde_json::from_str(&json).unwrap(); + assert_eq!(event, event2); +} + +#[test] +fn test_single_exception_inline() { + let json = "{\"exception\":{\"type\":\"ZeroDivisionError\"}}"; + let event: v7::Event = serde_json::from_str(&json).unwrap(); + let mut ref_event: v7::Event = Default::default(); + ref_event.exceptions.push(v7::Exception { + ty: "ZeroDivisionError".into(), + ..Default::default() + }); + assert_eq!(event, ref_event); +} + +#[test] +fn test_multi_exception_list() { + let json = "{\"exception\":[{\"type\":\"ZeroDivisionError\"}]}"; + let event: v7::Event = serde_json::from_str(&json).unwrap(); + let mut ref_event: v7::Event = Default::default(); + ref_event.exceptions.push(v7::Exception { + ty: "ZeroDivisionError".into(), + ..Default::default() + }); + assert_eq!(event, ref_event); +}