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