sentry_types/protocol/
envelope.rs

1use std::{io::Write, path::Path};
2
3use serde::Deserialize;
4use thiserror::Error;
5use uuid::Uuid;
6
7use super::v7 as protocol;
8
9use protocol::{
10    Attachment, AttachmentType, Event, MonitorCheckIn, SessionAggregates, SessionUpdate,
11    Transaction,
12};
13
14/// Raised if a envelope cannot be parsed from a given input.
15#[derive(Debug, Error)]
16pub enum EnvelopeError {
17    /// Unexpected end of file
18    #[error("unexpected end of file")]
19    UnexpectedEof,
20    /// Missing envelope header
21    #[error("missing envelope header")]
22    MissingHeader,
23    /// Missing item header
24    #[error("missing item header")]
25    MissingItemHeader,
26    /// Missing newline after header or payload
27    #[error("missing newline after header or payload")]
28    MissingNewline,
29    /// Invalid envelope header
30    #[error("invalid envelope header")]
31    InvalidHeader(#[source] serde_json::Error),
32    /// Invalid item header
33    #[error("invalid item header")]
34    InvalidItemHeader(#[source] serde_json::Error),
35    /// Invalid item payload
36    #[error("invalid item payload")]
37    InvalidItemPayload(#[source] serde_json::Error),
38}
39
40#[derive(Deserialize)]
41struct EnvelopeHeader {
42    event_id: Option<Uuid>,
43}
44
45/// An Envelope Item Type.
46#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
47#[non_exhaustive]
48enum EnvelopeItemType {
49    /// An Event Item type.
50    #[serde(rename = "event")]
51    Event,
52    /// A Session Item type.
53    #[serde(rename = "session")]
54    SessionUpdate,
55    /// A Session Aggregates Item type.
56    #[serde(rename = "sessions")]
57    SessionAggregates,
58    /// A Transaction Item type.
59    #[serde(rename = "transaction")]
60    Transaction,
61    /// An Attachment Item type.
62    #[serde(rename = "attachment")]
63    Attachment,
64    /// A Monitor Check In Item Type
65    #[serde(rename = "check_in")]
66    MonitorCheckIn,
67    /// A Metrics Item type.
68    #[cfg(feature = "metrics")]
69    #[serde(rename = "statsd")]
70    Metrics,
71}
72
73/// An Envelope Item Header.
74#[derive(Clone, Debug, Deserialize)]
75struct EnvelopeItemHeader {
76    r#type: EnvelopeItemType,
77    length: Option<usize>,
78    // Fields below apply only to Attachment Item type
79    filename: Option<String>,
80    attachment_type: Option<AttachmentType>,
81    content_type: Option<String>,
82}
83
84/// An Envelope Item.
85///
86/// See the [documentation on Items](https://develop.sentry.dev/sdk/envelopes/#items)
87/// for more details.
88#[derive(Clone, Debug, PartialEq)]
89#[non_exhaustive]
90#[allow(clippy::large_enum_variant)]
91pub enum EnvelopeItem {
92    /// An Event Item.
93    ///
94    /// See the [Event Item documentation](https://develop.sentry.dev/sdk/envelopes/#event)
95    /// for more details.
96    Event(Event<'static>),
97    /// A Session Item.
98    ///
99    /// See the [Session Item documentation](https://develop.sentry.dev/sdk/envelopes/#session)
100    /// for more details.
101    SessionUpdate(SessionUpdate<'static>),
102    /// A Session Aggregates Item.
103    ///
104    /// See the [Session Aggregates Item documentation](https://develop.sentry.dev/sdk/envelopes/#sessions)
105    /// for more details.
106    SessionAggregates(SessionAggregates<'static>),
107    /// A Transaction Item.
108    ///
109    /// See the [Transaction Item documentation](https://develop.sentry.dev/sdk/envelopes/#transaction)
110    /// for more details.
111    Transaction(Transaction<'static>),
112    /// An Attachment Item.
113    ///
114    /// See the [Attachment Item documentation](https://develop.sentry.dev/sdk/envelopes/#attachment)
115    /// for more details.
116    Attachment(Attachment),
117    /// A MonitorCheckIn item.
118    MonitorCheckIn(MonitorCheckIn),
119    /// A Metrics Item.
120    #[cfg(feature = "metrics")]
121    Statsd(Vec<u8>),
122    /// This is a sentinel item used to `filter` raw envelopes.
123    Raw,
124    // TODO:
125    // etc…
126}
127
128impl From<Event<'static>> for EnvelopeItem {
129    fn from(event: Event<'static>) -> Self {
130        EnvelopeItem::Event(event)
131    }
132}
133
134impl From<SessionUpdate<'static>> for EnvelopeItem {
135    fn from(session: SessionUpdate<'static>) -> Self {
136        EnvelopeItem::SessionUpdate(session)
137    }
138}
139
140impl From<SessionAggregates<'static>> for EnvelopeItem {
141    fn from(aggregates: SessionAggregates<'static>) -> Self {
142        EnvelopeItem::SessionAggregates(aggregates)
143    }
144}
145
146impl From<Transaction<'static>> for EnvelopeItem {
147    fn from(transaction: Transaction<'static>) -> Self {
148        EnvelopeItem::Transaction(transaction)
149    }
150}
151
152impl From<Attachment> for EnvelopeItem {
153    fn from(attachment: Attachment) -> Self {
154        EnvelopeItem::Attachment(attachment)
155    }
156}
157
158impl From<MonitorCheckIn> for EnvelopeItem {
159    fn from(check_in: MonitorCheckIn) -> Self {
160        EnvelopeItem::MonitorCheckIn(check_in)
161    }
162}
163
164/// An Iterator over the items of an Envelope.
165#[derive(Clone)]
166pub struct EnvelopeItemIter<'s> {
167    inner: std::slice::Iter<'s, EnvelopeItem>,
168}
169
170impl<'s> Iterator for EnvelopeItemIter<'s> {
171    type Item = &'s EnvelopeItem;
172
173    fn next(&mut self) -> Option<Self::Item> {
174        self.inner.next()
175    }
176}
177
178/// The items contained in an [`Envelope`].
179///
180/// This may be a vector of [`EnvelopeItem`]s (the standard case)
181/// or a binary blob.
182#[derive(Debug, Clone, PartialEq)]
183enum Items {
184    EnvelopeItems(Vec<EnvelopeItem>),
185    Raw(Vec<u8>),
186}
187
188impl Default for Items {
189    fn default() -> Self {
190        Self::EnvelopeItems(Default::default())
191    }
192}
193
194impl Items {
195    fn is_empty(&self) -> bool {
196        match self {
197            Items::EnvelopeItems(items) => items.is_empty(),
198            Items::Raw(bytes) => bytes.is_empty(),
199        }
200    }
201}
202
203/// A Sentry Envelope.
204///
205/// An Envelope is the data format that Sentry uses for Ingestion. It can contain
206/// multiple Items, some of which are related, such as Events, and Event Attachments.
207/// Other Items, such as Sessions are independent.
208///
209/// See the [documentation on Envelopes](https://develop.sentry.dev/sdk/envelopes/)
210/// for more details.
211#[derive(Clone, Default, Debug, PartialEq)]
212pub struct Envelope {
213    event_id: Option<Uuid>,
214    items: Items,
215}
216
217impl Envelope {
218    /// Creates a new empty Envelope.
219    pub fn new() -> Envelope {
220        Default::default()
221    }
222
223    /// Add a new Envelope Item.
224    pub fn add_item<I>(&mut self, item: I)
225    where
226        I: Into<EnvelopeItem>,
227    {
228        let item = item.into();
229
230        let Items::EnvelopeItems(ref mut items) = self.items else {
231            if item != EnvelopeItem::Raw {
232                eprintln!(
233                    "WARNING: This envelope contains raw items. Adding an item is not supported."
234                );
235            }
236            return;
237        };
238
239        if self.event_id.is_none() {
240            if let EnvelopeItem::Event(ref event) = item {
241                self.event_id = Some(event.event_id);
242            } else if let EnvelopeItem::Transaction(ref transaction) = item {
243                self.event_id = Some(transaction.event_id);
244            }
245        }
246        items.push(item);
247    }
248
249    /// Create an [`Iterator`] over all the [`EnvelopeItem`]s.
250    pub fn items(&self) -> EnvelopeItemIter {
251        let inner = match &self.items {
252            Items::EnvelopeItems(items) => items.iter(),
253            Items::Raw(_) => [].iter(),
254        };
255
256        EnvelopeItemIter { inner }
257    }
258
259    /// Returns the Envelopes Uuid, if any.
260    pub fn uuid(&self) -> Option<&Uuid> {
261        self.event_id.as_ref()
262    }
263
264    /// Returns the [`Event`] contained in this Envelope, if any.
265    ///
266    /// [`Event`]: struct.Event.html
267    pub fn event(&self) -> Option<&Event<'static>> {
268        let Items::EnvelopeItems(ref items) = self.items else {
269            return None;
270        };
271
272        items.iter().find_map(|item| match item {
273            EnvelopeItem::Event(event) => Some(event),
274            _ => None,
275        })
276    }
277
278    /// Filters the Envelope's [`EnvelopeItem`]s based on a predicate,
279    /// and returns a new Envelope containing only the filtered items.
280    ///
281    /// Retains the [`EnvelopeItem`]s for which the predicate returns `true`.
282    /// Additionally, [`EnvelopeItem::Attachment`]s are only kept if the Envelope
283    /// contains an [`EnvelopeItem::Event`] or [`EnvelopeItem::Transaction`].
284    ///
285    /// [`None`] is returned if no items remain in the Envelope after filtering.
286    pub fn filter<P>(self, mut predicate: P) -> Option<Self>
287    where
288        P: FnMut(&EnvelopeItem) -> bool,
289    {
290        let Items::EnvelopeItems(items) = self.items else {
291            return if predicate(&EnvelopeItem::Raw) {
292                Some(self)
293            } else {
294                None
295            };
296        };
297
298        let mut filtered = Envelope::new();
299        for item in items {
300            if predicate(&item) {
301                filtered.add_item(item);
302            }
303        }
304
305        // filter again, removing attachments which do not make any sense without
306        // an event/transaction
307        if filtered.uuid().is_none() {
308            if let Items::EnvelopeItems(ref mut items) = filtered.items {
309                items.retain(|item| !matches!(item, EnvelopeItem::Attachment(..)))
310            }
311        }
312
313        if filtered.items.is_empty() {
314            None
315        } else {
316            Some(filtered)
317        }
318    }
319
320    /// Serialize the Envelope into the given [`Write`].
321    ///
322    /// [`Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
323    pub fn to_writer<W>(&self, mut writer: W) -> std::io::Result<()>
324    where
325        W: Write,
326    {
327        let items = match &self.items {
328            Items::Raw(bytes) => return writer.write_all(bytes).map(|_| ()),
329            Items::EnvelopeItems(items) => items,
330        };
331
332        // write the headers:
333        let event_id = self.uuid();
334        match event_id {
335            Some(uuid) => writeln!(writer, r#"{{"event_id":"{uuid}"}}"#)?,
336            _ => writeln!(writer, "{{}}")?,
337        }
338
339        let mut item_buf = Vec::new();
340        // write each item:
341        for item in items {
342            // we write them to a temporary buffer first, since we need their length
343            match item {
344                EnvelopeItem::Event(event) => serde_json::to_writer(&mut item_buf, event)?,
345                EnvelopeItem::SessionUpdate(session) => {
346                    serde_json::to_writer(&mut item_buf, session)?
347                }
348                EnvelopeItem::SessionAggregates(aggregates) => {
349                    serde_json::to_writer(&mut item_buf, aggregates)?
350                }
351                EnvelopeItem::Transaction(transaction) => {
352                    serde_json::to_writer(&mut item_buf, transaction)?
353                }
354                EnvelopeItem::Attachment(attachment) => {
355                    attachment.to_writer(&mut writer)?;
356                    writeln!(writer)?;
357                    continue;
358                }
359                EnvelopeItem::MonitorCheckIn(check_in) => {
360                    serde_json::to_writer(&mut item_buf, check_in)?
361                }
362                #[cfg(feature = "metrics")]
363                EnvelopeItem::Statsd(statsd) => item_buf.extend_from_slice(statsd),
364                EnvelopeItem::Raw => {
365                    continue;
366                }
367            }
368            let item_type = match item {
369                EnvelopeItem::Event(_) => "event",
370                EnvelopeItem::SessionUpdate(_) => "session",
371                EnvelopeItem::SessionAggregates(_) => "sessions",
372                EnvelopeItem::Transaction(_) => "transaction",
373                EnvelopeItem::MonitorCheckIn(_) => "check_in",
374                #[cfg(feature = "metrics")]
375                EnvelopeItem::Statsd(_) => "statsd",
376                EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(),
377            };
378            writeln!(
379                writer,
380                r#"{{"type":"{}","length":{}}}"#,
381                item_type,
382                item_buf.len()
383            )?;
384            writer.write_all(&item_buf)?;
385            writeln!(writer)?;
386            item_buf.clear();
387        }
388
389        Ok(())
390    }
391
392    /// Creates a new Envelope from slice.
393    pub fn from_slice(slice: &[u8]) -> Result<Envelope, EnvelopeError> {
394        let (header, offset) = Self::parse_header(slice)?;
395        let items = Self::parse_items(slice, offset)?;
396
397        let mut envelope = Envelope {
398            event_id: header.event_id,
399            ..Default::default()
400        };
401
402        for item in items {
403            envelope.add_item(item);
404        }
405
406        Ok(envelope)
407    }
408
409    /// Creates a new raw Envelope from the given buffer.
410    pub fn from_bytes_raw(bytes: Vec<u8>) -> Result<Self, EnvelopeError> {
411        Ok(Self {
412            event_id: None,
413            items: Items::Raw(bytes),
414        })
415    }
416
417    /// Creates a new Envelope from path.
418    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Envelope, EnvelopeError> {
419        let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
420        Envelope::from_slice(&bytes)
421    }
422
423    /// Creates a new Envelope from path without attempting to parse anything.
424    ///
425    /// The resulting Envelope will have no `event_id` and the file contents will
426    /// be contained verbatim in the `items` field.
427    pub fn from_path_raw<P: AsRef<Path>>(path: P) -> Result<Self, EnvelopeError> {
428        let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
429        Self::from_bytes_raw(bytes)
430    }
431
432    fn parse_header(slice: &[u8]) -> Result<(EnvelopeHeader, usize), EnvelopeError> {
433        let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
434
435        let header: EnvelopeHeader = match stream.next() {
436            None => return Err(EnvelopeError::MissingHeader),
437            Some(Err(error)) => return Err(EnvelopeError::InvalidHeader(error)),
438            Some(Ok(header)) => header,
439        };
440
441        // Each header is terminated by a UNIX newline.
442        Self::require_termination(slice, stream.byte_offset())?;
443
444        Ok((header, stream.byte_offset() + 1))
445    }
446
447    fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
448        let mut items = Vec::new();
449
450        while offset < slice.len() {
451            let bytes = slice
452                .get(offset..)
453                .ok_or(EnvelopeError::MissingItemHeader)?;
454            let (item, item_size) = Self::parse_item(bytes)?;
455            offset += item_size;
456            items.push(item);
457        }
458
459        Ok(items)
460    }
461
462    fn parse_item(slice: &[u8]) -> Result<(EnvelopeItem, usize), EnvelopeError> {
463        let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
464
465        let header: EnvelopeItemHeader = match stream.next() {
466            None => return Err(EnvelopeError::UnexpectedEof),
467            Some(Err(error)) => return Err(EnvelopeError::InvalidItemHeader(error)),
468            Some(Ok(header)) => header,
469        };
470
471        // Each header is terminated by a UNIX newline.
472        let header_end = stream.byte_offset();
473        Self::require_termination(slice, header_end)?;
474
475        // The last header does not require a trailing newline, so `payload_start` may point
476        // past the end of the buffer.
477        let payload_start = std::cmp::min(header_end + 1, slice.len());
478        let payload_end = match header.length {
479            Some(len) => {
480                let payload_end = payload_start + len;
481                if slice.len() < payload_end {
482                    return Err(EnvelopeError::UnexpectedEof);
483                }
484
485                // Each payload is terminated by a UNIX newline.
486                Self::require_termination(slice, payload_end)?;
487                payload_end
488            }
489            None => match slice.get(payload_start..) {
490                Some(range) => match range.iter().position(|&b| b == b'\n') {
491                    Some(relative_end) => payload_start + relative_end,
492                    None => slice.len(),
493                },
494                None => slice.len(),
495            },
496        };
497
498        let payload = slice.get(payload_start..payload_end).unwrap();
499
500        let item = match header.r#type {
501            EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event),
502            EnvelopeItemType::Transaction => {
503                serde_json::from_slice(payload).map(EnvelopeItem::Transaction)
504            }
505            EnvelopeItemType::SessionUpdate => {
506                serde_json::from_slice(payload).map(EnvelopeItem::SessionUpdate)
507            }
508            EnvelopeItemType::SessionAggregates => {
509                serde_json::from_slice(payload).map(EnvelopeItem::SessionAggregates)
510            }
511            EnvelopeItemType::Attachment => Ok(EnvelopeItem::Attachment(Attachment {
512                buffer: payload.to_owned(),
513                filename: header.filename.unwrap_or_default(),
514                content_type: header.content_type,
515                ty: header.attachment_type,
516            })),
517            EnvelopeItemType::MonitorCheckIn => {
518                serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
519            }
520            #[cfg(feature = "metrics")]
521            EnvelopeItemType::Metrics => Ok(EnvelopeItem::Statsd(payload.into())),
522        }
523        .map_err(EnvelopeError::InvalidItemPayload)?;
524
525        Ok((item, payload_end + 1))
526    }
527
528    fn require_termination(slice: &[u8], offset: usize) -> Result<(), EnvelopeError> {
529        match slice.get(offset) {
530            Some(&b'\n') | None => Ok(()),
531            Some(_) => Err(EnvelopeError::MissingNewline),
532        }
533    }
534}
535
536impl<T> From<T> for Envelope
537where
538    T: Into<EnvelopeItem>,
539{
540    fn from(item: T) -> Self {
541        let mut envelope = Self::default();
542        envelope.add_item(item.into());
543        envelope
544    }
545}
546
547#[cfg(test)]
548mod test {
549    use std::str::FromStr;
550    use std::time::{Duration, SystemTime};
551
552    use time::format_description::well_known::Rfc3339;
553    use time::OffsetDateTime;
554
555    use super::*;
556    use crate::protocol::v7::{
557        Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SessionAttributes,
558        SessionStatus, Span,
559    };
560
561    fn to_str(envelope: Envelope) -> String {
562        let mut vec = Vec::new();
563        envelope.to_writer(&mut vec).unwrap();
564        String::from_utf8_lossy(&vec).to_string()
565    }
566
567    fn timestamp(s: &str) -> SystemTime {
568        let dt = OffsetDateTime::parse(s, &Rfc3339).unwrap();
569        let secs = dt.unix_timestamp() as u64;
570        let nanos = dt.nanosecond();
571        let duration = Duration::new(secs, nanos);
572        SystemTime::UNIX_EPOCH.checked_add(duration).unwrap()
573    }
574
575    #[test]
576    fn test_empty() {
577        assert_eq!(to_str(Envelope::new()), "{}\n");
578    }
579
580    #[test]
581    fn raw_roundtrip() {
582        let buf = r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
583{"type":"event","length":74}
584{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
585"#;
586        let envelope = Envelope::from_bytes_raw(buf.to_string().into_bytes()).unwrap();
587        let serialized = to_str(envelope);
588        assert_eq!(&serialized, buf);
589
590        let random_invalid_bytes = b"oh stahp!\0\x01\x02";
591        let envelope = Envelope::from_bytes_raw(random_invalid_bytes.to_vec()).unwrap();
592        let mut serialized = Vec::new();
593        envelope.to_writer(&mut serialized).unwrap();
594        assert_eq!(&serialized, random_invalid_bytes);
595    }
596
597    #[test]
598    fn test_event() {
599        let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
600        let timestamp = timestamp("2020-07-20T14:51:14.296Z");
601        let event = Event {
602            event_id,
603            timestamp,
604            ..Default::default()
605        };
606        let envelope: Envelope = event.into();
607        assert_eq!(
608            to_str(envelope),
609            r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
610{"type":"event","length":74}
611{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
612"#
613        )
614    }
615
616    #[test]
617    fn test_session() {
618        let session_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
619        let started = timestamp("2020-07-20T14:51:14.296Z");
620        let session = SessionUpdate {
621            session_id,
622            distinct_id: Some("foo@bar.baz".to_owned()),
623            sequence: None,
624            timestamp: None,
625            started,
626            init: true,
627            duration: Some(1.234),
628            status: SessionStatus::Ok,
629            errors: 123,
630            attributes: SessionAttributes {
631                release: "foo-bar@1.2.3".into(),
632                environment: Some("production".into()),
633                ip_address: None,
634                user_agent: None,
635            },
636        };
637        let mut envelope = Envelope::new();
638        envelope.add_item(session);
639        assert_eq!(
640            to_str(envelope),
641            r#"{}
642{"type":"session","length":222}
643{"sid":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","did":"foo@bar.baz","started":"2020-07-20T14:51:14.296Z","init":true,"duration":1.234,"status":"ok","errors":123,"attrs":{"release":"foo-bar@1.2.3","environment":"production"}}
644"#
645        )
646    }
647
648    #[test]
649    fn test_transaction() {
650        let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
651        let span_id = "d42cee9fc3e74f5c".parse().unwrap();
652        let trace_id = "335e53d614474acc9f89e632b776cc28".parse().unwrap();
653        let start_timestamp = timestamp("2020-07-20T14:51:14.296Z");
654        let spans = vec![Span {
655            span_id,
656            trace_id,
657            start_timestamp,
658            ..Default::default()
659        }];
660        let transaction = Transaction {
661            event_id,
662            start_timestamp,
663            spans,
664            ..Default::default()
665        };
666        let envelope: Envelope = transaction.into();
667        assert_eq!(
668            to_str(envelope),
669            r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
670{"type":"transaction","length":200}
671{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]}
672"#
673        )
674    }
675
676    #[test]
677    fn test_monitor_checkin() {
678        let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
679
680        let check_in = MonitorCheckIn {
681            check_in_id,
682            monitor_slug: "my-monitor".into(),
683            status: MonitorCheckInStatus::Ok,
684            duration: Some(123.4),
685            environment: Some("production".into()),
686            monitor_config: Some(MonitorConfig {
687                schedule: MonitorSchedule::Crontab {
688                    value: "12 0 * * *".into(),
689                },
690                checkin_margin: Some(5),
691                max_runtime: Some(30),
692                timezone: Some("UTC".into()),
693                failure_issue_threshold: None,
694                recovery_threshold: None,
695            }),
696        };
697        let envelope: Envelope = check_in.into();
698        assert_eq!(
699            to_str(envelope),
700            r#"{}
701{"type":"check_in","length":259}
702{"check_in_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","monitor_slug":"my-monitor","status":"ok","environment":"production","duration":123.4,"monitor_config":{"schedule":{"type":"crontab","value":"12 0 * * *"},"checkin_margin":5,"max_runtime":30,"timezone":"UTC"}}
703"#
704        )
705    }
706
707    #[test]
708    fn test_monitor_checkin_with_thresholds() {
709        let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
710
711        let check_in = MonitorCheckIn {
712            check_in_id,
713            monitor_slug: "my-monitor".into(),
714            status: MonitorCheckInStatus::Ok,
715            duration: Some(123.4),
716            environment: Some("production".into()),
717            monitor_config: Some(MonitorConfig {
718                schedule: MonitorSchedule::Crontab {
719                    value: "12 0 * * *".into(),
720                },
721                checkin_margin: Some(5),
722                max_runtime: Some(30),
723                timezone: Some("UTC".into()),
724                failure_issue_threshold: Some(4),
725                recovery_threshold: Some(7),
726            }),
727        };
728        let envelope: Envelope = check_in.into();
729        assert_eq!(
730            to_str(envelope),
731            r#"{}
732{"type":"check_in","length":310}
733{"check_in_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","monitor_slug":"my-monitor","status":"ok","environment":"production","duration":123.4,"monitor_config":{"schedule":{"type":"crontab","value":"12 0 * * *"},"checkin_margin":5,"max_runtime":30,"timezone":"UTC","failure_issue_threshold":4,"recovery_threshold":7}}
734"#
735        )
736    }
737
738    #[test]
739    fn test_event_with_attachment() {
740        let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
741        let timestamp = timestamp("2020-07-20T14:51:14.296Z");
742        let event = Event {
743            event_id,
744            timestamp,
745            ..Default::default()
746        };
747        let mut envelope: Envelope = event.into();
748
749        envelope.add_item(Attachment {
750            buffer: "some content".as_bytes().to_vec(),
751            filename: "file.txt".to_string(),
752            ..Default::default()
753        });
754
755        assert_eq!(
756            to_str(envelope),
757            r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
758{"type":"event","length":74}
759{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
760{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"}
761some content
762"#
763        )
764    }
765
766    #[test]
767    fn test_deserialize_envelope_empty() {
768        // Without terminating newline after header
769        let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}";
770        let envelope = Envelope::from_slice(bytes).unwrap();
771
772        let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap();
773        assert_eq!(envelope.event_id, Some(event_id));
774        assert_eq!(envelope.items().count(), 0);
775    }
776
777    #[test]
778    fn test_deserialize_envelope_empty_newline() {
779        // With terminating newline after header
780        let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n";
781        let envelope = Envelope::from_slice(bytes).unwrap();
782        assert_eq!(envelope.items().count(), 0);
783    }
784
785    #[test]
786    fn test_deserialize_envelope_empty_item_newline() {
787        // With terminating newline after item payload
788        let bytes = b"\
789             {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
790             {\"type\":\"attachment\",\"length\":0}\n\
791             \n\
792             {\"type\":\"attachment\",\"length\":0}\n\
793             ";
794
795        let envelope = Envelope::from_slice(bytes).unwrap();
796        assert_eq!(envelope.items().count(), 2);
797
798        let mut items = envelope.items();
799
800        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
801            assert_eq!(attachment.buffer.len(), 0);
802        } else {
803            panic!("invalid item type");
804        }
805
806        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
807            assert_eq!(attachment.buffer.len(), 0);
808        } else {
809            panic!("invalid item type");
810        }
811    }
812
813    #[test]
814    fn test_deserialize_envelope_empty_item_eof() {
815        // With terminating newline after item payload
816        let bytes = b"\
817             {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
818             {\"type\":\"attachment\",\"length\":0}\n\
819             \n\
820             {\"type\":\"attachment\",\"length\":0}\
821             ";
822
823        let envelope = Envelope::from_slice(bytes).unwrap();
824        assert_eq!(envelope.items().count(), 2);
825
826        let mut items = envelope.items();
827
828        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
829            assert_eq!(attachment.buffer.len(), 0);
830        } else {
831            panic!("invalid item type");
832        }
833
834        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
835            assert_eq!(attachment.buffer.len(), 0);
836        } else {
837            panic!("invalid item type");
838        }
839    }
840
841    #[test]
842    fn test_deserialize_envelope_implicit_length() {
843        // With terminating newline after item payload
844        let bytes = b"\
845             {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
846             {\"type\":\"attachment\"}\n\
847             helloworld\n\
848             ";
849
850        let envelope = Envelope::from_slice(bytes).unwrap();
851        assert_eq!(envelope.items().count(), 1);
852
853        let mut items = envelope.items();
854
855        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
856            assert_eq!(attachment.buffer.len(), 10);
857        } else {
858            panic!("invalid item type");
859        }
860    }
861
862    #[test]
863    fn test_deserialize_envelope_implicit_length_eof() {
864        // With item ending the envelope
865        let bytes = b"\
866             {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
867             {\"type\":\"attachment\"}\n\
868             helloworld\
869             ";
870
871        let envelope = Envelope::from_slice(bytes).unwrap();
872        assert_eq!(envelope.items().count(), 1);
873
874        let mut items = envelope.items();
875
876        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
877            assert_eq!(attachment.buffer.len(), 10);
878        } else {
879            panic!("invalid item type");
880        }
881    }
882
883    #[test]
884    fn test_deserialize_envelope_implicit_length_empty_eof() {
885        // Empty item with implicit length ending the envelope
886        let bytes = b"\
887             {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
888             {\"type\":\"attachment\"}\
889             ";
890
891        let envelope = Envelope::from_slice(bytes).unwrap();
892        assert_eq!(envelope.items().count(), 1);
893
894        let mut items = envelope.items();
895
896        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
897            assert_eq!(attachment.buffer.len(), 0);
898        } else {
899            panic!("invalid item type");
900        }
901    }
902
903    #[test]
904    fn test_deserialize_envelope_multiple_items() {
905        // With terminating newline
906        let bytes = b"\
907            {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
908            {\"type\":\"attachment\",\"length\":10,\"content_type\":\"text/plain\",\"filename\":\"hello.txt\"}\n\
909            \xef\xbb\xbfHello\r\n\n\
910            {\"type\":\"event\",\"length\":41,\"content_type\":\"application/json\",\"filename\":\"application.log\"}\n\
911            {\"message\":\"hello world\",\"level\":\"error\"}\n\
912            ";
913
914        let envelope = Envelope::from_slice(bytes).unwrap();
915        assert_eq!(envelope.items().count(), 2);
916
917        let mut items = envelope.items();
918
919        if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
920            assert_eq!(attachment.buffer.len(), 10);
921            assert_eq!(attachment.buffer, b"\xef\xbb\xbfHello\r\n");
922            assert_eq!(attachment.filename, "hello.txt");
923            assert_eq!(attachment.content_type, Some("text/plain".to_string()));
924        } else {
925            panic!("invalid item type");
926        }
927
928        if let EnvelopeItem::Event(event) = items.next().unwrap() {
929            assert_eq!(event.message, Some("hello world".to_string()));
930            assert_eq!(event.level, Level::Error);
931        } else {
932            panic!("invalid item type");
933        }
934    }
935
936    // Test all possible item types in a single envelope
937    #[test]
938    fn test_deserialize_serialized() {
939        // Event
940        let event = Event {
941            event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
942            timestamp: timestamp("2020-07-20T14:51:14.296Z"),
943            ..Default::default()
944        };
945
946        // Transaction
947        let transaction = Transaction {
948            event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9d").unwrap(),
949            start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
950            spans: vec![Span {
951                span_id: "d42cee9fc3e74f5c".parse().unwrap(),
952                trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
953                start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
954                ..Default::default()
955            }],
956            ..Default::default()
957        };
958
959        // Session
960        let session = SessionUpdate {
961            session_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
962            distinct_id: Some("foo@bar.baz".to_owned()),
963            sequence: None,
964            timestamp: None,
965            started: timestamp("2020-07-20T14:51:14.296Z"),
966            init: true,
967            duration: Some(1.234),
968            status: SessionStatus::Ok,
969            errors: 123,
970            attributes: SessionAttributes {
971                release: "foo-bar@1.2.3".into(),
972                environment: Some("production".into()),
973                ip_address: None,
974                user_agent: None,
975            },
976        };
977
978        // Attachment
979        let attachment = Attachment {
980            buffer: "some content".as_bytes().to_vec(),
981            filename: "file.txt".to_string(),
982            ..Default::default()
983        };
984
985        let mut envelope: Envelope = Envelope::new();
986
987        envelope.add_item(event);
988        envelope.add_item(transaction);
989        envelope.add_item(session);
990        envelope.add_item(attachment);
991
992        let serialized = to_str(envelope);
993        let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
994        assert_eq!(serialized, to_str(deserialized))
995    }
996}