sentry_types/protocol/
envelope.rs

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