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