From bc18d7fec7710d407509d480293a3eb16f839d93 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 25 Mar 2018 19:08:37 +0200 Subject: [PATCH] feat: Added subsecond serialization for breadcrumbs --- src/protocol/mod.rs | 2 ++ src/protocol/utils.rs | 56 ++++++++++++++++++++++++++++++++++++ src/protocol/v7.rs | 7 +++-- tests/test_protocol_v7.rs | 60 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 src/protocol/utils.rs diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index fbfd71ff..7f253ccd 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -2,6 +2,8 @@ pub mod v7; +mod utils; + /// the always latest sentry protocol version pub mod latest { pub use super::v7::*; diff --git a/src/protocol/utils.rs b/src/protocol/utils.rs new file mode 100644 index 00000000..9940f18c --- /dev/null +++ b/src/protocol/utils.rs @@ -0,0 +1,56 @@ +pub mod ts_seconds_float { + use std::fmt; + use serde::{ser, de}; + use chrono::{DateTime, Utc, TimeZone}; + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where D: de::Deserializer<'de> + { + Ok(d.deserialize_any(SecondsTimestampVisitor) + .map(|dt| dt.with_timezone(&Utc))?) + } + + pub fn serialize(dt: &DateTime, serializer: S) -> Result + where S: ser::Serializer + { + if dt.timestamp_subsec_nanos() == 0 { + serializer.serialize_i64(dt.timestamp()) + } else { + serializer.serialize_f64( + (dt.timestamp() as f64) + + ((dt.timestamp_subsec_micros() as f64) / 1_000_000f64) + ) + } + } + + struct SecondsTimestampVisitor; + + impl<'de> de::Visitor<'de> for SecondsTimestampVisitor { + type Value = DateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result + { + write!(formatter, "a unix timestamp in seconds") + } + + fn visit_f64(self, value: f64) -> Result, E> + where E: de::Error + { + let secs = value as i64; + let micros = (value.fract() * 1_000_000f64) as u32; + Ok(Utc.timestamp_opt(secs, micros * 1000).unwrap()) + } + + fn visit_i64(self, value: i64) -> Result, E> + where E: de::Error + { + Ok(Utc.timestamp_opt(value, 0).unwrap()) + } + + fn visit_u64(self, value: u64) -> Result, E> + where E: de::Error + { + Ok(Utc.timestamp_opt(value as i64, 0).unwrap()) + } + } +} diff --git a/src/protocol/v7.rs b/src/protocol/v7.rs index 3d33601d..11a23006 100644 --- a/src/protocol/v7.rs +++ b/src/protocol/v7.rs @@ -7,7 +7,6 @@ use std::fmt; use std::net::IpAddr; -use chrono; use chrono::{DateTime, Utc}; use url_serde; use url::Url; @@ -16,6 +15,8 @@ use serde::de::{Deserialize, Deserializer, Error as DeError}; use serde::ser::{Error as SerError, Serialize, SerializeMap, Serializer}; use serde_json::{from_value, to_value}; +use protocol::utils::ts_seconds_float; + /// An arbitrary (JSON) value (`serde_json::value::Value`) pub mod value { pub use serde_json::value::{Value, Index, Number, from_value, to_value}; @@ -52,6 +53,7 @@ pub struct LogEntry { /// Represents a frame. #[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq)] +#[serde(default)] pub struct Frame { /// The name of the function is known. /// @@ -139,6 +141,7 @@ pub struct TemplateInfo { /// Represents contextual information in a frame. #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(default)] pub struct EmbeddedSources { /// The sources of the lines leading up to the current line. #[serde(rename = "pre_context", skip_serializing_if = "Vec::is_empty")] @@ -316,7 +319,7 @@ impl Level { #[serde(default)] pub struct Breadcrumb { /// The timestamp of the breadcrumb. This is required. - #[serde(with = "chrono::serde::ts_seconds")] + #[serde(with = "ts_seconds_float")] pub timestamp: DateTime, /// The type of the breadcrumb. #[serde(rename = "type")] diff --git a/tests/test_protocol_v7.rs b/tests/test_protocol_v7.rs index 006c363e..bbb4b1e5 100644 --- a/tests/test_protocol_v7.rs +++ b/tests/test_protocol_v7.rs @@ -13,6 +13,11 @@ fn reserialize(event: &v7::Event) -> v7::Event { serde_json::from_str(&json).unwrap() } +fn assert_roundtrip(event: &v7::Event) { + let event_roundtripped = reserialize(event); + assert_eq!(event, &event_roundtripped); +} + #[test] fn test_event_default_vs_new() { let event_new = reserialize(&v7::Event::new()); @@ -90,6 +95,7 @@ fn test_fingerprint() { assert_eq!(serde_json::to_string(&event).unwrap(), "{}"); event.fingerprint.push("extra".into()); + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"fingerprint\":[\"{{ default }}\",\"extra\"]}" @@ -102,6 +108,7 @@ fn test_basic_message_event() { event.level = v7::Level::Warning; event.message = Some("Hello World!".into()); event.logger = Some("root".into()); + assert_roundtrip(&event); let json = serde_json::to_string(&event).unwrap(); assert_eq!( &json, @@ -117,6 +124,7 @@ fn test_message_basics() { level: v7::Level::Info, ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"level\":\"info\",\"culprit\":\"foo in bar\",\"message\":\"Hello World!\"}" @@ -134,6 +142,7 @@ fn test_logentry_basics() { level: v7::Level::Debug, ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"level\":\"debug\",\"culprit\":\"foo in bar\",\"logentry\":{\"message\":\ @@ -151,6 +160,7 @@ fn test_modules() { }, ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"modules\":{\"System\":\"1.0.0\"}}" @@ -172,6 +182,7 @@ fn test_repos() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"repos\":{\"/raven\":{\"name\":\"github/raven\",\"prefix\":\"/\",\"revision\":\"49f45700b5fe606c1bcd9bf0205ecbb83db17f52\"}}}" @@ -190,6 +201,7 @@ fn test_repos() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"repos\":{\"/raven\":{\"name\":\"github/raven\"}}}" @@ -204,6 +216,7 @@ fn test_platform_and_timestamp() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"platform\":\"python\",\"timestamp\":\"2017-12-24T08:12:00Z\"}" @@ -227,6 +240,7 @@ fn test_user() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"user\":{\"id\":\"8fd5a33b-5b0e-45b2-aff2-9e4f067756ba\",\ @@ -245,6 +259,7 @@ fn test_user() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"user\":{\"id\":\"8fd5a33b-5b0e-45b2-aff2-9e4f067756ba\"}}" @@ -278,12 +293,41 @@ fn test_breadcrumbs() { ..Default::default() }; + assert_roundtrip(&event); + assert_eq!( + serde_json::to_string(&event).unwrap(), + "{\"breadcrumbs\":[{\"timestamp\":1514103120.713,\"type\":\"default\",\ + \"category\":\"ui.click\",\"message\":\"span.platform-card > li.platform-tile\"\ + },{\"timestamp\":1514103120.913,\"type\":\"http\",\"category\":\"xhr\",\"data\"\ + :{\"url\":\"/api/0/organizations/foo\",\"status_code\":200,\"method\":\"GET\"}}]}" + ); +} + +#[test] +fn test_stacktrace() { + let event = v7::Event { + stacktrace: Some(v7::Stacktrace { + frames: vec![ + v7::Frame { + function: Some("main".into()), + location: v7::FileLocation { + filename: Some("hello.py".into()), + line: Some(1), + ..Default::default() + }, + ..Default::default() + } + ], + ..Default::default() + }), + ..Default::default() + }; + + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), - "{\"breadcrumbs\":[{\"timestamp\":1514103120,\"type\":\"default\",\ - \"category\":\"ui.click\",\"message\":\"span.platform-card > li.platform-tile\"}\ - ,{\"timestamp\":1514103120,\"type\":\"http\",\"category\":\"xhr\",\"data\":\ - {\"url\":\"/api/0/organizations/foo\",\"status_code\":200,\"method\":\"GET\"}}]}" + "{\"stacktrace\":{\"frames\":[{\"function\":\"main\",\ + \"filename\":\"hello.py\",\"lineno\":1}]}}" ); } @@ -311,6 +355,7 @@ fn test_request() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"request\":{\"url\":\"https://www.example.invalid/bar\",\ @@ -337,6 +382,7 @@ fn test_request() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"request\":{\"url\":\"https://www.example.invalid/bar\",\ @@ -350,6 +396,7 @@ fn test_request() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"request\":{}}" @@ -364,6 +411,7 @@ fn test_canonical_exception() { ..Default::default() }); let json = serde_json::to_string(&event).unwrap(); + assert_roundtrip(&event); assert_eq!( json, "{\"exception\":{\"values\":[{\"type\":\"ZeroDivisionError\"}]}}" @@ -394,6 +442,7 @@ fn test_multi_exception_list() { ty: "ZeroDivisionError".into(), ..Default::default() }); + assert_roundtrip(&event); assert_eq!(event, ref_event); } @@ -422,6 +471,7 @@ fn test_minimal_exception_stacktrace() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"exception\":{\"values\":[{\"type\":\"DivisionByZero\",\ @@ -468,6 +518,7 @@ fn test_slightly_larger_exception_stacktrace() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"exception\":{\"values\":[{\"type\":\"DivisionByZero\",\"value\":\ @@ -523,6 +574,7 @@ fn test_full_exception_stacktrace() { ..Default::default() }; + assert_roundtrip(&event); assert_eq!( serde_json::to_string(&event).unwrap(), "{\"exception\":{\"values\":[{\"type\":\"DivisionByZero\",\