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