sentry_types/protocol/
envelope.rs

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