Skip to content

Commit

Permalink
feat: Added subsecond serialization for breadcrumbs
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Mar 25, 2018
1 parent f564fe4 commit bc18d7f
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 6 deletions.
2 changes: 2 additions & 0 deletions src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pub mod v7;

mod utils;

/// the always latest sentry protocol version
pub mod latest {
pub use super::v7::*;
Expand Down
56 changes: 56 additions & 0 deletions src/protocol/utils.rs
Original file line number Diff line number Diff line change
@@ -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<DateTime<Utc>, D::Error>
where D: de::Deserializer<'de>
{
Ok(d.deserialize_any(SecondsTimestampVisitor)
.map(|dt| dt.with_timezone(&Utc))?)
}

pub fn serialize<S>(dt: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
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<Utc>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result
{
write!(formatter, "a unix timestamp in seconds")
}

fn visit_f64<E>(self, value: f64) -> Result<DateTime<Utc>, 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<E>(self, value: i64) -> Result<DateTime<Utc>, E>
where E: de::Error
{
Ok(Utc.timestamp_opt(value, 0).unwrap())
}

fn visit_u64<E>(self, value: u64) -> Result<DateTime<Utc>, E>
where E: de::Error
{
Ok(Utc.timestamp_opt(value as i64, 0).unwrap())
}
}
}
7 changes: 5 additions & 2 deletions src/protocol/v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use std::fmt;
use std::net::IpAddr;

use chrono;
use chrono::{DateTime, Utc};
use url_serde;
use url::Url;
Expand All @@ -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};
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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<Utc>,
/// The type of the breadcrumb.
#[serde(rename = "type")]
Expand Down
60 changes: 56 additions & 4 deletions tests/test_protocol_v7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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\"]}"
Expand All @@ -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,
Expand All @@ -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!\"}"
Expand All @@ -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\":\
Expand All @@ -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\"}}"
Expand All @@ -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\"}}}"
Expand All @@ -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\"}}}"
Expand All @@ -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\"}"
Expand All @@ -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\",\
Expand All @@ -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\"}}"
Expand Down Expand Up @@ -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}]}}"
);
}

Expand Down Expand Up @@ -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\",\
Expand All @@ -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\",\
Expand All @@ -350,6 +396,7 @@ fn test_request() {
..Default::default()
};

assert_roundtrip(&event);
assert_eq!(
serde_json::to_string(&event).unwrap(),
"{\"request\":{}}"
Expand All @@ -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\"}]}}"
Expand Down Expand Up @@ -394,6 +442,7 @@ fn test_multi_exception_list() {
ty: "ZeroDivisionError".into(),
..Default::default()
});
assert_roundtrip(&event);
assert_eq!(event, ref_event);
}

Expand Down Expand Up @@ -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\",\
Expand Down Expand Up @@ -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\":\
Expand Down Expand Up @@ -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\",\
Expand Down

0 comments on commit bc18d7f

Please sign in to comment.