1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
#[cfg(test)]
use chrono::{TimeZone, Utc};
use relay_protocol::{Annotated, Empty, FromValue, IntoValue, Object, Value};
use crate::processor::ProcessValue;
use crate::protocol::{EventId, Level, Timestamp};
/// The Breadcrumbs Interface specifies a series of application events, or "breadcrumbs", that
/// occurred before an event.
///
/// An event may contain one or more breadcrumbs in an attribute named `breadcrumbs`. The entries
/// are ordered from oldest to newest. Consequently, the last entry in the list should be the last
/// entry before the event occurred.
///
/// While breadcrumb attributes are not strictly validated in Sentry, a breadcrumb is most useful
/// when it includes at least a `timestamp` and `type`, `category` or `message`. The rendering of
/// breadcrumbs in Sentry depends on what is provided.
///
/// The following example illustrates the breadcrumbs part of the event payload and omits other
/// attributes for simplicity.
///
/// ```json
/// {
/// "breadcrumbs": {
/// "values": [
/// {
/// "timestamp": "2016-04-20T20:55:53.845Z",
/// "message": "Something happened",
/// "category": "log",
/// "data": {
/// "foo": "bar",
/// "blub": "blah"
/// }
/// },
/// {
/// "timestamp": "2016-04-20T20:55:53.847Z",
/// "type": "navigation",
/// "data": {
/// "from": "/login",
/// "to": "/dashboard"
/// }
/// }
/// ]
/// }
/// }
/// ```
#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
#[metastructure(process_func = "process_breadcrumb", value_type = "Breadcrumb")]
pub struct Breadcrumb {
/// The timestamp of the breadcrumb. Recommended.
///
/// A timestamp representing when the breadcrumb occurred. The format is either a string as
/// defined in [RFC 3339](https://tools.ietf.org/html/rfc3339) or a numeric (integer or float)
/// value representing the number of seconds that have elapsed since the [Unix
/// epoch](https://en.wikipedia.org/wiki/Unix_time).
///
/// Breadcrumbs are most useful when they include a timestamp, as it creates a timeline leading
/// up to an event.
pub timestamp: Annotated<Timestamp>,
/// The type of the breadcrumb. _Optional_, defaults to `default`.
///
/// - `default`: Describes a generic breadcrumb. This is typically a log message or
/// user-generated breadcrumb. The `data` field is entirely undefined and as such, completely
/// rendered as a key/value table.
///
/// - `navigation`: Describes a navigation breadcrumb. A navigation event can be a URL change
/// in a web application, or a UI transition in a mobile or desktop application, etc.
///
/// Such a breadcrumb's `data` object has the required fields `from` and `to`, which
/// represent an application route/url each.
///
/// - `http`: Describes an HTTP request breadcrumb. This represents an HTTP request transmitted
/// from your application. This could be an AJAX request from a web application, or a
/// server-to-server HTTP request to an API service provider, etc.
///
/// Such a breadcrumb's `data` property has the fields `url`, `method`, `status_code`
/// (integer) and `reason` (string).
#[metastructure(field = "type", legacy_alias = "ty", max_chars = 128)]
pub ty: Annotated<String>,
/// A dotted string indicating what the crumb is or from where it comes. _Optional._
///
/// Typically it is a module name or a descriptive string. For instance, _ui.click_ could be
/// used to indicate that a click happened in the UI or _flask_ could be used to indicate that
/// the event originated in the Flask framework.
#[metastructure(max_chars = 128)]
pub category: Annotated<String>,
/// Severity level of the breadcrumb. _Optional._
///
/// Allowed values are, from highest to lowest: `fatal`, `error`, `warning`, `info`, and
/// `debug`. Levels are used in the UI to emphasize and deemphasize the crumb. Defaults to
/// `info`.
pub level: Annotated<Level>,
/// Human readable message for the breadcrumb.
///
/// If a message is provided, it is rendered as text with all whitespace preserved. Very long
/// text might be truncated in the UI.
#[metastructure(pii = "true", max_chars = 8192, max_chars_allowance = 200)]
pub message: Annotated<String>,
/// Arbitrary data associated with this breadcrumb.
///
/// Contains a dictionary whose contents depend on the breadcrumb `type`. Additional parameters
/// that are unsupported by the type are rendered as a key/value table.
#[metastructure(pii = "true", max_depth = 5, max_bytes = 2048)]
#[metastructure(skip_serialization = "empty")]
pub data: Annotated<Object<Value>>,
/// Identifier of the event this breadcrumb belongs to.
///
/// Sentry events can appear as breadcrumbs in other events as long as they have occurred in the
/// same organization. This identifier links to the original event.
#[metastructure(skip_serialization = "null")]
pub event_id: Annotated<EventId>,
/// Additional arbitrary fields for forwards compatibility.
#[metastructure(additional_properties)]
pub other: Object<Value>,
}
#[cfg(test)]
mod tests {
use relay_protocol::Map;
use similar_asserts::assert_eq;
use super::*;
#[test]
fn test_breadcrumb_roundtrip() {
let input = r#"{
"timestamp": 946684800,
"type": "mytype",
"category": "mycategory",
"level": "fatal",
"message": "my message",
"data": {
"a": "b"
},
"event_id": "52df9022835246eeb317dbd739ccd059",
"c": "d"
}"#;
let output = r#"{
"timestamp": 946684800.0,
"type": "mytype",
"category": "mycategory",
"level": "fatal",
"message": "my message",
"data": {
"a": "b"
},
"event_id": "52df9022835246eeb317dbd739ccd059",
"c": "d"
}"#;
let breadcrumb = Annotated::new(Breadcrumb {
timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
ty: Annotated::new("mytype".to_string()),
category: Annotated::new("mycategory".to_string()),
level: Annotated::new(Level::Fatal),
message: Annotated::new("my message".to_string()),
data: {
let mut map = Map::new();
map.insert(
"a".to_string(),
Annotated::new(Value::String("b".to_string())),
);
Annotated::new(map)
},
event_id: Annotated::new("52df9022835246eeb317dbd739ccd059".parse().unwrap()),
other: {
let mut map = Map::new();
map.insert(
"c".to_string(),
Annotated::new(Value::String("d".to_string())),
);
map
},
});
assert_eq!(breadcrumb, Annotated::from_json(input).unwrap());
assert_eq!(output, breadcrumb.to_json_pretty().unwrap());
}
#[test]
fn test_breadcrumb_default_values() {
let input = r#"{"timestamp":946684800}"#;
let output = r#"{"timestamp":946684800.0}"#;
let breadcrumb = Annotated::new(Breadcrumb {
timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
..Default::default()
});
assert_eq!(breadcrumb, Annotated::from_json(input).unwrap());
assert_eq!(output, breadcrumb.to_json().unwrap());
}
#[test]
fn test_python_ty_regression() {
// The Python SDK used to send "ty" instead of "type". We're lenient to accept both.
let input = r#"{"timestamp":946684800,"ty":"http"}"#;
let output = r#"{"timestamp":946684800.0,"type":"http"}"#;
let breadcrumb = Annotated::new(Breadcrumb {
timestamp: Annotated::new(Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap().into()),
ty: Annotated::new("http".into()),
..Default::default()
});
assert_eq!(breadcrumb, Annotated::from_json(input).unwrap());
assert_eq!(output, breadcrumb.to_json().unwrap());
}
}