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 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 let header_end = stream.byte_offset();
656 Self::require_termination(slice, header_end)?;
657
658 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 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 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 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 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 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 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 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 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 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]
1201 fn test_deserialize_serialized() {
1202 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 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 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 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}