Skip to main content

sentry_types/protocol/
envelope.rs

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