From 89e6c04225e44c813bdfc08d546acba750bf84dd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 22 Mar 2018 00:12:16 +0100 Subject: [PATCH] feat: Added most of v7 serialization/deserialization --- src/protocol/mod.rs | 7 +- src/protocol/v1.rs | 162 -------------------------- src/protocol/v7.rs | 271 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 163 deletions(-) delete mode 100644 src/protocol/v1.rs create mode 100644 src/protocol/v7.rs diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 4a21b1d7..fbfd71ff 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,3 +1,8 @@ //! This module exposes the types for the Sentry protocol in different versions. -pub mod v1; +pub mod v7; + +/// the always latest sentry protocol version +pub mod latest { + pub use super::v7::*; +} diff --git a/src/protocol/v1.rs b/src/protocol/v1.rs deleted file mode 100644 index a6504edd..00000000 --- a/src/protocol/v1.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::collections::HashMap; -use std::net::IpAddr; - -use url_serde; -use url::Url; -use serde_json::Value; - -/// Represents a message. -#[derive(Serialize, Deserialize, Default, Clone, Debug)] -pub struct Message { - pub message: String, - #[serde(skip_serializing_if = "Vec::is_empty")] pub params: Vec, -} - -/// Represents a frame. -#[derive(Serialize, Deserialize, Default, Clone, Debug)] -pub struct Frame { - pub filename: String, - pub abs_path: Option, - pub function: String, - pub lineno: Option, - pub context_line: Option, - pub pre_context: Option>, - pub post_context: Option>, -} - -/// Represents a stacktrace. -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct Stacktrace { - pub frames: Vec, -} - -/// Represents a list of exceptions. -#[derive(Serialize, Deserialize, Default, Clone, Debug)] -pub struct Exception { - pub values: Vec, -} - -/// Represents a single exception -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct SingleException { - #[serde(rename = "type")] pub ty: String, - pub value: String, - pub stacktrace: Option, -} - -/// Represents a single breadcrumb -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct Breadcrumb { - pub timestamp: f64, - #[serde(rename = "type")] pub ty: String, - pub message: String, - pub category: String, - #[serde(flatten)] - pub data: HashMap, -} - -/// Represents user info. -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct User { - pub id: Option, - pub email: Option, - pub ip_address: Option, - pub username: Option, - #[serde(flatten)] pub data: HashMap, -} - -/// Represents http request data. -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct Request { - #[serde(with = "url_serde")] pub url: Option, - pub method: Option, - // XXX: this makes absolutely no sense because of unicode - pub data: Option, - pub query_string: Option, - pub cookies: Option, - #[serde(skip_serializing_if = "HashMap::is_empty")] pub headers: HashMap, - #[serde(skip_serializing_if = "HashMap::is_empty")] pub env: HashMap, - #[serde(flatten)] pub other: HashMap, -} - -/// Represents a full event for Sentry. -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct Event { - pub tags: HashMap, - pub extra: HashMap, - pub level: String, - #[serde(skip_serializing_if = "Option::is_none")] pub fingerprint: Option>, - #[serde(skip_serializing_if = "Option::is_none", rename = "sentry.interfaces.Message")] - pub message: Option, - pub platform: String, - pub timestamp: f64, - #[serde(skip_serializing_if = "Option::is_none")] pub server_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub release: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub dist: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub request: Option, - #[serde(skip_serializing_if = "HashMap::is_empty")] - pub contexts: HashMap>, - #[serde(skip_serializing_if = "Vec::is_empty")] pub breadcrumbs: Vec, - pub exception: Option, - #[serde(flatten)] pub other: HashMap, -} - -/// Holds a single contextual item. -#[derive(Serialize, Deserialize, Debug, Default, Clone)] -pub struct RawContext { - #[serde(rename="type")] - ty: Option, - #[serde(flatten)] - data: HashMap, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[serde(rename_all = "lowercase")] -pub enum Orientation { - Portrait, - Landscape, -} - -#[derive(Debug, Clone)] -pub enum ContextData { - Default, - Device { - name: Option, - family: Option, - model: Option, - model_id: Option, - arch: Option, - battery_level: Option, - orientation: Option, - }, - Os { - name: Option, - version: Option, - build: Option, - kernel_version: Option, - rooted: Option, - }, - Runtime { - name: Option, - version: Option, - }, -} - -impl Default for ContextData { - fn default() -> ContextData { - ContextData::Default - } -} - -impl ContextData { - pub fn get_type(&self) -> &str { - match *self { - ContextData::Default => "default", - ContextData::Device { .. } => "device", - ContextData::Os { .. } => "os", - ContextData::Runtime { .. } => "runtime", - } - } -} diff --git a/src/protocol/v7.rs b/src/protocol/v7.rs new file mode 100644 index 00000000..2032ecfc --- /dev/null +++ b/src/protocol/v7.rs @@ -0,0 +1,271 @@ +//! The current latest sentry protocol version. +use std::collections::HashMap; +use std::net::IpAddr; + +use url_serde; +use url::Url; +use serde::de::{Deserialize, Deserializer, Error as DeError}; +use serde::ser::{Error as SerError, SerializeMap, Serializer}; +use serde_json::{from_value, to_value, Value}; + +/// Represents a log entry message. +#[derive(Serialize, Deserialize, Default, Clone, Debug)] +pub struct LogEntry { + pub message: String, + #[serde(skip_serializing_if = "Vec::is_empty")] pub params: Vec, +} + +/// Represents a frame. +#[derive(Serialize, Deserialize, Default, Clone, Debug)] +pub struct Frame { + pub filename: String, + pub abs_path: Option, + pub function: String, + pub lineno: Option, + pub context_line: Option, + pub pre_context: Option>, + pub post_context: Option>, +} + +/// Represents a stacktrace. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct Stacktrace { + pub frames: Vec, +} + +/// Represents a list of exceptions. +#[derive(Serialize, Deserialize, Default, Clone, Debug)] +pub struct Exception { + pub values: Vec, +} + +/// Represents a single exception +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct SingleException { + #[serde(rename = "type")] pub ty: String, + pub value: String, + pub stacktrace: Option, +} + +/// Represents a single breadcrumb +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct Breadcrumb { + pub timestamp: f64, + #[serde(rename = "type")] pub ty: String, + pub message: String, + pub category: String, + #[serde(flatten)] pub data: HashMap, +} + +/// Represents user info. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(default)] +pub struct User { + pub id: Option, + pub email: Option, + pub ip_address: Option, + pub username: Option, + #[serde(flatten)] pub data: HashMap, +} + +/// Represents http request data. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(default)] +pub struct Request { + #[serde(with = "url_serde")] pub url: Option, + pub method: Option, + // XXX: this makes absolutely no sense because of unicode + pub data: Option, + pub query_string: Option, + pub cookies: Option, + #[serde(skip_serializing_if = "HashMap::is_empty")] pub headers: HashMap, + #[serde(skip_serializing_if = "HashMap::is_empty")] pub env: HashMap, + #[serde(flatten)] pub other: HashMap, +} + +/// Represents a full event for Sentry. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(default)] +pub struct Event { + #[serde(skip_serializing_if = "Option::is_none")] pub level: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub fingerprint: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub logentry: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub platform: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub server_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub release: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub dist: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub request: Option, + #[serde(skip_serializing_if = "HashMap::is_empty", serialize_with = "serialize_context", + 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 = "HashMap::is_empty")] pub tags: HashMap, + #[serde(skip_serializing_if = "HashMap::is_empty")] pub extra: HashMap, + #[serde(flatten)] pub other: HashMap, +} + +fn deserialize_context<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let raw = >::deserialize(deserializer)?; + let mut rv = HashMap::new(); + + #[derive(Deserialize)] + pub struct Helper { + #[serde(flatten)] data: T, + #[serde(flatten)] extra: HashMap, + } + + for (key, mut raw_context) in raw { + let (ty, mut data) = match raw_context { + Value::Object(mut map) => { + let has_type = if let Some(&Value::String(..)) = map.get("type") { + true + } else { + false + }; + let ty = if has_type { + map.remove("type") + .and_then(|x| x.as_str().map(|x| x.to_string())) + .unwrap() + } else { + key.to_string() + }; + (ty, Value::Object(map)) + } + _ => continue, + }; + + macro_rules! convert_context { + ($enum:path, $ty:ident) => {{ + let helper = from_value::>(data) + .map_err(D::Error::custom)?; + ($enum(helper.data), helper.extra) + }} + } + + let (data, extra) = match ty.as_str() { + "device" => convert_context!(ContextType::Device, DeviceContext), + "os" => convert_context!(ContextType::Os, OsContext), + "runtime" => convert_context!(ContextType::Runtime, RuntimeContext), + _ => ( + ContextType::Default, + from_value(data).map_err(D::Error::custom)?, + ), + }; + rv.insert(key, Context { data, extra }); + } + + Ok(rv) +} + +fn serialize_context(value: &HashMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = try!(serializer.serialize_map(Some(value.len()))); + + for (key, value) in value { + let mut c = match to_value(&value.data).map_err(S::Error::custom)? { + Value::Object(map) => map, + _ => unreachable!(), + }; + c.insert("type".into(), value.data.type_name().into()); + c.extend( + value + .extra + .iter() + .map(|(key, value)| (key.to_string(), value.clone())), + ); + try!(map.serialize_entry(key, &c)); + } + + map.end() +} + +/// Optional device screen orientation +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum Orientation { + Portrait, + Landscape, +} + +/// General context data. +#[derive(Debug, Clone, Default)] +pub struct Context { + pub data: ContextType, + pub extra: HashMap, +} + +impl From for Context { + fn from(data: ContextType) -> Context { + Context { + data: data, + extra: HashMap::new(), + } + } +} + +/// Typed contextual data +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case", untagged)] +pub enum ContextType { + Default, + Device(DeviceContext), + Os(OsContext), + Runtime(RuntimeContext), +} + +impl Default for ContextType { + fn default() -> ContextType { + ContextType::Default + } +} + +/// Holds device information. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct DeviceContext { + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub family: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub arch: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub battery_level: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub orientation: Option, +} + +/// Holds operating system information. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct OsContext { + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub build: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub kernel_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub rooted: Option, +} + +/// Holds information about the runtime. +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct RuntimeContext { + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, +} + +impl ContextType { + /// Returns the name of the type for sentry. + pub fn type_name(&self) -> &str { + match *self { + ContextType::Default => "default", + ContextType::Device(..) => "device", + ContextType::Os(..) => "os", + ContextType::Runtime(..) => "runtime", + } + } +}