1use std::mem;
2use std::{borrow::Cow, io::Write, path::Path, time::SystemTime};
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use uuid::Uuid;
7
8use crate::Dsn;
9use crate::{protocol::v7::ClientReport, utils::ts_rfc3339_opt};
10
11use super::v7 as protocol;
12
13use protocol::{
14 Attachment, AttachmentType, ClientSdkInfo, DynamicSamplingContext, Event, Log, Metric,
15 MonitorCheckIn, SessionAggregates, SessionUpdate, Transaction,
16};
17
18#[derive(Debug, Error)]
20#[non_exhaustive]
21pub enum EnvelopeError {
22 #[error("unexpected end of file")]
24 UnexpectedEof,
25 #[error("missing envelope header")]
27 MissingHeader,
28 #[error("missing item header")]
30 MissingItemHeader,
31 #[error("missing newline after header or payload")]
33 MissingNewline,
34 #[error("invalid envelope header")]
36 InvalidHeader(#[source] serde_json::Error),
37 #[error("invalid item header")]
39 InvalidItemHeader(#[source] serde_json::Error),
40 #[error("invalid item payload")]
42 InvalidItemPayload(#[source] serde_json::Error),
43}
44
45#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)]
47pub struct EnvelopeHeaders {
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 event_id: Option<Uuid>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 dsn: Option<Dsn>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 sdk: Option<ClientSdkInfo>,
54 #[serde(
55 default,
56 skip_serializing_if = "Option::is_none",
57 with = "ts_rfc3339_opt"
58 )]
59 sent_at: Option<SystemTime>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 trace: Option<DynamicSamplingContext>,
62}
63
64impl EnvelopeHeaders {
65 pub fn new() -> EnvelopeHeaders {
67 Default::default()
68 }
69
70 #[must_use]
72 pub fn with_event_id(mut self, event_id: Uuid) -> Self {
73 self.event_id = Some(event_id);
74 self
75 }
76
77 #[must_use]
79 pub fn with_dsn(mut self, dsn: Dsn) -> Self {
80 self.dsn = Some(dsn);
81 self
82 }
83
84 #[must_use]
86 pub fn with_sdk(mut self, sdk: ClientSdkInfo) -> Self {
87 self.sdk = Some(sdk);
88 self
89 }
90
91 #[must_use]
94 pub fn with_sent_at(mut self, sent_at: SystemTime) -> Self {
95 self.sent_at = Some(sent_at);
96 self
97 }
98
99 #[must_use]
101 pub fn with_trace(mut self, trace: DynamicSamplingContext) -> Self {
102 self.trace = Some(trace);
103 self
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
109#[non_exhaustive]
110enum EnvelopeItemType {
111 #[serde(rename = "event")]
113 Event,
114 #[serde(rename = "session")]
116 SessionUpdate,
117 #[serde(rename = "sessions")]
119 SessionAggregates,
120 #[serde(rename = "transaction")]
122 Transaction,
123 #[serde(rename = "attachment")]
125 Attachment,
126 #[serde(rename = "check_in")]
128 MonitorCheckIn,
129 #[serde(rename = "log")]
131 LogsContainer,
132 #[serde(rename = "trace_metric")]
135 MetricsContainer,
136 #[serde(rename = "client_report")]
138 ClientReport,
139}
140
141#[derive(Clone, Debug, Deserialize)]
143struct EnvelopeItemHeader {
144 r#type: EnvelopeItemType,
145 length: Option<usize>,
146 content_type: Option<String>,
148 filename: Option<String>,
150 attachment_type: Option<AttachmentType>,
151}
152
153#[derive(Clone, Debug, PartialEq)]
158#[non_exhaustive]
159pub enum EnvelopeItem {
160 Event(Box<Event<'static>>),
165 SessionUpdate(SessionUpdate<'static>),
170 SessionAggregates(SessionAggregates<'static>),
175 Transaction(Box<Transaction<'static>>),
180 Attachment(Attachment),
185 MonitorCheckIn(MonitorCheckIn),
187 ClientReport(ClientReport),
189 ItemContainer(ItemContainer),
191 Raw,
193 }
196
197#[derive(Clone, Debug, PartialEq)]
201#[non_exhaustive]
202pub enum ItemContainer {
203 Logs(Vec<Log>),
205 Metrics(Vec<Metric>),
207}
208
209impl ItemContainer {
210 pub fn len(&self) -> usize {
212 match self {
213 Self::Logs(logs) => logs.len(),
214 Self::Metrics(metrics) => metrics.len(),
215 }
216 }
217
218 pub fn ty(&self) -> &'static str {
220 match self {
221 Self::Logs(_) => "log",
222 Self::Metrics(_) => "trace_metric",
223 }
224 }
225
226 pub fn content_type(&self) -> &'static str {
228 match self {
229 Self::Logs(_) => "application/vnd.sentry.items.log+json",
230 Self::Metrics(_) => "application/vnd.sentry.items.trace-metric+json",
231 }
232 }
233
234 pub fn is_empty(&self) -> bool {
236 match self {
237 Self::Logs(logs) => logs.is_empty(),
238 Self::Metrics(metrics) => metrics.is_empty(),
239 }
240 }
241}
242
243impl From<Vec<Log>> for ItemContainer {
244 fn from(logs: Vec<Log>) -> Self {
245 Self::Logs(logs)
246 }
247}
248
249#[derive(Deserialize, Serialize)]
256struct ItemsSerdeWrapper<'a, T: Clone> {
257 items: Cow<'a, [T]>,
258}
259
260impl From<Vec<Metric>> for ItemContainer {
261 fn from(metrics: Vec<Metric>) -> Self {
262 Self::Metrics(metrics)
263 }
264}
265
266impl ItemContainer {
267 fn item_type(&self) -> EnvelopeItemType {
268 match self {
269 Self::Logs(_) => EnvelopeItemType::LogsContainer,
270 Self::Metrics(_) => EnvelopeItemType::MetricsContainer,
271 }
272 }
273}
274
275impl EnvelopeItem {
276 fn item_type(&self) -> Option<EnvelopeItemType> {
277 match self {
278 Self::Event(_) => Some(EnvelopeItemType::Event),
279 Self::SessionUpdate(_) => Some(EnvelopeItemType::SessionUpdate),
280 Self::SessionAggregates(_) => Some(EnvelopeItemType::SessionAggregates),
281 Self::Transaction(_) => Some(EnvelopeItemType::Transaction),
282 Self::Attachment(_) => Some(EnvelopeItemType::Attachment),
283 Self::MonitorCheckIn(_) => Some(EnvelopeItemType::MonitorCheckIn),
284 Self::ClientReport(_) => Some(EnvelopeItemType::ClientReport),
285 Self::ItemContainer(container) => Some(container.item_type()),
286 Self::Raw => None,
287 }
288 }
289}
290
291impl From<Event<'static>> for EnvelopeItem {
292 fn from(event: Event<'static>) -> Self {
293 EnvelopeItem::Event(event.into())
294 }
295}
296
297impl From<SessionUpdate<'static>> for EnvelopeItem {
298 fn from(session: SessionUpdate<'static>) -> Self {
299 EnvelopeItem::SessionUpdate(session)
300 }
301}
302
303impl From<SessionAggregates<'static>> for EnvelopeItem {
304 fn from(aggregates: SessionAggregates<'static>) -> Self {
305 EnvelopeItem::SessionAggregates(aggregates)
306 }
307}
308
309impl From<Transaction<'static>> for EnvelopeItem {
310 fn from(transaction: Transaction<'static>) -> Self {
311 EnvelopeItem::Transaction(transaction.into())
312 }
313}
314
315impl From<Attachment> for EnvelopeItem {
316 fn from(attachment: Attachment) -> Self {
317 EnvelopeItem::Attachment(attachment)
318 }
319}
320
321impl From<MonitorCheckIn> for EnvelopeItem {
322 fn from(check_in: MonitorCheckIn) -> Self {
323 EnvelopeItem::MonitorCheckIn(check_in)
324 }
325}
326
327impl From<ItemContainer> for EnvelopeItem {
328 fn from(container: ItemContainer) -> Self {
329 EnvelopeItem::ItemContainer(container)
330 }
331}
332
333impl From<Vec<Log>> for EnvelopeItem {
334 fn from(logs: Vec<Log>) -> Self {
335 EnvelopeItem::ItemContainer(logs.into())
336 }
337}
338
339impl From<Vec<Metric>> for EnvelopeItem {
340 fn from(metrics: Vec<Metric>) -> Self {
341 EnvelopeItem::ItemContainer(metrics.into())
342 }
343}
344
345impl From<ClientReport> for EnvelopeItem {
346 fn from(value: ClientReport) -> Self {
347 EnvelopeItem::ClientReport(value)
348 }
349}
350
351#[derive(Clone)]
353pub struct EnvelopeItemIter<'s> {
354 inner: std::slice::Iter<'s, EnvelopeItem>,
355}
356
357impl<'s> Iterator for EnvelopeItemIter<'s> {
358 type Item = &'s EnvelopeItem;
359
360 fn next(&mut self) -> Option<Self::Item> {
361 self.inner.next()
362 }
363}
364
365#[derive(Debug, Clone, PartialEq)]
370enum Items {
371 EnvelopeItems(Vec<EnvelopeItem>),
372 Raw(Vec<u8>),
373}
374
375impl Default for Items {
376 fn default() -> Self {
377 Self::EnvelopeItems(Default::default())
378 }
379}
380
381impl Items {
382 fn is_empty(&self) -> bool {
383 match self {
384 Items::EnvelopeItems(items) => items.is_empty(),
385 Items::Raw(bytes) => bytes.is_empty(),
386 }
387 }
388}
389
390pub trait EnvelopeFilter: private::Sealed {
394 fn filter(&mut self, item: &EnvelopeItem) -> bool;
403
404 fn on_filtered(&mut self, item: EnvelopeItem) {
407 let _ = item;
408 }
409}
410
411pub struct EnvelopeFilterCallbacks<F, C> {
413 filter: F,
414 on_filtered: C,
415}
416
417impl<F, C> EnvelopeFilterCallbacks<F, C> {
418 pub fn new(filter: F, on_filtered: C) -> Self {
423 Self {
424 filter,
425 on_filtered,
426 }
427 }
428}
429
430#[derive(Clone, Default, Debug, PartialEq)]
439pub struct Envelope {
440 headers: EnvelopeHeaders,
441 items: Items,
442}
443
444impl Envelope {
445 pub fn new() -> Envelope {
447 Default::default()
448 }
449
450 pub fn add_item<I>(&mut self, item: I)
452 where
453 I: Into<EnvelopeItem>,
454 {
455 let item = item.into();
456
457 let Items::EnvelopeItems(ref mut items) = self.items else {
458 if item != EnvelopeItem::Raw {
459 eprintln!(
460 "WARNING: This envelope contains raw items. Adding an item is not supported."
461 );
462 }
463 return;
464 };
465
466 if self.headers.event_id.is_none() {
467 if let EnvelopeItem::Event(ref event) = item {
468 self.headers.event_id = Some(event.event_id);
469 } else if let EnvelopeItem::Transaction(ref transaction) = item {
470 self.headers.event_id = Some(transaction.event_id);
471 }
472 }
473 items.push(item);
474 }
475
476 pub fn items(&self) -> EnvelopeItemIter<'_> {
478 let inner = match &self.items {
479 Items::EnvelopeItems(items) => items.iter(),
480 Items::Raw(_) => [].iter(),
481 };
482
483 EnvelopeItemIter { inner }
484 }
485
486 pub fn into_items(self) -> impl Iterator<Item = EnvelopeItem> {
491 match self.items {
492 Items::EnvelopeItems(items) => items.into_iter(),
493 Items::Raw(_) => Default::default(),
494 }
495 }
496
497 pub fn headers(&self) -> &EnvelopeHeaders {
499 &self.headers
500 }
501
502 #[must_use]
504 pub fn with_headers(mut self, headers: EnvelopeHeaders) -> Self {
505 self.headers = headers;
506 self
507 }
508
509 pub fn uuid(&self) -> Option<&Uuid> {
511 self.headers.event_id.as_ref()
512 }
513
514 pub fn event(&self) -> Option<&Event<'static>> {
518 let Items::EnvelopeItems(ref items) = self.items else {
519 return None;
520 };
521
522 items.iter().find_map(|item| match item {
523 EnvelopeItem::Event(event) => Some(&**event),
524 _ => None,
525 })
526 }
527
528 pub fn filter<F>(self, mut filter: F) -> Option<Self>
536 where
537 F: EnvelopeFilter,
538 {
539 let Items::EnvelopeItems(items) = self.items else {
540 return if filter.filter(&EnvelopeItem::Raw) {
541 Some(self)
542 } else {
543 filter.on_filtered(EnvelopeItem::Raw);
544 None
545 };
546 };
547
548 let mut filtered = Envelope::new();
549 for item in items {
550 if filter.filter(&item) {
551 filtered.add_item(item);
552 } else {
553 filter.on_filtered(item);
554 }
555 }
556
557 if filtered.uuid().is_none() {
560 if let Items::EnvelopeItems(ref mut items) = filtered.items {
561 let old_items = mem::take(items);
562 *items = old_items
563 .into_iter()
564 .filter_map(|item| match item {
565 EnvelopeItem::Attachment(..) => {
566 filter.on_filtered(item);
567 None
568 }
569 _ => Some(item),
570 })
571 .collect();
572 }
573 }
574
575 if filtered.items.is_empty() {
576 None
577 } else {
578 Some(filtered)
579 }
580 }
581
582 pub fn to_writer<W>(&self, mut writer: W) -> std::io::Result<()>
586 where
587 W: Write,
588 {
589 let items = match &self.items {
590 Items::Raw(bytes) => return writer.write_all(bytes).map(|_| ()),
591 Items::EnvelopeItems(items) => items,
592 };
593
594 serde_json::to_writer(&mut writer, &self.headers)?;
596 writeln!(writer)?;
597
598 let mut item_buf = Vec::new();
599 for item in items {
601 match item {
603 EnvelopeItem::Event(event) => serde_json::to_writer(&mut item_buf, event)?,
604 EnvelopeItem::SessionUpdate(session) => {
605 serde_json::to_writer(&mut item_buf, session)?
606 }
607 EnvelopeItem::SessionAggregates(aggregates) => {
608 serde_json::to_writer(&mut item_buf, aggregates)?
609 }
610 EnvelopeItem::Transaction(transaction) => {
611 serde_json::to_writer(&mut item_buf, transaction)?
612 }
613 EnvelopeItem::Attachment(attachment) => {
614 attachment.to_writer(&mut writer)?;
615 writeln!(writer)?;
616 continue;
617 }
618 EnvelopeItem::MonitorCheckIn(check_in) => {
619 serde_json::to_writer(&mut item_buf, check_in)?
620 }
621 EnvelopeItem::ClientReport(client_report) => {
622 serde_json::to_writer(&mut item_buf, client_report)?
623 }
624 EnvelopeItem::ItemContainer(container) => match container {
625 ItemContainer::Logs(logs) => {
626 let wrapper = ItemsSerdeWrapper { items: logs.into() };
627 serde_json::to_writer(&mut item_buf, &wrapper)?
628 }
629 ItemContainer::Metrics(metrics) => {
630 let wrapper = ItemsSerdeWrapper {
631 items: metrics.into(),
632 };
633 serde_json::to_writer(&mut item_buf, &wrapper)?
634 }
635 },
636 EnvelopeItem::Raw => {
637 continue;
638 }
639 }
640 let item_type = item
641 .item_type()
642 .expect("raw envelope items are skipped during serialization");
643 let item_type = serde_json::to_string(&item_type)?;
644
645 if let EnvelopeItem::ItemContainer(container) = item {
646 writeln!(
647 writer,
648 r#"{{"type":{},"item_count":{},"content_type":"{}"}}"#,
649 item_type,
650 container.len(),
651 container.content_type()
652 )?;
653 } else {
654 writeln!(
655 writer,
656 r#"{{"type":{},"length":{}}}"#,
657 item_type,
658 item_buf.len()
659 )?;
660 }
661 writer.write_all(&item_buf)?;
662 writeln!(writer)?;
663 item_buf.clear();
664 }
665
666 Ok(())
667 }
668
669 pub fn from_slice(slice: &[u8]) -> Result<Envelope, EnvelopeError> {
671 let (headers, offset) = Self::parse_headers(slice)?;
672 let items = Self::parse_items(slice, offset)?;
673
674 let mut envelope = Envelope {
675 headers,
676 ..Default::default()
677 };
678
679 for item in items {
680 envelope.add_item(item);
681 }
682
683 Ok(envelope)
684 }
685
686 pub fn from_bytes_raw(bytes: Vec<u8>) -> Result<Self, EnvelopeError> {
688 Ok(Self {
689 items: Items::Raw(bytes),
690 ..Default::default()
691 })
692 }
693
694 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Envelope, EnvelopeError> {
696 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
697 Envelope::from_slice(&bytes)
698 }
699
700 pub fn from_path_raw<P: AsRef<Path>>(path: P) -> Result<Self, EnvelopeError> {
705 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
706 Self::from_bytes_raw(bytes)
707 }
708
709 fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> {
710 let first_line = slice
711 .split(|b| *b == b'\n')
712 .next()
713 .ok_or(EnvelopeError::MissingHeader)?;
714
715 let headers: EnvelopeHeaders =
716 serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?;
717
718 let offset = first_line.len();
719 Self::require_termination(slice, offset)?;
720
721 let byte_after_header = offset
724 .checked_add(1)
725 .expect("offset is at most isize::MAX; adding 1 cannot overflow usize");
726
727 Ok((headers, byte_after_header))
728 }
729
730 fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
731 let mut items = Vec::new();
732
733 while offset < slice.len() {
734 let bytes = slice
735 .get(offset..)
736 .ok_or(EnvelopeError::MissingItemHeader)?;
737 let (item, item_offset) = Self::parse_item(bytes)?;
738 offset = offset
742 .checked_add(item_offset)
743 .expect("offset + item_offset is at most slice.len() + 1");
744 items.push(item);
745 }
746
747 Ok(items)
748 }
749
750 fn parse_item(slice: &[u8]) -> Result<(EnvelopeItem, usize), EnvelopeError> {
758 let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
759
760 let header: EnvelopeItemHeader = match stream.next() {
761 None => return Err(EnvelopeError::UnexpectedEof),
762 Some(Err(error)) => return Err(EnvelopeError::InvalidItemHeader(error)),
763 Some(Ok(header)) => header,
764 };
765
766 let header_end = stream.byte_offset();
768 Self::require_termination(slice, header_end)?;
769
770 let payload_start = std::cmp::min(header_end, slice.len().saturating_sub(1))
775 .checked_add(1)
776 .expect("&[u8] length is at most isize::MAX; adding one cannot overflow usize");
777 let payload_end = match header.length {
778 Some(len) => {
779 let payload_end = payload_start.saturating_add(len);
780 if slice.len() < payload_end {
781 return Err(EnvelopeError::UnexpectedEof);
782 }
783
784 Self::require_termination(slice, payload_end)?;
786 payload_end
787 }
788 None => match slice.get(payload_start..) {
789 Some(range) => match range.iter().position(|&b| b == b'\n') {
790 Some(relative_end) => payload_start.checked_add(relative_end).expect(
791 "relative_end is an index into slice[payload_start..]; \
792 so the sum is within slice and cannot overflow usize",
793 ),
794 None => slice.len(),
795 },
796 None => slice.len(),
797 },
798 };
799
800 let payload = slice.get(payload_start..payload_end).unwrap();
801
802 let item = match header.r#type {
803 EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event),
804 EnvelopeItemType::Transaction => {
805 serde_json::from_slice(payload).map(EnvelopeItem::Transaction)
806 }
807 EnvelopeItemType::SessionUpdate => {
808 serde_json::from_slice(payload).map(EnvelopeItem::SessionUpdate)
809 }
810 EnvelopeItemType::SessionAggregates => {
811 serde_json::from_slice(payload).map(EnvelopeItem::SessionAggregates)
812 }
813 EnvelopeItemType::Attachment => Ok(EnvelopeItem::Attachment(Attachment {
814 buffer: payload.to_owned(),
815 filename: header.filename.unwrap_or_default(),
816 content_type: header.content_type,
817 ty: header.attachment_type,
818 })),
819 EnvelopeItemType::MonitorCheckIn => {
820 serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
821 }
822 EnvelopeItemType::ClientReport => {
823 serde_json::from_slice(payload).map(EnvelopeItem::ClientReport)
824 }
825 EnvelopeItemType::LogsContainer => {
826 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
827 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
828 }
829 EnvelopeItemType::MetricsContainer => {
830 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
831 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Metrics(x.items.into())))
832 }
833 }
834 .map_err(EnvelopeError::InvalidItemPayload)?;
835
836 let byte_after_payload = payload_end
839 .checked_add(1)
840 .expect("payload_end <= slice.len() <= isize::MAX, so adding 1 cannot overflow usize");
841
842 Ok((item, byte_after_payload))
843 }
844
845 fn require_termination(slice: &[u8], offset: usize) -> Result<(), EnvelopeError> {
846 match slice.get(offset) {
847 Some(&b'\n') | None => Ok(()),
848 Some(_) => Err(EnvelopeError::MissingNewline),
849 }
850 }
851}
852
853impl<T> From<T> for Envelope
854where
855 T: Into<EnvelopeItem>,
856{
857 fn from(item: T) -> Self {
858 let mut envelope = Self::default();
859 envelope.add_item(item.into());
860 envelope
861 }
862}
863
864impl<F, C> EnvelopeFilter for EnvelopeFilterCallbacks<F, C>
865where
866 F: FnMut(&EnvelopeItem) -> bool,
867 C: FnMut(EnvelopeItem),
868{
869 fn filter(&mut self, item: &EnvelopeItem) -> bool {
870 (self.filter)(item)
871 }
872
873 fn on_filtered(&mut self, item: EnvelopeItem) {
874 (self.on_filtered)(item);
875 }
876}
877
878impl<F> EnvelopeFilter for F
879where
880 F: FnMut(&EnvelopeItem) -> bool,
881{
882 fn filter(&mut self, item: &EnvelopeItem) -> bool {
883 self(item)
884 }
885}
886
887mod private {
888 use super::*;
889
890 pub trait Sealed {}
891
892 impl<F, C> Sealed for EnvelopeFilterCallbacks<F, C>
893 where
894 F: FnMut(&EnvelopeItem) -> bool,
895 C: FnMut(EnvelopeItem),
896 {
897 }
898
899 impl<F> Sealed for F where F: FnMut(&EnvelopeItem) -> bool {}
900}
901
902#[cfg(test)]
903mod test {
904 use std::borrow::Cow;
905 use std::str::FromStr;
906 use std::time::{Duration, SystemTime};
907
908 use protocol::Map;
909 use serde_json::Value;
910 use time::format_description::well_known::Rfc3339;
911 use time::OffsetDateTime;
912 use uuid::Uuid;
913
914 use super::*;
915 use crate::protocol::client_report::{Item, LossSource};
916 use crate::protocol::v7::client_report::Category;
917 use crate::protocol::v7::{
918 ClientReport, Level, LogLevel, MetricType, MonitorCheckInStatus, MonitorConfig,
919 MonitorSchedule, SampleRand, SessionAggregateItem, SessionAttributes, SessionStatus, Span,
920 };
921
922 fn to_str(envelope: Envelope) -> String {
923 let mut vec = Vec::new();
924 envelope.to_writer(&mut vec).unwrap();
925 String::from_utf8_lossy(&vec).to_string()
926 }
927
928 fn timestamp(s: &str) -> SystemTime {
929 let dt = OffsetDateTime::parse(s, &Rfc3339).unwrap();
930 let secs = dt.unix_timestamp() as u64;
931 let nanos = dt.nanosecond();
932 let duration = Duration::new(secs, nanos);
933 SystemTime::UNIX_EPOCH.checked_add(duration).unwrap()
934 }
935
936 fn session_attributes() -> SessionAttributes<'static> {
937 SessionAttributes {
938 release: Cow::Borrowed("release@1.0.0"),
939 environment: Some(Cow::Borrowed("production")),
940 ip_address: None,
941 user_agent: None,
942 }
943 }
944
945 fn collect_losses(envelope: &Envelope) -> Vec<(Category, u64)> {
946 envelope
947 .losses()
948 .map(|loss| (loss.category, loss.quantity))
949 .collect()
950 }
951
952 #[test]
953 fn test_empty() {
954 assert_eq!(to_str(Envelope::new()), "{}\n");
955 }
956
957 #[test]
958 fn raw_roundtrip() {
959 let buf = r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
960{"type":"event","length":74}
961{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
962"#;
963 let envelope = Envelope::from_bytes_raw(buf.to_string().into_bytes()).unwrap();
964 let serialized = to_str(envelope);
965 assert_eq!(&serialized, buf);
966
967 let random_invalid_bytes = b"oh stahp!\0\x01\x02";
968 let envelope = Envelope::from_bytes_raw(random_invalid_bytes.to_vec()).unwrap();
969 let mut serialized = Vec::new();
970 envelope.to_writer(&mut serialized).unwrap();
971 assert_eq!(&serialized, random_invalid_bytes);
972 }
973
974 #[test]
975 fn test_event() {
976 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
977 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
978 let event = Event {
979 event_id,
980 timestamp,
981 ..Default::default()
982 };
983 let envelope: Envelope = event.into();
984 assert_eq!(
985 to_str(envelope),
986 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
987{"type":"event","length":74}
988{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
989"#
990 )
991 }
992
993 #[test]
994 fn test_session() {
995 let session_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
996 let started = timestamp("2020-07-20T14:51:14.296Z");
997 let session = SessionUpdate {
998 session_id,
999 distinct_id: Some("foo@bar.baz".to_owned()),
1000 sequence: None,
1001 timestamp: None,
1002 started,
1003 init: true,
1004 duration: Some(1.234),
1005 status: SessionStatus::Ok,
1006 errors: 123,
1007 attributes: SessionAttributes {
1008 release: "foo-bar@1.2.3".into(),
1009 environment: Some("production".into()),
1010 ip_address: None,
1011 user_agent: None,
1012 },
1013 };
1014 let mut envelope = Envelope::new();
1015 envelope.add_item(session);
1016 assert_eq!(
1017 to_str(envelope),
1018 r#"{}
1019{"type":"session","length":222}
1020{"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"}}
1021"#
1022 )
1023 }
1024
1025 #[test]
1026 fn test_transaction() {
1027 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1028 let span_id = "d42cee9fc3e74f5c".parse().unwrap();
1029 let trace_id = "335e53d614474acc9f89e632b776cc28".parse().unwrap();
1030 let start_timestamp = timestamp("2020-07-20T14:51:14.296Z");
1031 let spans = vec![Span {
1032 span_id,
1033 trace_id,
1034 start_timestamp,
1035 ..Default::default()
1036 }];
1037 let transaction = Transaction {
1038 event_id,
1039 start_timestamp,
1040 spans,
1041 ..Default::default()
1042 };
1043 let envelope: Envelope = transaction.into();
1044 assert_eq!(
1045 to_str(envelope),
1046 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1047{"type":"transaction","length":200}
1048{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]}
1049"#
1050 )
1051 }
1052
1053 #[test]
1054 fn test_monitor_checkin() {
1055 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1056
1057 let check_in = MonitorCheckIn {
1058 check_in_id,
1059 monitor_slug: "my-monitor".into(),
1060 status: MonitorCheckInStatus::Ok,
1061 duration: Some(123.4),
1062 environment: Some("production".into()),
1063 monitor_config: Some(MonitorConfig {
1064 schedule: MonitorSchedule::Crontab {
1065 value: "12 0 * * *".into(),
1066 },
1067 checkin_margin: Some(5),
1068 max_runtime: Some(30),
1069 timezone: Some("UTC".into()),
1070 failure_issue_threshold: None,
1071 recovery_threshold: None,
1072 }),
1073 };
1074 let envelope: Envelope = check_in.into();
1075 assert_eq!(
1076 to_str(envelope),
1077 r#"{}
1078{"type":"check_in","length":259}
1079{"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"}}
1080"#
1081 )
1082 }
1083
1084 #[test]
1085 fn test_monitor_checkin_with_thresholds() {
1086 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1087
1088 let check_in = MonitorCheckIn {
1089 check_in_id,
1090 monitor_slug: "my-monitor".into(),
1091 status: MonitorCheckInStatus::Ok,
1092 duration: Some(123.4),
1093 environment: Some("production".into()),
1094 monitor_config: Some(MonitorConfig {
1095 schedule: MonitorSchedule::Crontab {
1096 value: "12 0 * * *".into(),
1097 },
1098 checkin_margin: Some(5),
1099 max_runtime: Some(30),
1100 timezone: Some("UTC".into()),
1101 failure_issue_threshold: Some(4),
1102 recovery_threshold: Some(7),
1103 }),
1104 };
1105 let envelope: Envelope = check_in.into();
1106 assert_eq!(
1107 to_str(envelope),
1108 r#"{}
1109{"type":"check_in","length":310}
1110{"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}}
1111"#
1112 )
1113 }
1114
1115 #[test]
1116 fn test_event_with_attachment() {
1117 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1118 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
1119 let event = Event {
1120 event_id,
1121 timestamp,
1122 ..Default::default()
1123 };
1124 let mut envelope: Envelope = event.into();
1125
1126 envelope.add_item(Attachment {
1127 buffer: "some content".as_bytes().to_vec(),
1128 filename: "file.txt".to_string(),
1129 ..Default::default()
1130 });
1131
1132 assert_eq!(
1133 to_str(envelope),
1134 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1135{"type":"event","length":74}
1136{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1137{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"}
1138some content
1139"#
1140 )
1141 }
1142
1143 #[test]
1144 fn test_deserialize_envelope_empty() {
1145 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}";
1147 let envelope = Envelope::from_slice(bytes).unwrap();
1148
1149 let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap();
1150 assert_eq!(envelope.headers.event_id, Some(event_id));
1151 assert_eq!(envelope.items().count(), 0);
1152 }
1153
1154 #[test]
1155 fn test_deserialize_envelope_empty_newline() {
1156 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n";
1158 let envelope = Envelope::from_slice(bytes).unwrap();
1159 assert_eq!(envelope.items().count(), 0);
1160 }
1161
1162 #[test]
1163 fn test_deserialize_envelope_empty_item_newline() {
1164 let bytes = b"\
1166 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1167 {\"type\":\"attachment\",\"length\":0}\n\
1168 \n\
1169 {\"type\":\"attachment\",\"length\":0}\n\
1170 ";
1171
1172 let envelope = Envelope::from_slice(bytes).unwrap();
1173 assert_eq!(envelope.items().count(), 2);
1174
1175 let mut items = envelope.items();
1176
1177 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1178 assert_eq!(attachment.buffer.len(), 0);
1179 } else {
1180 panic!("invalid item type");
1181 }
1182
1183 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1184 assert_eq!(attachment.buffer.len(), 0);
1185 } else {
1186 panic!("invalid item type");
1187 }
1188 }
1189
1190 #[test]
1191 fn test_deserialize_envelope_empty_item_eof() {
1192 let bytes = b"\
1194 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1195 {\"type\":\"attachment\",\"length\":0}\n\
1196 \n\
1197 {\"type\":\"attachment\",\"length\":0}\
1198 ";
1199
1200 let envelope = Envelope::from_slice(bytes).unwrap();
1201 assert_eq!(envelope.items().count(), 2);
1202
1203 let mut items = envelope.items();
1204
1205 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1206 assert_eq!(attachment.buffer.len(), 0);
1207 } else {
1208 panic!("invalid item type");
1209 }
1210
1211 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1212 assert_eq!(attachment.buffer.len(), 0);
1213 } else {
1214 panic!("invalid item type");
1215 }
1216 }
1217
1218 #[test]
1219 fn test_deserialize_envelope_implicit_length() {
1220 let bytes = b"\
1222 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1223 {\"type\":\"attachment\"}\n\
1224 helloworld\n\
1225 ";
1226
1227 let envelope = Envelope::from_slice(bytes).unwrap();
1228 assert_eq!(envelope.items().count(), 1);
1229
1230 let mut items = envelope.items();
1231
1232 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1233 assert_eq!(attachment.buffer.len(), 10);
1234 } else {
1235 panic!("invalid item type");
1236 }
1237 }
1238
1239 #[test]
1240 fn test_deserialize_envelope_implicit_length_eof() {
1241 let bytes = b"\
1243 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1244 {\"type\":\"attachment\"}\n\
1245 helloworld\
1246 ";
1247
1248 let envelope = Envelope::from_slice(bytes).unwrap();
1249 assert_eq!(envelope.items().count(), 1);
1250
1251 let mut items = envelope.items();
1252
1253 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1254 assert_eq!(attachment.buffer.len(), 10);
1255 } else {
1256 panic!("invalid item type");
1257 }
1258 }
1259
1260 #[test]
1261 fn test_deserialize_envelope_implicit_length_empty_eof() {
1262 let bytes = b"\
1264 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1265 {\"type\":\"attachment\"}\
1266 ";
1267
1268 let envelope = Envelope::from_slice(bytes).unwrap();
1269 assert_eq!(envelope.items().count(), 1);
1270
1271 let mut items = envelope.items();
1272
1273 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1274 assert_eq!(attachment.buffer.len(), 0);
1275 } else {
1276 panic!("invalid item type");
1277 }
1278 }
1279
1280 #[test]
1281 fn test_deserialize_envelope_multiple_items() {
1282 let bytes = b"\
1284 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1285 {\"type\":\"attachment\",\"length\":10,\"content_type\":\"text/plain\",\"filename\":\"hello.txt\"}\n\
1286 \xef\xbb\xbfHello\r\n\n\
1287 {\"type\":\"event\",\"length\":41,\"content_type\":\"application/json\",\"filename\":\"application.log\"}\n\
1288 {\"message\":\"hello world\",\"level\":\"error\"}\n\
1289 ";
1290
1291 let envelope = Envelope::from_slice(bytes).unwrap();
1292 assert_eq!(envelope.items().count(), 2);
1293
1294 let mut items = envelope.items();
1295
1296 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1297 assert_eq!(attachment.buffer.len(), 10);
1298 assert_eq!(attachment.buffer, b"\xef\xbb\xbfHello\r\n");
1299 assert_eq!(attachment.filename, "hello.txt");
1300 assert_eq!(attachment.content_type, Some("text/plain".to_string()));
1301 } else {
1302 panic!("invalid item type");
1303 }
1304
1305 if let EnvelopeItem::Event(event) = items.next().unwrap() {
1306 assert_eq!(event.message, Some("hello world".to_string()));
1307 assert_eq!(event.level, Level::Error);
1308 } else {
1309 panic!("invalid item type");
1310 }
1311 }
1312
1313 #[test]
1314 fn test_all_envelope_headers_roundtrip() {
1315 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"}}
1316{"type":"event","length":74}
1317{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1318"#;
1319
1320 let envelope = Envelope::from_slice(bytes);
1321 assert!(envelope.is_ok());
1322 let envelope = envelope.unwrap();
1323 let serialized = to_str(envelope);
1324 assert_eq!(bytes, serialized.as_bytes());
1325 }
1326
1327 #[test]
1328 fn test_sample_rand_rounding() {
1329 let envelope = Envelope::new().with_headers(
1330 EnvelopeHeaders::new().with_trace(
1331 DynamicSamplingContext::new()
1332 .with_sample_rand(SampleRand::try_from(0.999_999_9).unwrap()),
1333 ),
1334 );
1335 let expected = br#"{"trace":{"sample_rand":"0.999999"}}
1336"#;
1337
1338 let serialized = to_str(envelope);
1339 assert_eq!(expected, serialized.as_bytes());
1340 }
1341
1342 #[test]
1343 fn test_metric_container_header() {
1344 let metrics: EnvelopeItem = vec![Metric {
1345 r#type: protocol::MetricType::Counter,
1346 name: "api.requests".into(),
1347 value: 1.0,
1348 timestamp: timestamp("2026-03-02T13:36:02.000Z"),
1349 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1350 span_id: None,
1351 unit: None,
1352 attributes: Map::new(),
1353 }]
1354 .into();
1355
1356 let mut envelope = Envelope::new();
1357 envelope.add_item(metrics);
1358
1359 let expected = [
1360 serde_json::json!({}),
1361 serde_json::json!({
1362 "type": "trace_metric",
1363 "item_count": 1,
1364 "content_type": "application/vnd.sentry.items.trace-metric+json"
1365 }),
1366 serde_json::json!({
1367 "items": [{
1368 "type": "counter",
1369 "name": "api.requests",
1370 "value": 1.0,
1371 "timestamp": 1772458562,
1372 "trace_id": "335e53d614474acc9f89e632b776cc28"
1373 }]
1374 }),
1375 ];
1376
1377 let serialized = to_str(envelope);
1378 let actual = serialized
1379 .lines()
1380 .map(|line| serde_json::from_str::<Value>(line).expect("envelope has invalid JSON"));
1381
1382 assert!(actual.eq(expected.into_iter()));
1383 }
1384
1385 #[test]
1387 fn test_deserialize_serialized() {
1388 let event = Event {
1390 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1391 timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1392 ..Default::default()
1393 };
1394
1395 let transaction = Transaction {
1397 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9d").unwrap(),
1398 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1399 spans: vec![Span {
1400 span_id: "d42cee9fc3e74f5c".parse().unwrap(),
1401 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1402 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1403 ..Default::default()
1404 }],
1405 ..Default::default()
1406 };
1407
1408 let session = SessionUpdate {
1410 session_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1411 distinct_id: Some("foo@bar.baz".to_owned()),
1412 sequence: None,
1413 timestamp: None,
1414 started: timestamp("2020-07-20T14:51:14.296Z"),
1415 init: true,
1416 duration: Some(1.234),
1417 status: SessionStatus::Ok,
1418 errors: 123,
1419 attributes: SessionAttributes {
1420 release: "foo-bar@1.2.3".into(),
1421 environment: Some("production".into()),
1422 ip_address: None,
1423 user_agent: None,
1424 },
1425 };
1426
1427 let attachment = Attachment {
1429 buffer: "some content".as_bytes().to_vec(),
1430 filename: "file.txt".to_string(),
1431 ..Default::default()
1432 };
1433
1434 let mut attributes = Map::new();
1435 attributes.insert("key".into(), "value".into());
1436 attributes.insert("num".into(), 10.into());
1437 attributes.insert("val".into(), 10.2.into());
1438 attributes.insert("bool".into(), false.into());
1439 let mut attributes_2 = attributes.clone();
1440 attributes_2.insert("more".into(), true.into());
1441 let logs: EnvelopeItem = vec![
1442 Log {
1443 level: protocol::LogLevel::Warn,
1444 body: "test".to_owned(),
1445 trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()),
1446 timestamp: timestamp("2022-07-25T14:51:14.296Z"),
1447 severity_number: Some(1.try_into().unwrap()),
1448 attributes,
1449 },
1450 Log {
1451 level: protocol::LogLevel::Error,
1452 body: "a body".to_owned(),
1453 trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
1454 timestamp: timestamp("2021-07-21T14:51:14.296Z"),
1455 severity_number: Some(1.try_into().unwrap()),
1456 attributes: attributes_2,
1457 },
1458 ]
1459 .into();
1460
1461 let mut metric_attributes = Map::new();
1462 metric_attributes.insert("route".into(), "/users".into());
1463 let metrics: EnvelopeItem = vec![Metric {
1464 r#type: protocol::MetricType::Distribution,
1465 name: "response.time".into(),
1466 value: 123.4,
1467 timestamp: timestamp("2022-07-26T14:51:14.296Z"),
1468 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1469 span_id: Some("d42cee9fc3e74f5c".parse().unwrap()),
1470 unit: Some("millisecond".into()),
1471 attributes: metric_attributes,
1472 }]
1473 .into();
1474
1475 let mut envelope: Envelope = Envelope::new();
1476 envelope.add_item(event);
1477 envelope.add_item(transaction);
1478 envelope.add_item(session);
1479 envelope.add_item(attachment);
1480 envelope.add_item(logs);
1481 envelope.add_item(metrics);
1482
1483 let serialized = to_str(envelope);
1484 let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
1485 assert_eq!(serialized, to_str(deserialized))
1486 }
1487
1488 #[test]
1489 fn losses_on_drop_maps_event_to_error() {
1490 let envelope: Envelope = Event::default().into();
1491
1492 assert_eq!(collect_losses(&envelope), vec![(Category::Error, 1)]);
1493 }
1494
1495 #[test]
1496 fn losses_on_drop_maps_session_update_to_session() {
1497 let envelope: Envelope = SessionUpdate {
1498 session_id: Uuid::nil(),
1499 distinct_id: None,
1500 sequence: None,
1501 timestamp: None,
1502 started: SystemTime::UNIX_EPOCH,
1503 init: false,
1504 duration: None,
1505 status: SessionStatus::Ok,
1506 errors: 0,
1507 attributes: session_attributes(),
1508 }
1509 .into();
1510
1511 assert_eq!(collect_losses(&envelope), vec![(Category::Session, 1)]);
1512 }
1513
1514 #[test]
1515 fn losses_on_drop_maps_attachment_to_buffer_bytes() {
1516 let envelope: Envelope = Attachment {
1517 buffer: b"attachment".to_vec(),
1518 filename: "attachment.bin".to_owned(),
1519 ..Default::default()
1520 }
1521 .into();
1522
1523 assert_eq!(collect_losses(&envelope), vec![(Category::Attachment, 10)]);
1524 }
1525
1526 #[test]
1527 fn losses_on_drop_maps_monitor_check_in_to_monitor() {
1528 let envelope: Envelope = MonitorCheckIn {
1529 check_in_id: Uuid::nil(),
1530 monitor_slug: "monitor".to_owned(),
1531 status: MonitorCheckInStatus::Ok,
1532 environment: None,
1533 duration: None,
1534 monitor_config: None,
1535 }
1536 .into();
1537
1538 assert_eq!(collect_losses(&envelope), vec![(Category::Monitor, 1)]);
1539 }
1540
1541 #[test]
1542 fn losses_on_drop_sums_session_aggregate_status_counts() {
1543 let envelope: Envelope = SessionAggregates {
1544 aggregates: vec![
1545 SessionAggregateItem {
1546 started: SystemTime::UNIX_EPOCH,
1547 distinct_id: None,
1548 exited: 2,
1549 errored: 3,
1550 abnormal: 5,
1551 crashed: 7,
1552 },
1553 SessionAggregateItem {
1554 started: SystemTime::UNIX_EPOCH,
1555 distinct_id: None,
1556 exited: 11,
1557 errored: 13,
1558 abnormal: 17,
1559 crashed: 19,
1560 },
1561 ],
1562 attributes: session_attributes(),
1563 }
1564 .into();
1565
1566 assert_eq!(collect_losses(&envelope), vec![(Category::Session, 77)]);
1567 }
1568
1569 #[test]
1570 fn losses_on_drop_counts_transaction_root_span() {
1571 let envelope: Envelope = Transaction {
1572 spans: vec![],
1573 ..Default::default()
1574 }
1575 .into();
1576
1577 assert_eq!(
1578 collect_losses(&envelope),
1579 vec![(Category::Transaction, 1), (Category::Span, 1)]
1580 );
1581 }
1582
1583 #[test]
1584 fn losses_on_drop_counts_transaction_child_spans() {
1585 let envelope: Envelope = Transaction {
1586 spans: vec![Span::default(), Span::default(), Span::default()],
1587 ..Default::default()
1588 }
1589 .into();
1590
1591 assert_eq!(
1592 collect_losses(&envelope),
1593 vec![(Category::Transaction, 1), (Category::Span, 4)]
1594 );
1595 }
1596
1597 #[test]
1598 fn losses_on_drop_counts_minimal_log_bytes() {
1599 let envelope: Envelope = vec![Log {
1600 level: LogLevel::Info,
1601 body: String::new(),
1602 trace_id: None,
1603 timestamp: SystemTime::UNIX_EPOCH,
1604 severity_number: None,
1605 attributes: Map::new(),
1606 }]
1607 .into();
1608
1609 assert_eq!(
1610 collect_losses(&envelope),
1611 vec![(Category::LogItem, 1), (Category::LogByte, 1)]
1612 );
1613 }
1614
1615 #[test]
1616 fn losses_on_drop_counts_complex_log_bytes() {
1617 let mut attributes = Map::new();
1618 attributes.insert("k1".to_owned(), "string value".into());
1619 attributes.insert("k2".to_owned(), u64::MAX.into());
1620 attributes.insert("k3".to_owned(), 42.0.into());
1621 attributes.insert("k4".to_owned(), false.into());
1622 attributes.insert(
1623 "k5".to_owned(),
1624 serde_json::json!({
1625 "nested": {
1626 "array": [1.0, 2, -12, "7 bytes", false]
1627 }
1628 })
1629 .into(),
1630 );
1631 let envelope: Envelope = vec![Log {
1632 level: LogLevel::Info,
1633 body: "7 bytes".to_owned(),
1634 trace_id: None,
1635 timestamp: SystemTime::UNIX_EPOCH,
1636 severity_number: None,
1637 attributes,
1638 }]
1639 .into();
1640
1641 assert_eq!(
1642 collect_losses(&envelope),
1643 vec![(Category::LogItem, 1), (Category::LogByte, 89)]
1644 );
1645 }
1646
1647 #[test]
1648 fn losses_on_drop_counts_minimal_metric_bytes() {
1649 let envelope: Envelope = vec![Metric {
1650 r#type: MetricType::Counter,
1651 name: Cow::Borrowed(""),
1652 value: 1.0,
1653 timestamp: SystemTime::UNIX_EPOCH,
1654 trace_id: Default::default(),
1655 span_id: None,
1656 unit: None,
1657 attributes: Map::new(),
1658 }]
1659 .into();
1660
1661 assert_eq!(
1662 collect_losses(&envelope),
1663 vec![(Category::TraceMetric, 1), (Category::TraceMetricByte, 8)]
1664 );
1665 }
1666
1667 #[test]
1668 fn losses_on_drop_counts_complex_metric_bytes() {
1669 let mut attributes = Map::new();
1670 attributes.insert(
1671 Cow::Borrowed("foo"),
1672 "ඞ and some more equals 33 bytes".into(),
1673 );
1674 let envelope: Envelope = vec![Metric {
1675 r#type: MetricType::Counter,
1676 name: Cow::Borrowed("counter"),
1677 value: 1.0,
1678 timestamp: SystemTime::UNIX_EPOCH,
1679 trace_id: Default::default(),
1680 span_id: None,
1681 unit: None,
1682 attributes,
1683 }]
1684 .into();
1685
1686 assert_eq!(
1687 collect_losses(&envelope),
1688 vec![(Category::TraceMetric, 1), (Category::TraceMetricByte, 51)]
1689 );
1690 }
1691
1692 #[test]
1693 fn losses_on_drop_skips_client_reports() {
1694 let envelope: Envelope = ClientReport::new(<[Item; 0]>::default()).into();
1695
1696 assert!(collect_losses(&envelope).is_empty());
1697 }
1698
1699 #[test]
1700 fn losses_on_drop_skips_raw_envelopes() {
1701 let envelope = Envelope::from_bytes_raw(b"not parsed".to_vec()).unwrap();
1702
1703 assert!(collect_losses(&envelope).is_empty());
1704 }
1705
1706 #[test]
1707 fn losses_on_drop_flattens_losses_from_all_envelope_items() {
1708 let log = Log {
1709 level: LogLevel::Warn,
1710 body: "flattened".to_owned(),
1711 trace_id: None,
1712 timestamp: SystemTime::UNIX_EPOCH,
1713 severity_number: None,
1714 attributes: Map::new(),
1715 };
1716 let mut envelope = Envelope::new();
1717 envelope.add_item(Event::default());
1718 envelope.add_item(Transaction {
1719 spans: vec![Span::default(), Span::default()],
1720 ..Default::default()
1721 });
1722 envelope.add_item(vec![log]);
1723 envelope.add_item(vec![Metric {
1724 r#type: MetricType::Counter,
1725 name: Cow::Borrowed("flattened.metric"),
1726 value: 1.0,
1727 timestamp: SystemTime::UNIX_EPOCH,
1728 trace_id: Default::default(),
1729 span_id: None,
1730 unit: None,
1731 attributes: Map::new(),
1732 }]);
1733
1734 assert_eq!(
1735 collect_losses(&envelope),
1736 vec![
1737 (Category::Error, 1),
1738 (Category::Transaction, 1),
1739 (Category::Span, 3),
1740 (Category::LogItem, 1),
1741 (Category::LogByte, 9),
1742 (Category::TraceMetric, 1),
1743 (Category::TraceMetricByte, 24),
1744 ]
1745 );
1746 }
1747}