1use std::{io::Write, path::Path, time::SystemTime};
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::utils::ts_rfc3339_opt;
8use crate::Dsn;
9
10use super::v7 as protocol;
11
12use protocol::{
13 Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, MonitorCheckIn,
14 SessionAggregates, SessionUpdate, Transaction,
15};
16
17#[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}
131
132#[derive(Clone, Debug, Deserialize)]
134struct EnvelopeItemHeader {
135 r#type: EnvelopeItemType,
136 length: Option<usize>,
137 content_type: Option<String>,
139 filename: Option<String>,
141 attachment_type: Option<AttachmentType>,
142}
143
144#[derive(Clone, Debug, PartialEq)]
149#[non_exhaustive]
150#[allow(clippy::large_enum_variant)]
151pub enum EnvelopeItem {
152 Event(Event<'static>),
157 SessionUpdate(SessionUpdate<'static>),
162 SessionAggregates(SessionAggregates<'static>),
167 Transaction(Transaction<'static>),
172 Attachment(Attachment),
177 MonitorCheckIn(MonitorCheckIn),
179 ItemContainer(ItemContainer),
181 Raw,
183 }
186
187#[derive(Clone, Debug, PartialEq)]
191#[non_exhaustive]
192pub enum ItemContainer {
193 Logs(Vec<Log>),
195}
196
197#[allow(clippy::len_without_is_empty, reason = "is_empty is not needed")]
198impl ItemContainer {
199 pub fn len(&self) -> usize {
201 match self {
202 Self::Logs(logs) => logs.len(),
203 }
204 }
205
206 pub fn ty(&self) -> &'static str {
208 match self {
209 Self::Logs(_) => "log",
210 }
211 }
212
213 pub fn content_type(&self) -> &'static str {
215 match self {
216 Self::Logs(_) => "application/vnd.sentry.items.log+json",
217 }
218 }
219}
220
221impl From<Vec<Log>> for ItemContainer {
222 fn from(logs: Vec<Log>) -> Self {
223 Self::Logs(logs)
224 }
225}
226
227#[derive(Serialize)]
228struct LogsSerializationWrapper<'a> {
229 items: &'a [Log],
230}
231
232#[derive(Deserialize)]
233struct LogsDeserializationWrapper {
234 items: Vec<Log>,
235}
236
237impl From<Event<'static>> for EnvelopeItem {
238 fn from(event: Event<'static>) -> Self {
239 EnvelopeItem::Event(event)
240 }
241}
242
243impl From<SessionUpdate<'static>> for EnvelopeItem {
244 fn from(session: SessionUpdate<'static>) -> Self {
245 EnvelopeItem::SessionUpdate(session)
246 }
247}
248
249impl From<SessionAggregates<'static>> for EnvelopeItem {
250 fn from(aggregates: SessionAggregates<'static>) -> Self {
251 EnvelopeItem::SessionAggregates(aggregates)
252 }
253}
254
255impl From<Transaction<'static>> for EnvelopeItem {
256 fn from(transaction: Transaction<'static>) -> Self {
257 EnvelopeItem::Transaction(transaction)
258 }
259}
260
261impl From<Attachment> for EnvelopeItem {
262 fn from(attachment: Attachment) -> Self {
263 EnvelopeItem::Attachment(attachment)
264 }
265}
266
267impl From<MonitorCheckIn> for EnvelopeItem {
268 fn from(check_in: MonitorCheckIn) -> Self {
269 EnvelopeItem::MonitorCheckIn(check_in)
270 }
271}
272
273impl From<ItemContainer> for EnvelopeItem {
274 fn from(container: ItemContainer) -> Self {
275 EnvelopeItem::ItemContainer(container)
276 }
277}
278
279impl From<Vec<Log>> for EnvelopeItem {
280 fn from(logs: Vec<Log>) -> Self {
281 EnvelopeItem::ItemContainer(logs.into())
282 }
283}
284
285#[derive(Clone)]
287pub struct EnvelopeItemIter<'s> {
288 inner: std::slice::Iter<'s, EnvelopeItem>,
289}
290
291impl<'s> Iterator for EnvelopeItemIter<'s> {
292 type Item = &'s EnvelopeItem;
293
294 fn next(&mut self) -> Option<Self::Item> {
295 self.inner.next()
296 }
297}
298
299#[derive(Debug, Clone, PartialEq)]
304enum Items {
305 EnvelopeItems(Vec<EnvelopeItem>),
306 Raw(Vec<u8>),
307}
308
309impl Default for Items {
310 fn default() -> Self {
311 Self::EnvelopeItems(Default::default())
312 }
313}
314
315impl Items {
316 fn is_empty(&self) -> bool {
317 match self {
318 Items::EnvelopeItems(items) => items.is_empty(),
319 Items::Raw(bytes) => bytes.is_empty(),
320 }
321 }
322}
323
324#[derive(Clone, Default, Debug, PartialEq)]
333pub struct Envelope {
334 headers: EnvelopeHeaders,
335 items: Items,
336}
337
338impl Envelope {
339 pub fn new() -> Envelope {
341 Default::default()
342 }
343
344 pub fn add_item<I>(&mut self, item: I)
346 where
347 I: Into<EnvelopeItem>,
348 {
349 let item = item.into();
350
351 let Items::EnvelopeItems(ref mut items) = self.items else {
352 if item != EnvelopeItem::Raw {
353 eprintln!(
354 "WARNING: This envelope contains raw items. Adding an item is not supported."
355 );
356 }
357 return;
358 };
359
360 if self.headers.event_id.is_none() {
361 if let EnvelopeItem::Event(ref event) = item {
362 self.headers.event_id = Some(event.event_id);
363 } else if let EnvelopeItem::Transaction(ref transaction) = item {
364 self.headers.event_id = Some(transaction.event_id);
365 }
366 }
367 items.push(item);
368 }
369
370 pub fn items(&self) -> EnvelopeItemIter<'_> {
372 let inner = match &self.items {
373 Items::EnvelopeItems(items) => items.iter(),
374 Items::Raw(_) => [].iter(),
375 };
376
377 EnvelopeItemIter { inner }
378 }
379
380 pub fn headers(&self) -> &EnvelopeHeaders {
382 &self.headers
383 }
384
385 #[must_use]
387 pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self {
388 self.headers = headers;
389 self
390 }
391
392 pub fn uuid(&self) -> Option<&Uuid> {
394 self.headers.event_id.as_ref()
395 }
396
397 pub fn event(&self) -> Option<&Event<'static>> {
401 let Items::EnvelopeItems(ref items) = self.items else {
402 return None;
403 };
404
405 items.iter().find_map(|item| match item {
406 EnvelopeItem::Event(event) => Some(event),
407 _ => None,
408 })
409 }
410
411 pub fn filter<P>(self, mut predicate: P) -> Option<Self>
420 where
421 P: FnMut(&EnvelopeItem) -> bool,
422 {
423 let Items::EnvelopeItems(items) = self.items else {
424 return if predicate(&EnvelopeItem::Raw) {
425 Some(self)
426 } else {
427 None
428 };
429 };
430
431 let mut filtered = Envelope::new();
432 for item in items {
433 if predicate(&item) {
434 filtered.add_item(item);
435 }
436 }
437
438 if filtered.uuid().is_none() {
441 if let Items::EnvelopeItems(ref mut items) = filtered.items {
442 items.retain(|item| !matches!(item, EnvelopeItem::Attachment(..)))
443 }
444 }
445
446 if filtered.items.is_empty() {
447 None
448 } else {
449 Some(filtered)
450 }
451 }
452
453 pub fn to_writer<W>(&self, mut writer: W) -> std::io::Result<()>
457 where
458 W: Write,
459 {
460 let items = match &self.items {
461 Items::Raw(bytes) => return writer.write_all(bytes).map(|_| ()),
462 Items::EnvelopeItems(items) => items,
463 };
464
465 serde_json::to_writer(&mut writer, &self.headers)?;
467 writeln!(writer)?;
468
469 let mut item_buf = Vec::new();
470 for item in items {
472 match item {
474 EnvelopeItem::Event(event) => serde_json::to_writer(&mut item_buf, event)?,
475 EnvelopeItem::SessionUpdate(session) => {
476 serde_json::to_writer(&mut item_buf, session)?
477 }
478 EnvelopeItem::SessionAggregates(aggregates) => {
479 serde_json::to_writer(&mut item_buf, aggregates)?
480 }
481 EnvelopeItem::Transaction(transaction) => {
482 serde_json::to_writer(&mut item_buf, transaction)?
483 }
484 EnvelopeItem::Attachment(attachment) => {
485 attachment.to_writer(&mut writer)?;
486 writeln!(writer)?;
487 continue;
488 }
489 EnvelopeItem::MonitorCheckIn(check_in) => {
490 serde_json::to_writer(&mut item_buf, check_in)?
491 }
492 EnvelopeItem::ItemContainer(container) => match container {
493 ItemContainer::Logs(logs) => {
494 let wrapper = LogsSerializationWrapper { items: logs };
495 serde_json::to_writer(&mut item_buf, &wrapper)?
496 }
497 },
498 EnvelopeItem::Raw => {
499 continue;
500 }
501 }
502 let item_type = match item {
503 EnvelopeItem::Event(_) => "event",
504 EnvelopeItem::SessionUpdate(_) => "session",
505 EnvelopeItem::SessionAggregates(_) => "sessions",
506 EnvelopeItem::Transaction(_) => "transaction",
507 EnvelopeItem::MonitorCheckIn(_) => "check_in",
508 EnvelopeItem::ItemContainer(container) => container.ty(),
509 EnvelopeItem::Attachment(_) | EnvelopeItem::Raw => unreachable!(),
510 };
511
512 if let EnvelopeItem::ItemContainer(container) = item {
513 writeln!(
514 writer,
515 r#"{{"type":"{}","item_count":{},"content_type":"{}"}}"#,
516 item_type,
517 container.len(),
518 container.content_type()
519 )?;
520 } else {
521 writeln!(
522 writer,
523 r#"{{"type":"{}","length":{}}}"#,
524 item_type,
525 item_buf.len()
526 )?;
527 }
528 writer.write_all(&item_buf)?;
529 writeln!(writer)?;
530 item_buf.clear();
531 }
532
533 Ok(())
534 }
535
536 pub fn from_slice(slice: &[u8]) -> Result<Envelope, EnvelopeError> {
538 let (headers, offset) = Self::parse_headers(slice)?;
539 let items = Self::parse_items(slice, offset)?;
540
541 let mut envelope = Envelope {
542 headers,
543 ..Default::default()
544 };
545
546 for item in items {
547 envelope.add_item(item);
548 }
549
550 Ok(envelope)
551 }
552
553 pub fn from_bytes_raw(bytes: Vec<u8>) -> Result<Self, EnvelopeError> {
555 Ok(Self {
556 items: Items::Raw(bytes),
557 ..Default::default()
558 })
559 }
560
561 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Envelope, EnvelopeError> {
563 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
564 Envelope::from_slice(&bytes)
565 }
566
567 pub fn from_path_raw<P: AsRef<Path>>(path: P) -> Result<Self, EnvelopeError> {
572 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
573 Self::from_bytes_raw(bytes)
574 }
575
576 fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> {
577 let first_line = slice
578 .split(|b| *b == b'\n')
579 .next()
580 .ok_or(EnvelopeError::MissingHeader)?;
581
582 let headers: EnvelopeHeaders =
583 serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?;
584
585 let offset = first_line.len();
586 Self::require_termination(slice, offset)?;
587
588 Ok((headers, offset + 1))
589 }
590
591 fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
592 let mut items = Vec::new();
593
594 while offset < slice.len() {
595 let bytes = slice
596 .get(offset..)
597 .ok_or(EnvelopeError::MissingItemHeader)?;
598 let (item, item_size) = Self::parse_item(bytes)?;
599 offset += item_size;
600 items.push(item);
601 }
602
603 Ok(items)
604 }
605
606 fn parse_item(slice: &[u8]) -> Result<(EnvelopeItem, usize), EnvelopeError> {
607 let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
608
609 let header: EnvelopeItemHeader = match stream.next() {
610 None => return Err(EnvelopeError::UnexpectedEof),
611 Some(Err(error)) => return Err(EnvelopeError::InvalidItemHeader(error)),
612 Some(Ok(header)) => header,
613 };
614
615 let header_end = stream.byte_offset();
617 Self::require_termination(slice, header_end)?;
618
619 let payload_start = std::cmp::min(header_end + 1, slice.len());
622 let payload_end = match header.length {
623 Some(len) => {
624 let payload_end = payload_start + len;
625 if slice.len() < payload_end {
626 return Err(EnvelopeError::UnexpectedEof);
627 }
628
629 Self::require_termination(slice, payload_end)?;
631 payload_end
632 }
633 None => match slice.get(payload_start..) {
634 Some(range) => match range.iter().position(|&b| b == b'\n') {
635 Some(relative_end) => payload_start + relative_end,
636 None => slice.len(),
637 },
638 None => slice.len(),
639 },
640 };
641
642 let payload = slice.get(payload_start..payload_end).unwrap();
643
644 let item = match header.r#type {
645 EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event),
646 EnvelopeItemType::Transaction => {
647 serde_json::from_slice(payload).map(EnvelopeItem::Transaction)
648 }
649 EnvelopeItemType::SessionUpdate => {
650 serde_json::from_slice(payload).map(EnvelopeItem::SessionUpdate)
651 }
652 EnvelopeItemType::SessionAggregates => {
653 serde_json::from_slice(payload).map(EnvelopeItem::SessionAggregates)
654 }
655 EnvelopeItemType::Attachment => Ok(EnvelopeItem::Attachment(Attachment {
656 buffer: payload.to_owned(),
657 filename: header.filename.unwrap_or_default(),
658 content_type: header.content_type,
659 ty: header.attachment_type,
660 })),
661 EnvelopeItemType::MonitorCheckIn => {
662 serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
663 }
664 EnvelopeItemType::LogsContainer => {
665 serde_json::from_slice::<LogsDeserializationWrapper>(payload)
666 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items)))
667 }
668 }
669 .map_err(EnvelopeError::InvalidItemPayload)?;
670
671 Ok((item, payload_end + 1))
672 }
673
674 fn require_termination(slice: &[u8], offset: usize) -> Result<(), EnvelopeError> {
675 match slice.get(offset) {
676 Some(&b'\n') | None => Ok(()),
677 Some(_) => Err(EnvelopeError::MissingNewline),
678 }
679 }
680}
681
682impl<T> From<T> for Envelope
683where
684 T: Into<EnvelopeItem>,
685{
686 fn from(item: T) -> Self {
687 let mut envelope = Self::default();
688 envelope.add_item(item.into());
689 envelope
690 }
691}
692
693#[cfg(test)]
694mod test {
695 use std::str::FromStr;
696 use std::time::{Duration, SystemTime};
697
698 use protocol::Map;
699 use time::format_description::well_known::Rfc3339;
700 use time::OffsetDateTime;
701
702 use super::*;
703 use crate::protocol::v7::{
704 Level, MonitorCheckInStatus, MonitorConfig, MonitorSchedule, SampleRand, SessionAttributes,
705 SessionStatus, Span,
706 };
707
708 fn to_str(envelope: Envelope) -> String {
709 let mut vec = Vec::new();
710 envelope.to_writer(&mut vec).unwrap();
711 String::from_utf8_lossy(&vec).to_string()
712 }
713
714 fn timestamp(s: &str) -> SystemTime {
715 let dt = OffsetDateTime::parse(s, &Rfc3339).unwrap();
716 let secs = dt.unix_timestamp() as u64;
717 let nanos = dt.nanosecond();
718 let duration = Duration::new(secs, nanos);
719 SystemTime::UNIX_EPOCH.checked_add(duration).unwrap()
720 }
721
722 #[test]
723 fn test_empty() {
724 assert_eq!(to_str(Envelope::new()), "{}\n");
725 }
726
727 #[test]
728 fn raw_roundtrip() {
729 let buf = r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
730{"type":"event","length":74}
731{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
732"#;
733 let envelope = Envelope::from_bytes_raw(buf.to_string().into_bytes()).unwrap();
734 let serialized = to_str(envelope);
735 assert_eq!(&serialized, buf);
736
737 let random_invalid_bytes = b"oh stahp!\0\x01\x02";
738 let envelope = Envelope::from_bytes_raw(random_invalid_bytes.to_vec()).unwrap();
739 let mut serialized = Vec::new();
740 envelope.to_writer(&mut serialized).unwrap();
741 assert_eq!(&serialized, random_invalid_bytes);
742 }
743
744 #[test]
745 fn test_event() {
746 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
747 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
748 let event = Event {
749 event_id,
750 timestamp,
751 ..Default::default()
752 };
753 let envelope: Envelope = event.into();
754 assert_eq!(
755 to_str(envelope),
756 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
757{"type":"event","length":74}
758{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
759"#
760 )
761 }
762
763 #[test]
764 fn test_session() {
765 let session_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
766 let started = timestamp("2020-07-20T14:51:14.296Z");
767 let session = SessionUpdate {
768 session_id,
769 distinct_id: Some("foo@bar.baz".to_owned()),
770 sequence: None,
771 timestamp: None,
772 started,
773 init: true,
774 duration: Some(1.234),
775 status: SessionStatus::Ok,
776 errors: 123,
777 attributes: SessionAttributes {
778 release: "foo-bar@1.2.3".into(),
779 environment: Some("production".into()),
780 ip_address: None,
781 user_agent: None,
782 },
783 };
784 let mut envelope = Envelope::new();
785 envelope.add_item(session);
786 assert_eq!(
787 to_str(envelope),
788 r#"{}
789{"type":"session","length":222}
790{"sid":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","did":"foo@bar.baz","started":"2020-07-20T14:51:14.296Z","init":true,"duration":1.234,"status":"ok","errors":123,"attrs":{"release":"foo-bar@1.2.3","environment":"production"}}
791"#
792 )
793 }
794
795 #[test]
796 fn test_transaction() {
797 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
798 let span_id = "d42cee9fc3e74f5c".parse().unwrap();
799 let trace_id = "335e53d614474acc9f89e632b776cc28".parse().unwrap();
800 let start_timestamp = timestamp("2020-07-20T14:51:14.296Z");
801 let spans = vec![Span {
802 span_id,
803 trace_id,
804 start_timestamp,
805 ..Default::default()
806 }];
807 let transaction = Transaction {
808 event_id,
809 start_timestamp,
810 spans,
811 ..Default::default()
812 };
813 let envelope: Envelope = transaction.into();
814 assert_eq!(
815 to_str(envelope),
816 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
817{"type":"transaction","length":200}
818{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]}
819"#
820 )
821 }
822
823 #[test]
824 fn test_monitor_checkin() {
825 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
826
827 let check_in = MonitorCheckIn {
828 check_in_id,
829 monitor_slug: "my-monitor".into(),
830 status: MonitorCheckInStatus::Ok,
831 duration: Some(123.4),
832 environment: Some("production".into()),
833 monitor_config: Some(MonitorConfig {
834 schedule: MonitorSchedule::Crontab {
835 value: "12 0 * * *".into(),
836 },
837 checkin_margin: Some(5),
838 max_runtime: Some(30),
839 timezone: Some("UTC".into()),
840 failure_issue_threshold: None,
841 recovery_threshold: None,
842 }),
843 };
844 let envelope: Envelope = check_in.into();
845 assert_eq!(
846 to_str(envelope),
847 r#"{}
848{"type":"check_in","length":259}
849{"check_in_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","monitor_slug":"my-monitor","status":"ok","environment":"production","duration":123.4,"monitor_config":{"schedule":{"type":"crontab","value":"12 0 * * *"},"checkin_margin":5,"max_runtime":30,"timezone":"UTC"}}
850"#
851 )
852 }
853
854 #[test]
855 fn test_monitor_checkin_with_thresholds() {
856 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
857
858 let check_in = MonitorCheckIn {
859 check_in_id,
860 monitor_slug: "my-monitor".into(),
861 status: MonitorCheckInStatus::Ok,
862 duration: Some(123.4),
863 environment: Some("production".into()),
864 monitor_config: Some(MonitorConfig {
865 schedule: MonitorSchedule::Crontab {
866 value: "12 0 * * *".into(),
867 },
868 checkin_margin: Some(5),
869 max_runtime: Some(30),
870 timezone: Some("UTC".into()),
871 failure_issue_threshold: Some(4),
872 recovery_threshold: Some(7),
873 }),
874 };
875 let envelope: Envelope = check_in.into();
876 assert_eq!(
877 to_str(envelope),
878 r#"{}
879{"type":"check_in","length":310}
880{"check_in_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","monitor_slug":"my-monitor","status":"ok","environment":"production","duration":123.4,"monitor_config":{"schedule":{"type":"crontab","value":"12 0 * * *"},"checkin_margin":5,"max_runtime":30,"timezone":"UTC","failure_issue_threshold":4,"recovery_threshold":7}}
881"#
882 )
883 }
884
885 #[test]
886 fn test_event_with_attachment() {
887 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
888 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
889 let event = Event {
890 event_id,
891 timestamp,
892 ..Default::default()
893 };
894 let mut envelope: Envelope = event.into();
895
896 envelope.add_item(Attachment {
897 buffer: "some content".as_bytes().to_vec(),
898 filename: "file.txt".to_string(),
899 ..Default::default()
900 });
901
902 assert_eq!(
903 to_str(envelope),
904 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
905{"type":"event","length":74}
906{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
907{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"}
908some content
909"#
910 )
911 }
912
913 #[test]
914 fn test_deserialize_envelope_empty() {
915 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}";
917 let envelope = Envelope::from_slice(bytes).unwrap();
918
919 let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap();
920 assert_eq!(envelope.headers.event_id, Some(event_id));
921 assert_eq!(envelope.items().count(), 0);
922 }
923
924 #[test]
925 fn test_deserialize_envelope_empty_newline() {
926 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n";
928 let envelope = Envelope::from_slice(bytes).unwrap();
929 assert_eq!(envelope.items().count(), 0);
930 }
931
932 #[test]
933 fn test_deserialize_envelope_empty_item_newline() {
934 let bytes = b"\
936 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
937 {\"type\":\"attachment\",\"length\":0}\n\
938 \n\
939 {\"type\":\"attachment\",\"length\":0}\n\
940 ";
941
942 let envelope = Envelope::from_slice(bytes).unwrap();
943 assert_eq!(envelope.items().count(), 2);
944
945 let mut items = envelope.items();
946
947 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
948 assert_eq!(attachment.buffer.len(), 0);
949 } else {
950 panic!("invalid item type");
951 }
952
953 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
954 assert_eq!(attachment.buffer.len(), 0);
955 } else {
956 panic!("invalid item type");
957 }
958 }
959
960 #[test]
961 fn test_deserialize_envelope_empty_item_eof() {
962 let bytes = b"\
964 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
965 {\"type\":\"attachment\",\"length\":0}\n\
966 \n\
967 {\"type\":\"attachment\",\"length\":0}\
968 ";
969
970 let envelope = Envelope::from_slice(bytes).unwrap();
971 assert_eq!(envelope.items().count(), 2);
972
973 let mut items = envelope.items();
974
975 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
976 assert_eq!(attachment.buffer.len(), 0);
977 } else {
978 panic!("invalid item type");
979 }
980
981 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
982 assert_eq!(attachment.buffer.len(), 0);
983 } else {
984 panic!("invalid item type");
985 }
986 }
987
988 #[test]
989 fn test_deserialize_envelope_implicit_length() {
990 let bytes = b"\
992 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
993 {\"type\":\"attachment\"}\n\
994 helloworld\n\
995 ";
996
997 let envelope = Envelope::from_slice(bytes).unwrap();
998 assert_eq!(envelope.items().count(), 1);
999
1000 let mut items = envelope.items();
1001
1002 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1003 assert_eq!(attachment.buffer.len(), 10);
1004 } else {
1005 panic!("invalid item type");
1006 }
1007 }
1008
1009 #[test]
1010 fn test_deserialize_envelope_implicit_length_eof() {
1011 let bytes = b"\
1013 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1014 {\"type\":\"attachment\"}\n\
1015 helloworld\
1016 ";
1017
1018 let envelope = Envelope::from_slice(bytes).unwrap();
1019 assert_eq!(envelope.items().count(), 1);
1020
1021 let mut items = envelope.items();
1022
1023 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1024 assert_eq!(attachment.buffer.len(), 10);
1025 } else {
1026 panic!("invalid item type");
1027 }
1028 }
1029
1030 #[test]
1031 fn test_deserialize_envelope_implicit_length_empty_eof() {
1032 let bytes = b"\
1034 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1035 {\"type\":\"attachment\"}\
1036 ";
1037
1038 let envelope = Envelope::from_slice(bytes).unwrap();
1039 assert_eq!(envelope.items().count(), 1);
1040
1041 let mut items = envelope.items();
1042
1043 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1044 assert_eq!(attachment.buffer.len(), 0);
1045 } else {
1046 panic!("invalid item type");
1047 }
1048 }
1049
1050 #[test]
1051 fn test_deserialize_envelope_multiple_items() {
1052 let bytes = b"\
1054 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1055 {\"type\":\"attachment\",\"length\":10,\"content_type\":\"text/plain\",\"filename\":\"hello.txt\"}\n\
1056 \xef\xbb\xbfHello\r\n\n\
1057 {\"type\":\"event\",\"length\":41,\"content_type\":\"application/json\",\"filename\":\"application.log\"}\n\
1058 {\"message\":\"hello world\",\"level\":\"error\"}\n\
1059 ";
1060
1061 let envelope = Envelope::from_slice(bytes).unwrap();
1062 assert_eq!(envelope.items().count(), 2);
1063
1064 let mut items = envelope.items();
1065
1066 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1067 assert_eq!(attachment.buffer.len(), 10);
1068 assert_eq!(attachment.buffer, b"\xef\xbb\xbfHello\r\n");
1069 assert_eq!(attachment.filename, "hello.txt");
1070 assert_eq!(attachment.content_type, Some("text/plain".to_string()));
1071 } else {
1072 panic!("invalid item type");
1073 }
1074
1075 if let EnvelopeItem::Event(event) = items.next().unwrap() {
1076 assert_eq!(event.message, Some("hello world".to_string()));
1077 assert_eq!(event.level, Level::Error);
1078 } else {
1079 panic!("invalid item type");
1080 }
1081 }
1082
1083 #[test]
1084 fn test_all_envelope_headers_roundtrip() {
1085 let bytes = br#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c","sdk":{"name":"3e934135-3f2b-49bc-8756-9f025b55143e","version":"3e31738e-4106-42d0-8be2-4a3a1bc648d3","integrations":["daec50ae-8729-49b5-82f7-991446745cd5","8fc94968-3499-4a2c-b4d7-ecc058d9c1b0"],"packages":[{"name":"b59a1949-9950-4203-b394-ddd8d02c9633","version":"3d7790f3-7f32-43f7-b82f-9f5bc85205a8"}]},"sent_at":"2020-02-07T14:16:00Z","trace":{"trace_id":"65bcd18546c942069ed957b15b4ace7c","public_key":"5d593cac-f833-4845-bb23-4eabdf720da2","sample_rate":"0.00000021","sample_rand":"0.123456","sampled":"true","environment":"0666ab02-6364-4135-aa59-02e8128ce052","transaction":"0252ec25-cd0a-4230-bd2f-936a4585637e"}}
1086{"type":"event","length":74}
1087{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1088"#;
1089
1090 let envelope = Envelope::from_slice(bytes);
1091 assert!(envelope.is_ok());
1092 let envelope = envelope.unwrap();
1093 let serialized = to_str(envelope);
1094 assert_eq!(bytes, serialized.as_bytes());
1095 }
1096
1097 #[test]
1098 fn test_sample_rand_rounding() {
1099 let envelope = Envelope::new().with_headers(
1100 EnvelopeHeaders::new().with_trace(
1101 DynamicSamplingContext::new()
1102 .with_sample_rand(SampleRand::try_from(0.999_999_9).unwrap()),
1103 ),
1104 );
1105 let expected = br#"{"trace":{"sample_rand":"0.999999"}}
1106"#;
1107
1108 let serialized = to_str(envelope);
1109 assert_eq!(expected, serialized.as_bytes());
1110 }
1111
1112 #[test]
1114 fn test_deserialize_serialized() {
1115 let event = Event {
1117 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1118 timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1119 ..Default::default()
1120 };
1121
1122 let transaction = Transaction {
1124 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9d").unwrap(),
1125 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1126 spans: vec![Span {
1127 span_id: "d42cee9fc3e74f5c".parse().unwrap(),
1128 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1129 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1130 ..Default::default()
1131 }],
1132 ..Default::default()
1133 };
1134
1135 let session = SessionUpdate {
1137 session_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1138 distinct_id: Some("foo@bar.baz".to_owned()),
1139 sequence: None,
1140 timestamp: None,
1141 started: timestamp("2020-07-20T14:51:14.296Z"),
1142 init: true,
1143 duration: Some(1.234),
1144 status: SessionStatus::Ok,
1145 errors: 123,
1146 attributes: SessionAttributes {
1147 release: "foo-bar@1.2.3".into(),
1148 environment: Some("production".into()),
1149 ip_address: None,
1150 user_agent: None,
1151 },
1152 };
1153
1154 let attachment = Attachment {
1156 buffer: "some content".as_bytes().to_vec(),
1157 filename: "file.txt".to_string(),
1158 ..Default::default()
1159 };
1160
1161 let mut attributes = Map::new();
1162 attributes.insert("key".into(), "value".into());
1163 attributes.insert("num".into(), 10.into());
1164 attributes.insert("val".into(), 10.2.into());
1165 attributes.insert("bool".into(), false.into());
1166 let mut attributes_2 = attributes.clone();
1167 attributes_2.insert("more".into(), true.into());
1168 let logs: EnvelopeItem = vec![
1169 Log {
1170 level: protocol::LogLevel::Warn,
1171 body: "test".to_owned(),
1172 trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()),
1173 timestamp: timestamp("2022-07-25T14:51:14.296Z"),
1174 severity_number: Some(1.try_into().unwrap()),
1175 attributes,
1176 },
1177 Log {
1178 level: protocol::LogLevel::Error,
1179 body: "a body".to_owned(),
1180 trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
1181 timestamp: timestamp("2021-07-21T14:51:14.296Z"),
1182 severity_number: Some(1.try_into().unwrap()),
1183 attributes: attributes_2,
1184 },
1185 ]
1186 .into();
1187
1188 let mut envelope: Envelope = Envelope::new();
1189 envelope.add_item(event);
1190 envelope.add_item(transaction);
1191 envelope.add_item(session);
1192 envelope.add_item(attachment);
1193 envelope.add_item(logs);
1194
1195 let serialized = to_str(envelope);
1196 let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
1197 assert_eq!(serialized, to_str(deserialized))
1198 }
1199}