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#[derive(Debug, Error)]
19pub enum EnvelopeError {
20 #[error("unexpected end of file")]
22 UnexpectedEof,
23 #[error("missing envelope header")]
25 MissingHeader,
26 #[error("missing item header")]
28 MissingItemHeader,
29 #[error("missing newline after header or payload")]
31 MissingNewline,
32 #[error("invalid envelope header")]
34 InvalidHeader(#[source] serde_json::Error),
35 #[error("invalid item header")]
37 InvalidItemHeader(#[source] serde_json::Error),
38 #[error("invalid item payload")]
40 InvalidItemPayload(#[source] serde_json::Error),
41}
42
43#[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 pub fn new() -> EnvelopeHeaders {
65 Default::default()
66 }
67
68 #[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 #[must_use]
77 pub fn with_dsn(mut self, dsn: Dsn) -> Self {
78 self.dsn = Some(dsn);
79 self
80 }
81
82 #[must_use]
84 pub fn with_sdk(mut self, sdk: ClientSdkInfo) -> Self {
85 self.sdk = Some(sdk);
86 self
87 }
88
89 #[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 #[must_use]
99 pub fn with_trace(mut self, trace: DynamicSamplingContext) -> Self {
100 self.trace = Some(trace);
101 self
102 }
103}
104
105#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
107#[non_exhaustive]
108enum EnvelopeItemType {
109 #[serde(rename = "event")]
111 Event,
112 #[serde(rename = "session")]
114 SessionUpdate,
115 #[serde(rename = "sessions")]
117 SessionAggregates,
118 #[serde(rename = "transaction")]
120 Transaction,
121 #[serde(rename = "attachment")]
123 Attachment,
124 #[serde(rename = "check_in")]
126 MonitorCheckIn,
127 #[serde(rename = "log")]
129 LogsContainer,
130 #[serde(rename = "trace_metric")]
133 MetricsContainer,
134}
135
136#[derive(Clone, Debug, Deserialize)]
138struct EnvelopeItemHeader {
139 r#type: EnvelopeItemType,
140 length: Option<usize>,
141 content_type: Option<String>,
143 filename: Option<String>,
145 attachment_type: Option<AttachmentType>,
146}
147
148#[derive(Clone, Debug, PartialEq)]
153#[non_exhaustive]
154#[allow(clippy::large_enum_variant)]
155pub enum EnvelopeItem {
156 Event(Event<'static>),
161 SessionUpdate(SessionUpdate<'static>),
166 SessionAggregates(SessionAggregates<'static>),
171 Transaction(Transaction<'static>),
176 Attachment(Attachment),
181 MonitorCheckIn(MonitorCheckIn),
183 ItemContainer(ItemContainer),
185 Raw,
187 }
190
191#[derive(Clone, Debug, PartialEq)]
195#[non_exhaustive]
196pub enum ItemContainer {
197 Logs(Vec<Log>),
199 Metrics(Vec<Metric>),
201}
202
203#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")]
204impl ItemContainer {
205 pub fn len(&self) -> usize {
207 match self {
208 Self::Logs(logs) => logs.len(),
209 Self::Metrics(metrics) => metrics.len(),
210 }
211 }
212
213 pub fn ty(&self) -> &'static str {
215 match self {
216 Self::Logs(_) => "log",
217 Self::Metrics(_) => "trace_metric",
218 }
219 }
220
221 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#[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#[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#[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#[derive(Clone, Default, Debug, PartialEq)]
355pub struct Envelope {
356 headers: EnvelopeHeaders,
357 items: Items,
358}
359
360impl Envelope {
361 pub fn new() -> Envelope {
363 Default::default()
364 }
365
366 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 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 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 pub fn headers(&self) -> &EnvelopeHeaders {
415 &self.headers
416 }
417
418 #[must_use]
420 pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self {
421 self.headers = headers;
422 self
423 }
424
425 pub fn uuid(&self) -> Option<&Uuid> {
427 self.headers.event_id.as_ref()
428 }
429
430 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 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 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 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 serde_json::to_writer(&mut writer, &self.headers)?;
500 writeln!(writer)?;
501
502 let mut item_buf = Vec::new();
503 for item in items {
505 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 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 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 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 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 let byte_after_header = offset
630 .checked_add(1)
631 .expect("offset is at most isize::MAX; adding 1 cannot overflow usize");
632
633 Ok((headers, byte_after_header))
634 }
635
636 fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
637 let mut items = Vec::new();
638
639 while offset < slice.len() {
640 let bytes = slice
641 .get(offset..)
642 .ok_or(EnvelopeError::MissingItemHeader)?;
643 let (item, item_offset) = Self::parse_item(bytes)?;
644 offset = offset
648 .checked_add(item_offset)
649 .expect("offset + item_offset is at most slice.len() + 1");
650 items.push(item);
651 }
652
653 Ok(items)
654 }
655
656 fn parse_item(slice: &[u8]) -> Result<(EnvelopeItem, usize), EnvelopeError> {
664 let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
665
666 let header: EnvelopeItemHeader = match stream.next() {
667 None => return Err(EnvelopeError::UnexpectedEof),
668 Some(Err(error)) => return Err(EnvelopeError::InvalidItemHeader(error)),
669 Some(Ok(header)) => header,
670 };
671
672 let header_end = stream.byte_offset();
674 Self::require_termination(slice, header_end)?;
675
676 let payload_start = std::cmp::min(header_end, slice.len().saturating_sub(1))
681 .checked_add(1)
682 .expect("&[u8] length is at most isize::MAX; adding one cannot overflow usize");
683 let payload_end = match header.length {
684 Some(len) => {
685 let payload_end = payload_start.saturating_add(len);
686 if slice.len() < payload_end {
687 return Err(EnvelopeError::UnexpectedEof);
688 }
689
690 Self::require_termination(slice, payload_end)?;
692 payload_end
693 }
694 None => match slice.get(payload_start..) {
695 Some(range) => match range.iter().position(|&b| b == b'\n') {
696 Some(relative_end) => payload_start.checked_add(relative_end).expect(
697 "relative_end is an index into slice[payload_start..]; \
698 so the sum is within slice and cannot overflow usize",
699 ),
700 None => slice.len(),
701 },
702 None => slice.len(),
703 },
704 };
705
706 let payload = slice.get(payload_start..payload_end).unwrap();
707
708 let item = match header.r#type {
709 EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event),
710 EnvelopeItemType::Transaction => {
711 serde_json::from_slice(payload).map(EnvelopeItem::Transaction)
712 }
713 EnvelopeItemType::SessionUpdate => {
714 serde_json::from_slice(payload).map(EnvelopeItem::SessionUpdate)
715 }
716 EnvelopeItemType::SessionAggregates => {
717 serde_json::from_slice(payload).map(EnvelopeItem::SessionAggregates)
718 }
719 EnvelopeItemType::Attachment => Ok(EnvelopeItem::Attachment(Attachment {
720 buffer: payload.to_owned(),
721 filename: header.filename.unwrap_or_default(),
722 content_type: header.content_type,
723 ty: header.attachment_type,
724 })),
725 EnvelopeItemType::MonitorCheckIn => {
726 serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
727 }
728 EnvelopeItemType::LogsContainer => {
729 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
730 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
731 }
732 EnvelopeItemType::MetricsContainer => {
733 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
734 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Metrics(x.items.into())))
735 }
736 }
737 .map_err(EnvelopeError::InvalidItemPayload)?;
738
739 let byte_after_payload = payload_end
742 .checked_add(1)
743 .expect("payload_end <= slice.len() <= isize::MAX, so adding 1 cannot overflow usize");
744
745 Ok((item, byte_after_payload))
746 }
747
748 fn require_termination(slice: &[u8], offset: usize) -> Result<(), EnvelopeError> {
749 match slice.get(offset) {
750 Some(&b'\n') | None => Ok(()),
751 Some(_) => Err(EnvelopeError::MissingNewline),
752 }
753 }
754}
755
756impl<T> From<T> for Envelope
757where
758 T: Into<EnvelopeItem>,
759{
760 fn from(item: T) -> Self {
761 let mut envelope = Self::default();
762 envelope.add_item(item.into());
763 envelope
764 }
765}
766
767#[cfg(test)]
768mod test {
769 use std::str::FromStr;
770 use std::time::{Duration, SystemTime};
771
772 use protocol::Map;
773 use serde_json::Value;
774 use time::format_description::well_known::Rfc3339;
775 use time::OffsetDateTime;
776
777 use super::*;
778 use crate::protocol::v7::{
779 Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SampleRand, SessionAttributes,
780 SessionStatus, Span,
781 };
782
783 fn to_str(envelope: Envelope) -> String {
784 let mut vec = Vec::new();
785 envelope.to_writer(&mut vec).unwrap();
786 String::from_utf8_lossy(&vec).to_string()
787 }
788
789 fn timestamp(s: &str) -> SystemTime {
790 let dt = OffsetDateTime::parse(s, &Rfc3339).unwrap();
791 let secs = dt.unix_timestamp() as u64;
792 let nanos = dt.nanosecond();
793 let duration = Duration::new(secs, nanos);
794 SystemTime::UNIX_EPOCH.checked_add(duration).unwrap()
795 }
796
797 #[test]
798 fn test_empty() {
799 assert_eq!(to_str(Envelope::new()), "{}\n");
800 }
801
802 #[test]
803 fn raw_roundtrip() {
804 let buf = r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
805{"type":"event","length":74}
806{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
807"#;
808 let envelope = Envelope::from_bytes_raw(buf.to_string().into_bytes()).unwrap();
809 let serialized = to_str(envelope);
810 assert_eq!(&serialized, buf);
811
812 let random_invalid_bytes = b"oh stahp!\0\x01\x02";
813 let envelope = Envelope::from_bytes_raw(random_invalid_bytes.to_vec()).unwrap();
814 let mut serialized = Vec::new();
815 envelope.to_writer(&mut serialized).unwrap();
816 assert_eq!(&serialized, random_invalid_bytes);
817 }
818
819 #[test]
820 fn test_event() {
821 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
822 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
823 let event = Event {
824 event_id,
825 timestamp,
826 ..Default::default()
827 };
828 let envelope: Envelope = event.into();
829 assert_eq!(
830 to_str(envelope),
831 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
832{"type":"event","length":74}
833{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
834"#
835 )
836 }
837
838 #[test]
839 fn test_session() {
840 let session_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
841 let started = timestamp("2020-07-20T14:51:14.296Z");
842 let session = SessionUpdate {
843 session_id,
844 distinct_id: Some("foo@bar.baz".to_owned()),
845 sequence: None,
846 timestamp: None,
847 started,
848 init: true,
849 duration: Some(1.234),
850 status: SessionStatus::Ok,
851 errors: 123,
852 attributes: SessionAttributes {
853 release: "foo-bar@1.2.3".into(),
854 environment: Some("production".into()),
855 ip_address: None,
856 user_agent: None,
857 },
858 };
859 let mut envelope = Envelope::new();
860 envelope.add_item(session);
861 assert_eq!(
862 to_str(envelope),
863 r#"{}
864{"type":"session","length":222}
865{"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"}}
866"#
867 )
868 }
869
870 #[test]
871 fn test_transaction() {
872 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
873 let span_id = "d42cee9fc3e74f5c".parse().unwrap();
874 let trace_id = "335e53d614474acc9f89e632b776cc28".parse().unwrap();
875 let start_timestamp = timestamp("2020-07-20T14:51:14.296Z");
876 let spans = vec![Span {
877 span_id,
878 trace_id,
879 start_timestamp,
880 ..Default::default()
881 }];
882 let transaction = Transaction {
883 event_id,
884 start_timestamp,
885 spans,
886 ..Default::default()
887 };
888 let envelope: Envelope = transaction.into();
889 assert_eq!(
890 to_str(envelope),
891 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
892{"type":"transaction","length":200}
893{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]}
894"#
895 )
896 }
897
898 #[test]
899 fn test_monitor_checkin() {
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: None,
916 recovery_threshold: None,
917 }),
918 };
919 let envelope: Envelope = check_in.into();
920 assert_eq!(
921 to_str(envelope),
922 r#"{}
923{"type":"check_in","length":259}
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"}}
925"#
926 )
927 }
928
929 #[test]
930 fn test_monitor_checkin_with_thresholds() {
931 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
932
933 let check_in = MonitorCheckIn {
934 check_in_id,
935 monitor_slug: "my-monitor".into(),
936 status: MonitorCheckInStatus::Ok,
937 duration: Some(123.4),
938 environment: Some("production".into()),
939 monitor_config: Some(MonitorConfig {
940 schedule: MonitorSchedule::Crontab {
941 value: "12 0 * * *".into(),
942 },
943 checkin_margin: Some(5),
944 max_runtime: Some(30),
945 timezone: Some("UTC".into()),
946 failure_issue_threshold: Some(4),
947 recovery_threshold: Some(7),
948 }),
949 };
950 let envelope: Envelope = check_in.into();
951 assert_eq!(
952 to_str(envelope),
953 r#"{}
954{"type":"check_in","length":310}
955{"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}}
956"#
957 )
958 }
959
960 #[test]
961 fn test_event_with_attachment() {
962 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
963 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
964 let event = Event {
965 event_id,
966 timestamp,
967 ..Default::default()
968 };
969 let mut envelope: Envelope = event.into();
970
971 envelope.add_item(Attachment {
972 buffer: "some content".as_bytes().to_vec(),
973 filename: "file.txt".to_string(),
974 ..Default::default()
975 });
976
977 assert_eq!(
978 to_str(envelope),
979 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
980{"type":"event","length":74}
981{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
982{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"}
983some content
984"#
985 )
986 }
987
988 #[test]
989 fn test_deserialize_envelope_empty() {
990 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}";
992 let envelope = Envelope::from_slice(bytes).unwrap();
993
994 let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap();
995 assert_eq!(envelope.headers.event_id, Some(event_id));
996 assert_eq!(envelope.items().count(), 0);
997 }
998
999 #[test]
1000 fn test_deserialize_envelope_empty_newline() {
1001 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n";
1003 let envelope = Envelope::from_slice(bytes).unwrap();
1004 assert_eq!(envelope.items().count(), 0);
1005 }
1006
1007 #[test]
1008 fn test_deserialize_envelope_empty_item_newline() {
1009 let bytes = b"\
1011 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1012 {\"type\":\"attachment\",\"length\":0}\n\
1013 \n\
1014 {\"type\":\"attachment\",\"length\":0}\n\
1015 ";
1016
1017 let envelope = Envelope::from_slice(bytes).unwrap();
1018 assert_eq!(envelope.items().count(), 2);
1019
1020 let mut items = envelope.items();
1021
1022 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1023 assert_eq!(attachment.buffer.len(), 0);
1024 } else {
1025 panic!("invalid item type");
1026 }
1027
1028 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1029 assert_eq!(attachment.buffer.len(), 0);
1030 } else {
1031 panic!("invalid item type");
1032 }
1033 }
1034
1035 #[test]
1036 fn test_deserialize_envelope_empty_item_eof() {
1037 let bytes = b"\
1039 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1040 {\"type\":\"attachment\",\"length\":0}\n\
1041 \n\
1042 {\"type\":\"attachment\",\"length\":0}\
1043 ";
1044
1045 let envelope = Envelope::from_slice(bytes).unwrap();
1046 assert_eq!(envelope.items().count(), 2);
1047
1048 let mut items = envelope.items();
1049
1050 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1051 assert_eq!(attachment.buffer.len(), 0);
1052 } else {
1053 panic!("invalid item type");
1054 }
1055
1056 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1057 assert_eq!(attachment.buffer.len(), 0);
1058 } else {
1059 panic!("invalid item type");
1060 }
1061 }
1062
1063 #[test]
1064 fn test_deserialize_envelope_implicit_length() {
1065 let bytes = b"\
1067 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1068 {\"type\":\"attachment\"}\n\
1069 helloworld\n\
1070 ";
1071
1072 let envelope = Envelope::from_slice(bytes).unwrap();
1073 assert_eq!(envelope.items().count(), 1);
1074
1075 let mut items = envelope.items();
1076
1077 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1078 assert_eq!(attachment.buffer.len(), 10);
1079 } else {
1080 panic!("invalid item type");
1081 }
1082 }
1083
1084 #[test]
1085 fn test_deserialize_envelope_implicit_length_eof() {
1086 let bytes = b"\
1088 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1089 {\"type\":\"attachment\"}\n\
1090 helloworld\
1091 ";
1092
1093 let envelope = Envelope::from_slice(bytes).unwrap();
1094 assert_eq!(envelope.items().count(), 1);
1095
1096 let mut items = envelope.items();
1097
1098 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1099 assert_eq!(attachment.buffer.len(), 10);
1100 } else {
1101 panic!("invalid item type");
1102 }
1103 }
1104
1105 #[test]
1106 fn test_deserialize_envelope_implicit_length_empty_eof() {
1107 let bytes = b"\
1109 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1110 {\"type\":\"attachment\"}\
1111 ";
1112
1113 let envelope = Envelope::from_slice(bytes).unwrap();
1114 assert_eq!(envelope.items().count(), 1);
1115
1116 let mut items = envelope.items();
1117
1118 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1119 assert_eq!(attachment.buffer.len(), 0);
1120 } else {
1121 panic!("invalid item type");
1122 }
1123 }
1124
1125 #[test]
1126 fn test_deserialize_envelope_multiple_items() {
1127 let bytes = b"\
1129 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1130 {\"type\":\"attachment\",\"length\":10,\"content_type\":\"text/plain\",\"filename\":\"hello.txt\"}\n\
1131 \xef\xbb\xbfHello\r\n\n\
1132 {\"type\":\"event\",\"length\":41,\"content_type\":\"application/json\",\"filename\":\"application.log\"}\n\
1133 {\"message\":\"hello world\",\"level\":\"error\"}\n\
1134 ";
1135
1136 let envelope = Envelope::from_slice(bytes).unwrap();
1137 assert_eq!(envelope.items().count(), 2);
1138
1139 let mut items = envelope.items();
1140
1141 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1142 assert_eq!(attachment.buffer.len(), 10);
1143 assert_eq!(attachment.buffer, b"\xef\xbb\xbfHello\r\n");
1144 assert_eq!(attachment.filename, "hello.txt");
1145 assert_eq!(attachment.content_type, Some("text/plain".to_string()));
1146 } else {
1147 panic!("invalid item type");
1148 }
1149
1150 if let EnvelopeItem::Event(event) = items.next().unwrap() {
1151 assert_eq!(event.message, Some("hello world".to_string()));
1152 assert_eq!(event.level, Level::Error);
1153 } else {
1154 panic!("invalid item type");
1155 }
1156 }
1157
1158 #[test]
1159 fn test_all_envelope_headers_roundtrip() {
1160 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"}}
1161{"type":"event","length":74}
1162{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1163"#;
1164
1165 let envelope = Envelope::from_slice(bytes);
1166 assert!(envelope.is_ok());
1167 let envelope = envelope.unwrap();
1168 let serialized = to_str(envelope);
1169 assert_eq!(bytes, serialized.as_bytes());
1170 }
1171
1172 #[test]
1173 fn test_sample_rand_rounding() {
1174 let envelope = Envelope::new().with_headers(
1175 EnvelopeHeaders::new().with_trace(
1176 DynamicSamplingContext::new()
1177 .with_sample_rand(SampleRand::try_from(0.999_999_9).unwrap()),
1178 ),
1179 );
1180 let expected = br#"{"trace":{"sample_rand":"0.999999"}}
1181"#;
1182
1183 let serialized = to_str(envelope);
1184 assert_eq!(expected, serialized.as_bytes());
1185 }
1186
1187 #[test]
1188 fn test_metric_container_header() {
1189 let metrics: EnvelopeItem = vec![Metric {
1190 r#type: protocol::MetricType::Counter,
1191 name: "api.requests".into(),
1192 value: 1.0,
1193 timestamp: timestamp("2026-03-02T13:36:02.000Z"),
1194 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1195 span_id: None,
1196 unit: None,
1197 attributes: Map::new(),
1198 }]
1199 .into();
1200
1201 let mut envelope = Envelope::new();
1202 envelope.add_item(metrics);
1203
1204 let expected = [
1205 serde_json::json!({}),
1206 serde_json::json!({
1207 "type": "trace_metric",
1208 "item_count": 1,
1209 "content_type": "application/vnd.sentry.items.trace-metric+json"
1210 }),
1211 serde_json::json!({
1212 "items": [{
1213 "type": "counter",
1214 "name": "api.requests",
1215 "value": 1.0,
1216 "timestamp": 1772458562,
1217 "trace_id": "335e53d614474acc9f89e632b776cc28"
1218 }]
1219 }),
1220 ];
1221
1222 let serialized = to_str(envelope);
1223 let actual = serialized
1224 .lines()
1225 .map(|line| serde_json::from_str::<Value>(line).expect("envelope has invalid JSON"));
1226
1227 assert!(actual.eq(expected.into_iter()));
1228 }
1229
1230 #[test]
1232 fn test_deserialize_serialized() {
1233 let event = Event {
1235 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1236 timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1237 ..Default::default()
1238 };
1239
1240 let transaction = Transaction {
1242 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9d").unwrap(),
1243 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1244 spans: vec![Span {
1245 span_id: "d42cee9fc3e74f5c".parse().unwrap(),
1246 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1247 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1248 ..Default::default()
1249 }],
1250 ..Default::default()
1251 };
1252
1253 let session = SessionUpdate {
1255 session_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1256 distinct_id: Some("foo@bar.baz".to_owned()),
1257 sequence: None,
1258 timestamp: None,
1259 started: timestamp("2020-07-20T14:51:14.296Z"),
1260 init: true,
1261 duration: Some(1.234),
1262 status: SessionStatus::Ok,
1263 errors: 123,
1264 attributes: SessionAttributes {
1265 release: "foo-bar@1.2.3".into(),
1266 environment: Some("production".into()),
1267 ip_address: None,
1268 user_agent: None,
1269 },
1270 };
1271
1272 let attachment = Attachment {
1274 buffer: "some content".as_bytes().to_vec(),
1275 filename: "file.txt".to_string(),
1276 ..Default::default()
1277 };
1278
1279 let mut attributes = Map::new();
1280 attributes.insert("key".into(), "value".into());
1281 attributes.insert("num".into(), 10.into());
1282 attributes.insert("val".into(), 10.2.into());
1283 attributes.insert("bool".into(), false.into());
1284 let mut attributes_2 = attributes.clone();
1285 attributes_2.insert("more".into(), true.into());
1286 let logs: EnvelopeItem = vec![
1287 Log {
1288 level: protocol::LogLevel::Warn,
1289 body: "test".to_owned(),
1290 trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()),
1291 timestamp: timestamp("2022-07-25T14:51:14.296Z"),
1292 severity_number: Some(1.try_into().unwrap()),
1293 attributes,
1294 },
1295 Log {
1296 level: protocol::LogLevel::Error,
1297 body: "a body".to_owned(),
1298 trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
1299 timestamp: timestamp("2021-07-21T14:51:14.296Z"),
1300 severity_number: Some(1.try_into().unwrap()),
1301 attributes: attributes_2,
1302 },
1303 ]
1304 .into();
1305
1306 let mut metric_attributes = Map::new();
1307 metric_attributes.insert("route".into(), "/users".into());
1308 let metrics: EnvelopeItem = vec![Metric {
1309 r#type: protocol::MetricType::Distribution,
1310 name: "response.time".into(),
1311 value: 123.4,
1312 timestamp: timestamp("2022-07-26T14:51:14.296Z"),
1313 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1314 span_id: Some("d42cee9fc3e74f5c".parse().unwrap()),
1315 unit: Some("millisecond".into()),
1316 attributes: metric_attributes,
1317 }]
1318 .into();
1319
1320 let mut envelope: Envelope = Envelope::new();
1321 envelope.add_item(event);
1322 envelope.add_item(transaction);
1323 envelope.add_item(session);
1324 envelope.add_item(attachment);
1325 envelope.add_item(logs);
1326 envelope.add_item(metrics);
1327
1328 let serialized = to_str(envelope);
1329 let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
1330 assert_eq!(serialized, to_str(deserialized))
1331 }
1332}