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}