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>
529 where
530 F: EnvelopeFilter,
531 {
532 let Items::EnvelopeItems(items) = self.items else {
533 return if filter.filter(&EnvelopeItem::Raw) {
534 Some(self)
535 } else {
536 filter.on_filtered(EnvelopeItem::Raw);
537 None
538 };
539 };
540
541 let mut filtered = Envelope::new();
542 for item in items {
543 if filter.filter(&item) {
544 filtered.add_item(item);
545 } else {
546 filter.on_filtered(item);
547 }
548 }
549
550 if filtered.uuid().is_none() {
553 if let Items::EnvelopeItems(ref mut items) = filtered.items {
554 let old_items = mem::take(items);
555 *items = old_items
556 .into_iter()
557 .filter_map(|item| match item {
558 EnvelopeItem::Attachment(..) => {
559 filter.on_filtered(item);
560 None
561 }
562 _ => Some(item),
563 })
564 .collect();
565 }
566 }
567
568 if filtered.items.is_empty() {
569 None
570 } else {
571 Some(filtered)
572 }
573 }
574
575 pub fn to_writer<W>(&self, mut writer: W) -> std::io::Result<()>
579 where
580 W: Write,
581 {
582 let items = match &self.items {
583 Items::Raw(bytes) => return writer.write_all(bytes).map(|_| ()),
584 Items::EnvelopeItems(items) => items,
585 };
586
587 serde_json::to_writer(&mut writer, &self.headers)?;
589 writeln!(writer)?;
590
591 let mut item_buf = Vec::new();
592 for item in items {
594 match item {
596 EnvelopeItem::Event(event) => serde_json::to_writer(&mut item_buf, event)?,
597 EnvelopeItem::SessionUpdate(session) => {
598 serde_json::to_writer(&mut item_buf, session)?
599 }
600 EnvelopeItem::SessionAggregates(aggregates) => {
601 serde_json::to_writer(&mut item_buf, aggregates)?
602 }
603 EnvelopeItem::Transaction(transaction) => {
604 serde_json::to_writer(&mut item_buf, transaction)?
605 }
606 EnvelopeItem::Attachment(attachment) => {
607 attachment.to_writer(&mut writer)?;
608 writeln!(writer)?;
609 continue;
610 }
611 EnvelopeItem::MonitorCheckIn(check_in) => {
612 serde_json::to_writer(&mut item_buf, check_in)?
613 }
614 EnvelopeItem::ClientReport(client_report) => {
615 serde_json::to_writer(&mut item_buf, client_report)?
616 }
617 EnvelopeItem::ItemContainer(container) => match container {
618 ItemContainer::Logs(logs) => {
619 let wrapper = ItemsSerdeWrapper { items: logs.into() };
620 serde_json::to_writer(&mut item_buf, &wrapper)?
621 }
622 ItemContainer::Metrics(metrics) => {
623 let wrapper = ItemsSerdeWrapper {
624 items: metrics.into(),
625 };
626 serde_json::to_writer(&mut item_buf, &wrapper)?
627 }
628 },
629 EnvelopeItem::Raw => {
630 continue;
631 }
632 }
633 let item_type = item
634 .item_type()
635 .expect("raw envelope items are skipped during serialization");
636 let item_type = serde_json::to_string(&item_type)?;
637
638 if let EnvelopeItem::ItemContainer(container) = item {
639 writeln!(
640 writer,
641 r#"{{"type":{},"item_count":{},"content_type":"{}"}}"#,
642 item_type,
643 container.len(),
644 container.content_type()
645 )?;
646 } else {
647 writeln!(
648 writer,
649 r#"{{"type":{},"length":{}}}"#,
650 item_type,
651 item_buf.len()
652 )?;
653 }
654 writer.write_all(&item_buf)?;
655 writeln!(writer)?;
656 item_buf.clear();
657 }
658
659 Ok(())
660 }
661
662 pub fn from_slice(slice: &[u8]) -> Result<Envelope, EnvelopeError> {
664 let (headers, offset) = Self::parse_headers(slice)?;
665 let items = Self::parse_items(slice, offset)?;
666
667 let mut envelope = Envelope {
668 headers,
669 ..Default::default()
670 };
671
672 for item in items {
673 envelope.add_item(item);
674 }
675
676 Ok(envelope)
677 }
678
679 pub fn from_bytes_raw(bytes: Vec<u8>) -> Result<Self, EnvelopeError> {
681 Ok(Self {
682 items: Items::Raw(bytes),
683 ..Default::default()
684 })
685 }
686
687 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Envelope, EnvelopeError> {
689 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
690 Envelope::from_slice(&bytes)
691 }
692
693 pub fn from_path_raw<P: AsRef<Path>>(path: P) -> Result<Self, EnvelopeError> {
698 let bytes = std::fs::read(path).map_err(|_| EnvelopeError::UnexpectedEof)?;
699 Self::from_bytes_raw(bytes)
700 }
701
702 fn parse_headers(slice: &[u8]) -> Result<(EnvelopeHeaders, usize), EnvelopeError> {
703 let first_line = slice
704 .split(|b| *b == b'\n')
705 .next()
706 .ok_or(EnvelopeError::MissingHeader)?;
707
708 let headers: EnvelopeHeaders =
709 serde_json::from_slice(first_line).map_err(EnvelopeError::InvalidHeader)?;
710
711 let offset = first_line.len();
712 Self::require_termination(slice, offset)?;
713
714 let byte_after_header = offset
717 .checked_add(1)
718 .expect("offset is at most isize::MAX; adding 1 cannot overflow usize");
719
720 Ok((headers, byte_after_header))
721 }
722
723 fn parse_items(slice: &[u8], mut offset: usize) -> Result<Vec<EnvelopeItem>, EnvelopeError> {
724 let mut items = Vec::new();
725
726 while offset < slice.len() {
727 let bytes = slice
728 .get(offset..)
729 .ok_or(EnvelopeError::MissingItemHeader)?;
730 let (item, item_offset) = Self::parse_item(bytes)?;
731 offset = offset
735 .checked_add(item_offset)
736 .expect("offset + item_offset is at most slice.len() + 1");
737 items.push(item);
738 }
739
740 Ok(items)
741 }
742
743 fn parse_item(slice: &[u8]) -> Result<(EnvelopeItem, usize), EnvelopeError> {
751 let mut stream = serde_json::Deserializer::from_slice(slice).into_iter();
752
753 let header: EnvelopeItemHeader = match stream.next() {
754 None => return Err(EnvelopeError::UnexpectedEof),
755 Some(Err(error)) => return Err(EnvelopeError::InvalidItemHeader(error)),
756 Some(Ok(header)) => header,
757 };
758
759 let header_end = stream.byte_offset();
761 Self::require_termination(slice, header_end)?;
762
763 let payload_start = std::cmp::min(header_end, slice.len().saturating_sub(1))
768 .checked_add(1)
769 .expect("&[u8] length is at most isize::MAX; adding one cannot overflow usize");
770 let payload_end = match header.length {
771 Some(len) => {
772 let payload_end = payload_start.saturating_add(len);
773 if slice.len() < payload_end {
774 return Err(EnvelopeError::UnexpectedEof);
775 }
776
777 Self::require_termination(slice, payload_end)?;
779 payload_end
780 }
781 None => match slice.get(payload_start..) {
782 Some(range) => match range.iter().position(|&b| b == b'\n') {
783 Some(relative_end) => payload_start.checked_add(relative_end).expect(
784 "relative_end is an index into slice[payload_start..]; \
785 so the sum is within slice and cannot overflow usize",
786 ),
787 None => slice.len(),
788 },
789 None => slice.len(),
790 },
791 };
792
793 let payload = slice.get(payload_start..payload_end).unwrap();
794
795 let item = match header.r#type {
796 EnvelopeItemType::Event => serde_json::from_slice(payload).map(EnvelopeItem::Event),
797 EnvelopeItemType::Transaction => {
798 serde_json::from_slice(payload).map(EnvelopeItem::Transaction)
799 }
800 EnvelopeItemType::SessionUpdate => {
801 serde_json::from_slice(payload).map(EnvelopeItem::SessionUpdate)
802 }
803 EnvelopeItemType::SessionAggregates => {
804 serde_json::from_slice(payload).map(EnvelopeItem::SessionAggregates)
805 }
806 EnvelopeItemType::Attachment => Ok(EnvelopeItem::Attachment(Attachment {
807 buffer: payload.to_owned(),
808 filename: header.filename.unwrap_or_default(),
809 content_type: header.content_type,
810 ty: header.attachment_type,
811 })),
812 EnvelopeItemType::MonitorCheckIn => {
813 serde_json::from_slice(payload).map(EnvelopeItem::MonitorCheckIn)
814 }
815 EnvelopeItemType::ClientReport => {
816 serde_json::from_slice(payload).map(EnvelopeItem::ClientReport)
817 }
818 EnvelopeItemType::LogsContainer => {
819 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
820 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Logs(x.items.into())))
821 }
822 EnvelopeItemType::MetricsContainer => {
823 serde_json::from_slice::<ItemsSerdeWrapper<_>>(payload)
824 .map(|x| EnvelopeItem::ItemContainer(ItemContainer::Metrics(x.items.into())))
825 }
826 }
827 .map_err(EnvelopeError::InvalidItemPayload)?;
828
829 let byte_after_payload = payload_end
832 .checked_add(1)
833 .expect("payload_end <= slice.len() <= isize::MAX, so adding 1 cannot overflow usize");
834
835 Ok((item, byte_after_payload))
836 }
837
838 fn require_termination(slice: &[u8], offset: usize) -> Result<(), EnvelopeError> {
839 match slice.get(offset) {
840 Some(&b'\n') | None => Ok(()),
841 Some(_) => Err(EnvelopeError::MissingNewline),
842 }
843 }
844}
845
846impl<T> From<T> for Envelope
847where
848 T: Into<EnvelopeItem>,
849{
850 fn from(item: T) -> Self {
851 let mut envelope = Self::default();
852 envelope.add_item(item.into());
853 envelope
854 }
855}
856
857impl<F, C> EnvelopeFilter for EnvelopeFilterCallbacks<F, C>
858where
859 F: FnMut(&EnvelopeItem) -> bool,
860 C: FnMut(EnvelopeItem),
861{
862 fn filter(&mut self, item: &EnvelopeItem) -> bool {
863 (self.filter)(item)
864 }
865
866 fn on_filtered(&mut self, item: EnvelopeItem) {
867 (self.on_filtered)(item);
868 }
869}
870
871impl<F> EnvelopeFilter for F
872where
873 F: FnMut(&EnvelopeItem) -> bool,
874{
875 fn filter(&mut self, item: &EnvelopeItem) -> bool {
876 self(item)
877 }
878}
879
880mod private {
881 use super::*;
882
883 pub trait Sealed {}
884
885 impl<F, C> Sealed for EnvelopeFilterCallbacks<F, C>
886 where
887 F: FnMut(&EnvelopeItem) -> bool,
888 C: FnMut(EnvelopeItem),
889 {
890 }
891
892 impl<F> Sealed for F where F: FnMut(&EnvelopeItem) -> bool {}
893}
894
895#[cfg(test)]
896mod test {
897 use std::borrow::Cow;
898 use std::str::FromStr;
899 use std::time::{Duration, SystemTime};
900
901 use protocol::Map;
902 use serde_json::Value;
903 use time::format_description::well_known::Rfc3339;
904 use time::OffsetDateTime;
905 use uuid::Uuid;
906
907 use super::*;
908 use crate::protocol::client_report::{Item, LossSource};
909 use crate::protocol::v7::client_report::Category;
910 use crate::protocol::v7::{
911 ClientReport, Level, LogLevel, MetricType, MonitorCheckInStatus, MonitorConfig,
912 MonitorSchedule, SampleRand, SessionAggregateItem, SessionAttributes, SessionStatus, Span,
913 };
914
915 fn to_str(envelope: Envelope) -> String {
916 let mut vec = Vec::new();
917 envelope.to_writer(&mut vec).unwrap();
918 String::from_utf8_lossy(&vec).to_string()
919 }
920
921 fn timestamp(s: &str) -> SystemTime {
922 let dt = OffsetDateTime::parse(s, &Rfc3339).unwrap();
923 let secs = dt.unix_timestamp() as u64;
924 let nanos = dt.nanosecond();
925 let duration = Duration::new(secs, nanos);
926 SystemTime::UNIX_EPOCH.checked_add(duration).unwrap()
927 }
928
929 fn session_attributes() -> SessionAttributes<'static> {
930 SessionAttributes {
931 release: Cow::Borrowed("release@1.0.0"),
932 environment: Some(Cow::Borrowed("production")),
933 ip_address: None,
934 user_agent: None,
935 }
936 }
937
938 fn collect_losses(envelope: &Envelope) -> Vec<(Category, u64)> {
939 envelope
940 .losses()
941 .map(|loss| (loss.category, loss.quantity))
942 .collect()
943 }
944
945 #[test]
946 fn test_empty() {
947 assert_eq!(to_str(Envelope::new()), "{}\n");
948 }
949
950 #[test]
951 fn raw_roundtrip() {
952 let buf = r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
953{"type":"event","length":74}
954{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
955"#;
956 let envelope = Envelope::from_bytes_raw(buf.to_string().into_bytes()).unwrap();
957 let serialized = to_str(envelope);
958 assert_eq!(&serialized, buf);
959
960 let random_invalid_bytes = b"oh stahp!\0\x01\x02";
961 let envelope = Envelope::from_bytes_raw(random_invalid_bytes.to_vec()).unwrap();
962 let mut serialized = Vec::new();
963 envelope.to_writer(&mut serialized).unwrap();
964 assert_eq!(&serialized, random_invalid_bytes);
965 }
966
967 #[test]
968 fn test_event() {
969 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
970 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
971 let event = Event {
972 event_id,
973 timestamp,
974 ..Default::default()
975 };
976 let envelope: Envelope = event.into();
977 assert_eq!(
978 to_str(envelope),
979 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
980{"type":"event","length":74}
981{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
982"#
983 )
984 }
985
986 #[test]
987 fn test_session() {
988 let session_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
989 let started = timestamp("2020-07-20T14:51:14.296Z");
990 let session = SessionUpdate {
991 session_id,
992 distinct_id: Some("foo@bar.baz".to_owned()),
993 sequence: None,
994 timestamp: None,
995 started,
996 init: true,
997 duration: Some(1.234),
998 status: SessionStatus::Ok,
999 errors: 123,
1000 attributes: SessionAttributes {
1001 release: "foo-bar@1.2.3".into(),
1002 environment: Some("production".into()),
1003 ip_address: None,
1004 user_agent: None,
1005 },
1006 };
1007 let mut envelope = Envelope::new();
1008 envelope.add_item(session);
1009 assert_eq!(
1010 to_str(envelope),
1011 r#"{}
1012{"type":"session","length":222}
1013{"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"}}
1014"#
1015 )
1016 }
1017
1018 #[test]
1019 fn test_transaction() {
1020 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1021 let span_id = "d42cee9fc3e74f5c".parse().unwrap();
1022 let trace_id = "335e53d614474acc9f89e632b776cc28".parse().unwrap();
1023 let start_timestamp = timestamp("2020-07-20T14:51:14.296Z");
1024 let spans = vec![Span {
1025 span_id,
1026 trace_id,
1027 start_timestamp,
1028 ..Default::default()
1029 }];
1030 let transaction = Transaction {
1031 event_id,
1032 start_timestamp,
1033 spans,
1034 ..Default::default()
1035 };
1036 let envelope: Envelope = transaction.into();
1037 assert_eq!(
1038 to_str(envelope),
1039 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1040{"type":"transaction","length":200}
1041{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","start_timestamp":1595256674.296,"spans":[{"span_id":"d42cee9fc3e74f5c","trace_id":"335e53d614474acc9f89e632b776cc28","start_timestamp":1595256674.296}]}
1042"#
1043 )
1044 }
1045
1046 #[test]
1047 fn test_monitor_checkin() {
1048 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1049
1050 let check_in = MonitorCheckIn {
1051 check_in_id,
1052 monitor_slug: "my-monitor".into(),
1053 status: MonitorCheckInStatus::Ok,
1054 duration: Some(123.4),
1055 environment: Some("production".into()),
1056 monitor_config: Some(MonitorConfig {
1057 schedule: MonitorSchedule::Crontab {
1058 value: "12 0 * * *".into(),
1059 },
1060 checkin_margin: Some(5),
1061 max_runtime: Some(30),
1062 timezone: Some("UTC".into()),
1063 failure_issue_threshold: None,
1064 recovery_threshold: None,
1065 }),
1066 };
1067 let envelope: Envelope = check_in.into();
1068 assert_eq!(
1069 to_str(envelope),
1070 r#"{}
1071{"type":"check_in","length":259}
1072{"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"}}
1073"#
1074 )
1075 }
1076
1077 #[test]
1078 fn test_monitor_checkin_with_thresholds() {
1079 let check_in_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1080
1081 let check_in = MonitorCheckIn {
1082 check_in_id,
1083 monitor_slug: "my-monitor".into(),
1084 status: MonitorCheckInStatus::Ok,
1085 duration: Some(123.4),
1086 environment: Some("production".into()),
1087 monitor_config: Some(MonitorConfig {
1088 schedule: MonitorSchedule::Crontab {
1089 value: "12 0 * * *".into(),
1090 },
1091 checkin_margin: Some(5),
1092 max_runtime: Some(30),
1093 timezone: Some("UTC".into()),
1094 failure_issue_threshold: Some(4),
1095 recovery_threshold: Some(7),
1096 }),
1097 };
1098 let envelope: Envelope = check_in.into();
1099 assert_eq!(
1100 to_str(envelope),
1101 r#"{}
1102{"type":"check_in","length":310}
1103{"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}}
1104"#
1105 )
1106 }
1107
1108 #[test]
1109 fn test_event_with_attachment() {
1110 let event_id = Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap();
1111 let timestamp = timestamp("2020-07-20T14:51:14.296Z");
1112 let event = Event {
1113 event_id,
1114 timestamp,
1115 ..Default::default()
1116 };
1117 let mut envelope: Envelope = event.into();
1118
1119 envelope.add_item(Attachment {
1120 buffer: "some content".as_bytes().to_vec(),
1121 filename: "file.txt".to_string(),
1122 ..Default::default()
1123 });
1124
1125 assert_eq!(
1126 to_str(envelope),
1127 r#"{"event_id":"22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c"}
1128{"type":"event","length":74}
1129{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1130{"type":"attachment","length":12,"filename":"file.txt","attachment_type":"event.attachment","content_type":"application/octet-stream"}
1131some content
1132"#
1133 )
1134 }
1135
1136 #[test]
1137 fn test_deserialize_envelope_empty() {
1138 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}";
1140 let envelope = Envelope::from_slice(bytes).unwrap();
1141
1142 let event_id = Uuid::from_str("9ec79c33ec9942ab8353589fcb2e04dc").unwrap();
1143 assert_eq!(envelope.headers.event_id, Some(event_id));
1144 assert_eq!(envelope.items().count(), 0);
1145 }
1146
1147 #[test]
1148 fn test_deserialize_envelope_empty_newline() {
1149 let bytes = b"{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n";
1151 let envelope = Envelope::from_slice(bytes).unwrap();
1152 assert_eq!(envelope.items().count(), 0);
1153 }
1154
1155 #[test]
1156 fn test_deserialize_envelope_empty_item_newline() {
1157 let bytes = b"\
1159 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1160 {\"type\":\"attachment\",\"length\":0}\n\
1161 \n\
1162 {\"type\":\"attachment\",\"length\":0}\n\
1163 ";
1164
1165 let envelope = Envelope::from_slice(bytes).unwrap();
1166 assert_eq!(envelope.items().count(), 2);
1167
1168 let mut items = envelope.items();
1169
1170 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1171 assert_eq!(attachment.buffer.len(), 0);
1172 } else {
1173 panic!("invalid item type");
1174 }
1175
1176 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1177 assert_eq!(attachment.buffer.len(), 0);
1178 } else {
1179 panic!("invalid item type");
1180 }
1181 }
1182
1183 #[test]
1184 fn test_deserialize_envelope_empty_item_eof() {
1185 let bytes = b"\
1187 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1188 {\"type\":\"attachment\",\"length\":0}\n\
1189 \n\
1190 {\"type\":\"attachment\",\"length\":0}\
1191 ";
1192
1193 let envelope = Envelope::from_slice(bytes).unwrap();
1194 assert_eq!(envelope.items().count(), 2);
1195
1196 let mut items = envelope.items();
1197
1198 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1199 assert_eq!(attachment.buffer.len(), 0);
1200 } else {
1201 panic!("invalid item type");
1202 }
1203
1204 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1205 assert_eq!(attachment.buffer.len(), 0);
1206 } else {
1207 panic!("invalid item type");
1208 }
1209 }
1210
1211 #[test]
1212 fn test_deserialize_envelope_implicit_length() {
1213 let bytes = b"\
1215 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1216 {\"type\":\"attachment\"}\n\
1217 helloworld\n\
1218 ";
1219
1220 let envelope = Envelope::from_slice(bytes).unwrap();
1221 assert_eq!(envelope.items().count(), 1);
1222
1223 let mut items = envelope.items();
1224
1225 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1226 assert_eq!(attachment.buffer.len(), 10);
1227 } else {
1228 panic!("invalid item type");
1229 }
1230 }
1231
1232 #[test]
1233 fn test_deserialize_envelope_implicit_length_eof() {
1234 let bytes = b"\
1236 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1237 {\"type\":\"attachment\"}\n\
1238 helloworld\
1239 ";
1240
1241 let envelope = Envelope::from_slice(bytes).unwrap();
1242 assert_eq!(envelope.items().count(), 1);
1243
1244 let mut items = envelope.items();
1245
1246 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1247 assert_eq!(attachment.buffer.len(), 10);
1248 } else {
1249 panic!("invalid item type");
1250 }
1251 }
1252
1253 #[test]
1254 fn test_deserialize_envelope_implicit_length_empty_eof() {
1255 let bytes = b"\
1257 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1258 {\"type\":\"attachment\"}\
1259 ";
1260
1261 let envelope = Envelope::from_slice(bytes).unwrap();
1262 assert_eq!(envelope.items().count(), 1);
1263
1264 let mut items = envelope.items();
1265
1266 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1267 assert_eq!(attachment.buffer.len(), 0);
1268 } else {
1269 panic!("invalid item type");
1270 }
1271 }
1272
1273 #[test]
1274 fn test_deserialize_envelope_multiple_items() {
1275 let bytes = b"\
1277 {\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n\
1278 {\"type\":\"attachment\",\"length\":10,\"content_type\":\"text/plain\",\"filename\":\"hello.txt\"}\n\
1279 \xef\xbb\xbfHello\r\n\n\
1280 {\"type\":\"event\",\"length\":41,\"content_type\":\"application/json\",\"filename\":\"application.log\"}\n\
1281 {\"message\":\"hello world\",\"level\":\"error\"}\n\
1282 ";
1283
1284 let envelope = Envelope::from_slice(bytes).unwrap();
1285 assert_eq!(envelope.items().count(), 2);
1286
1287 let mut items = envelope.items();
1288
1289 if let EnvelopeItem::Attachment(attachment) = items.next().unwrap() {
1290 assert_eq!(attachment.buffer.len(), 10);
1291 assert_eq!(attachment.buffer, b"\xef\xbb\xbfHello\r\n");
1292 assert_eq!(attachment.filename, "hello.txt");
1293 assert_eq!(attachment.content_type, Some("text/plain".to_string()));
1294 } else {
1295 panic!("invalid item type");
1296 }
1297
1298 if let EnvelopeItem::Event(event) = items.next().unwrap() {
1299 assert_eq!(event.message, Some("hello world".to_string()));
1300 assert_eq!(event.level, Level::Error);
1301 } else {
1302 panic!("invalid item type");
1303 }
1304 }
1305
1306 #[test]
1307 fn test_all_envelope_headers_roundtrip() {
1308 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"}}
1309{"type":"event","length":74}
1310{"event_id":"22d00b3fd1b14b5d8d2049d138cd8a9c","timestamp":1595256674.296}
1311"#;
1312
1313 let envelope = Envelope::from_slice(bytes);
1314 assert!(envelope.is_ok());
1315 let envelope = envelope.unwrap();
1316 let serialized = to_str(envelope);
1317 assert_eq!(bytes, serialized.as_bytes());
1318 }
1319
1320 #[test]
1321 fn test_sample_rand_rounding() {
1322 let envelope = Envelope::new().with_headers(
1323 EnvelopeHeaders::new().with_trace(
1324 DynamicSamplingContext::new()
1325 .with_sample_rand(SampleRand::try_from(0.999_999_9).unwrap()),
1326 ),
1327 );
1328 let expected = br#"{"trace":{"sample_rand":"0.999999"}}
1329"#;
1330
1331 let serialized = to_str(envelope);
1332 assert_eq!(expected, serialized.as_bytes());
1333 }
1334
1335 #[test]
1336 fn test_metric_container_header() {
1337 let metrics: EnvelopeItem = vec![Metric {
1338 r#type: protocol::MetricType::Counter,
1339 name: "api.requests".into(),
1340 value: 1.0,
1341 timestamp: timestamp("2026-03-02T13:36:02.000Z"),
1342 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1343 span_id: None,
1344 unit: None,
1345 attributes: Map::new(),
1346 }]
1347 .into();
1348
1349 let mut envelope = Envelope::new();
1350 envelope.add_item(metrics);
1351
1352 let expected = [
1353 serde_json::json!({}),
1354 serde_json::json!({
1355 "type": "trace_metric",
1356 "item_count": 1,
1357 "content_type": "application/vnd.sentry.items.trace-metric+json"
1358 }),
1359 serde_json::json!({
1360 "items": [{
1361 "type": "counter",
1362 "name": "api.requests",
1363 "value": 1.0,
1364 "timestamp": 1772458562,
1365 "trace_id": "335e53d614474acc9f89e632b776cc28"
1366 }]
1367 }),
1368 ];
1369
1370 let serialized = to_str(envelope);
1371 let actual = serialized
1372 .lines()
1373 .map(|line| serde_json::from_str::<Value>(line).expect("envelope has invalid JSON"));
1374
1375 assert!(actual.eq(expected.into_iter()));
1376 }
1377
1378 #[test]
1380 fn test_deserialize_serialized() {
1381 let event = Event {
1383 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1384 timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1385 ..Default::default()
1386 };
1387
1388 let transaction = Transaction {
1390 event_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9d").unwrap(),
1391 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1392 spans: vec![Span {
1393 span_id: "d42cee9fc3e74f5c".parse().unwrap(),
1394 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1395 start_timestamp: timestamp("2020-07-20T14:51:14.296Z"),
1396 ..Default::default()
1397 }],
1398 ..Default::default()
1399 };
1400
1401 let session = SessionUpdate {
1403 session_id: Uuid::parse_str("22d00b3f-d1b1-4b5d-8d20-49d138cd8a9c").unwrap(),
1404 distinct_id: Some("foo@bar.baz".to_owned()),
1405 sequence: None,
1406 timestamp: None,
1407 started: timestamp("2020-07-20T14:51:14.296Z"),
1408 init: true,
1409 duration: Some(1.234),
1410 status: SessionStatus::Ok,
1411 errors: 123,
1412 attributes: SessionAttributes {
1413 release: "foo-bar@1.2.3".into(),
1414 environment: Some("production".into()),
1415 ip_address: None,
1416 user_agent: None,
1417 },
1418 };
1419
1420 let attachment = Attachment {
1422 buffer: "some content".as_bytes().to_vec(),
1423 filename: "file.txt".to_string(),
1424 ..Default::default()
1425 };
1426
1427 let mut attributes = Map::new();
1428 attributes.insert("key".into(), "value".into());
1429 attributes.insert("num".into(), 10.into());
1430 attributes.insert("val".into(), 10.2.into());
1431 attributes.insert("bool".into(), false.into());
1432 let mut attributes_2 = attributes.clone();
1433 attributes_2.insert("more".into(), true.into());
1434 let logs: EnvelopeItem = vec![
1435 Log {
1436 level: protocol::LogLevel::Warn,
1437 body: "test".to_owned(),
1438 trace_id: Some("335e53d614474acc9f89e632b776cc28".parse().unwrap()),
1439 timestamp: timestamp("2022-07-25T14:51:14.296Z"),
1440 severity_number: Some(1.try_into().unwrap()),
1441 attributes,
1442 },
1443 Log {
1444 level: protocol::LogLevel::Error,
1445 body: "a body".to_owned(),
1446 trace_id: Some("332253d614472a2c9f89e232b7762c28".parse().unwrap()),
1447 timestamp: timestamp("2021-07-21T14:51:14.296Z"),
1448 severity_number: Some(1.try_into().unwrap()),
1449 attributes: attributes_2,
1450 },
1451 ]
1452 .into();
1453
1454 let mut metric_attributes = Map::new();
1455 metric_attributes.insert("route".into(), "/users".into());
1456 let metrics: EnvelopeItem = vec![Metric {
1457 r#type: protocol::MetricType::Distribution,
1458 name: "response.time".into(),
1459 value: 123.4,
1460 timestamp: timestamp("2022-07-26T14:51:14.296Z"),
1461 trace_id: "335e53d614474acc9f89e632b776cc28".parse().unwrap(),
1462 span_id: Some("d42cee9fc3e74f5c".parse().unwrap()),
1463 unit: Some("millisecond".into()),
1464 attributes: metric_attributes,
1465 }]
1466 .into();
1467
1468 let mut envelope: Envelope = Envelope::new();
1469 envelope.add_item(event);
1470 envelope.add_item(transaction);
1471 envelope.add_item(session);
1472 envelope.add_item(attachment);
1473 envelope.add_item(logs);
1474 envelope.add_item(metrics);
1475
1476 let serialized = to_str(envelope);
1477 let deserialized = Envelope::from_slice(serialized.as_bytes()).unwrap();
1478 assert_eq!(serialized, to_str(deserialized))
1479 }
1480
1481 #[test]
1482 fn losses_on_drop_maps_event_to_error() {
1483 let envelope: Envelope = Event::default().into();
1484
1485 assert_eq!(collect_losses(&envelope), vec![(Category::Error, 1)]);
1486 }
1487
1488 #[test]
1489 fn losses_on_drop_maps_session_update_to_session() {
1490 let envelope: Envelope = SessionUpdate {
1491 session_id: Uuid::nil(),
1492 distinct_id: None,
1493 sequence: None,
1494 timestamp: None,
1495 started: SystemTime::UNIX_EPOCH,
1496 init: false,
1497 duration: None,
1498 status: SessionStatus::Ok,
1499 errors: 0,
1500 attributes: session_attributes(),
1501 }
1502 .into();
1503
1504 assert_eq!(collect_losses(&envelope), vec![(Category::Session, 1)]);
1505 }
1506
1507 #[test]
1508 fn losses_on_drop_maps_attachment_to_buffer_bytes() {
1509 let envelope: Envelope = Attachment {
1510 buffer: b"attachment".to_vec(),
1511 filename: "attachment.bin".to_owned(),
1512 ..Default::default()
1513 }
1514 .into();
1515
1516 assert_eq!(collect_losses(&envelope), vec![(Category::Attachment, 10)]);
1517 }
1518
1519 #[test]
1520 fn losses_on_drop_maps_monitor_check_in_to_monitor() {
1521 let envelope: Envelope = MonitorCheckIn {
1522 check_in_id: Uuid::nil(),
1523 monitor_slug: "monitor".to_owned(),
1524 status: MonitorCheckInStatus::Ok,
1525 environment: None,
1526 duration: None,
1527 monitor_config: None,
1528 }
1529 .into();
1530
1531 assert_eq!(collect_losses(&envelope), vec![(Category::Monitor, 1)]);
1532 }
1533
1534 #[test]
1535 fn losses_on_drop_sums_session_aggregate_status_counts() {
1536 let envelope: Envelope = SessionAggregates {
1537 aggregates: vec![
1538 SessionAggregateItem {
1539 started: SystemTime::UNIX_EPOCH,
1540 distinct_id: None,
1541 exited: 2,
1542 errored: 3,
1543 abnormal: 5,
1544 crashed: 7,
1545 },
1546 SessionAggregateItem {
1547 started: SystemTime::UNIX_EPOCH,
1548 distinct_id: None,
1549 exited: 11,
1550 errored: 13,
1551 abnormal: 17,
1552 crashed: 19,
1553 },
1554 ],
1555 attributes: session_attributes(),
1556 }
1557 .into();
1558
1559 assert_eq!(collect_losses(&envelope), vec![(Category::Session, 77)]);
1560 }
1561
1562 #[test]
1563 fn losses_on_drop_counts_transaction_root_span() {
1564 let envelope: Envelope = Transaction {
1565 spans: vec![],
1566 ..Default::default()
1567 }
1568 .into();
1569
1570 assert_eq!(
1571 collect_losses(&envelope),
1572 vec![(Category::Transaction, 1), (Category::Span, 1)]
1573 );
1574 }
1575
1576 #[test]
1577 fn losses_on_drop_counts_transaction_child_spans() {
1578 let envelope: Envelope = Transaction {
1579 spans: vec![Span::default(), Span::default(), Span::default()],
1580 ..Default::default()
1581 }
1582 .into();
1583
1584 assert_eq!(
1585 collect_losses(&envelope),
1586 vec![(Category::Transaction, 1), (Category::Span, 4)]
1587 );
1588 }
1589
1590 #[test]
1591 fn losses_on_drop_counts_minimal_log_bytes() {
1592 let envelope: Envelope = vec![Log {
1593 level: LogLevel::Info,
1594 body: String::new(),
1595 trace_id: None,
1596 timestamp: SystemTime::UNIX_EPOCH,
1597 severity_number: None,
1598 attributes: Map::new(),
1599 }]
1600 .into();
1601
1602 assert_eq!(
1603 collect_losses(&envelope),
1604 vec![(Category::LogItem, 1), (Category::LogByte, 1)]
1605 );
1606 }
1607
1608 #[test]
1609 fn losses_on_drop_counts_complex_log_bytes() {
1610 let mut attributes = Map::new();
1611 attributes.insert("k1".to_owned(), "string value".into());
1612 attributes.insert("k2".to_owned(), u64::MAX.into());
1613 attributes.insert("k3".to_owned(), 42.0.into());
1614 attributes.insert("k4".to_owned(), false.into());
1615 attributes.insert(
1616 "k5".to_owned(),
1617 serde_json::json!({
1618 "nested": {
1619 "array": [1.0, 2, -12, "7 bytes", false]
1620 }
1621 })
1622 .into(),
1623 );
1624 let envelope: Envelope = vec![Log {
1625 level: LogLevel::Info,
1626 body: "7 bytes".to_owned(),
1627 trace_id: None,
1628 timestamp: SystemTime::UNIX_EPOCH,
1629 severity_number: None,
1630 attributes,
1631 }]
1632 .into();
1633
1634 assert_eq!(
1635 collect_losses(&envelope),
1636 vec![(Category::LogItem, 1), (Category::LogByte, 89)]
1637 );
1638 }
1639
1640 #[test]
1641 fn losses_on_drop_counts_minimal_metric_bytes() {
1642 let envelope: Envelope = vec![Metric {
1643 r#type: MetricType::Counter,
1644 name: Cow::Borrowed(""),
1645 value: 1.0,
1646 timestamp: SystemTime::UNIX_EPOCH,
1647 trace_id: Default::default(),
1648 span_id: None,
1649 unit: None,
1650 attributes: Map::new(),
1651 }]
1652 .into();
1653
1654 assert_eq!(
1655 collect_losses(&envelope),
1656 vec![(Category::TraceMetric, 1), (Category::TraceMetricByte, 8)]
1657 );
1658 }
1659
1660 #[test]
1661 fn losses_on_drop_counts_complex_metric_bytes() {
1662 let mut attributes = Map::new();
1663 attributes.insert(
1664 Cow::Borrowed("foo"),
1665 "ඞ and some more equals 33 bytes".into(),
1666 );
1667 let envelope: Envelope = vec![Metric {
1668 r#type: MetricType::Counter,
1669 name: Cow::Borrowed("counter"),
1670 value: 1.0,
1671 timestamp: SystemTime::UNIX_EPOCH,
1672 trace_id: Default::default(),
1673 span_id: None,
1674 unit: None,
1675 attributes,
1676 }]
1677 .into();
1678
1679 assert_eq!(
1680 collect_losses(&envelope),
1681 vec![(Category::TraceMetric, 1), (Category::TraceMetricByte, 51)]
1682 );
1683 }
1684
1685 #[test]
1686 fn losses_on_drop_skips_client_reports() {
1687 let envelope: Envelope = ClientReport::new(<[Item; 0]>::default()).into();
1688
1689 assert!(collect_losses(&envelope).is_empty());
1690 }
1691
1692 #[test]
1693 fn losses_on_drop_skips_raw_envelopes() {
1694 let envelope = Envelope::from_bytes_raw(b"not parsed".to_vec()).unwrap();
1695
1696 assert!(collect_losses(&envelope).is_empty());
1697 }
1698
1699 #[test]
1700 fn losses_on_drop_flattens_losses_from_all_envelope_items() {
1701 let log = Log {
1702 level: LogLevel::Warn,
1703 body: "flattened".to_owned(),
1704 trace_id: None,
1705 timestamp: SystemTime::UNIX_EPOCH,
1706 severity_number: None,
1707 attributes: Map::new(),
1708 };
1709 let mut envelope = Envelope::new();
1710 envelope.add_item(Event::default());
1711 envelope.add_item(Transaction {
1712 spans: vec![Span::default(), Span::default()],
1713 ..Default::default()
1714 });
1715 envelope.add_item(vec![log]);
1716 envelope.add_item(vec![Metric {
1717 r#type: MetricType::Counter,
1718 name: Cow::Borrowed("flattened.metric"),
1719 value: 1.0,
1720 timestamp: SystemTime::UNIX_EPOCH,
1721 trace_id: Default::default(),
1722 span_id: None,
1723 unit: None,
1724 attributes: Map::new(),
1725 }]);
1726
1727 assert_eq!(
1728 collect_losses(&envelope),
1729 vec![
1730 (Category::Error, 1),
1731 (Category::Transaction, 1),
1732 (Category::Span, 3),
1733 (Category::LogItem, 1),
1734 (Category::LogByte, 9),
1735 (Category::TraceMetric, 1),
1736 (Category::TraceMetricByte, 24),
1737 ]
1738 );
1739 }
1740}