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