diff --git a/sentry-core/src/client/client_reports/mod.rs b/sentry-core/src/client/client_reports/mod.rs index 841e5db3..b98fa220 100644 --- a/sentry-core/src/client/client_reports/mod.rs +++ b/sentry-core/src/client/client_reports/mod.rs @@ -8,9 +8,9 @@ #[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))] use std::sync::Arc; -use sentry_types::protocol::v7::client_report::{Category, Reason}; #[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))] -use sentry_types::protocol::v7::client_report::{ItemLoss, LossSource}; +use sentry_types::protocol::v7::client_report::ItemLoss; +use sentry_types::protocol::v7::client_report::{Category, LossSource, Reason}; use sentry_types::protocol::v7::ClientReport; #[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))] @@ -52,14 +52,16 @@ impl ClientReportAggregator { /// Record lost Sentry data. /// /// Records the given Sentry telemetry item as discarded for the provided `reason`. - #[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))] pub(crate) fn record_lost_data(&self, data: &L, reason: Reason) { + #[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))] data.losses().for_each(|loss| { let ItemLoss { category, quantity, .. } = loss; self.record_loss(category, reason, quantity) }); + #[cfg(not(all(target_has_atomic = "64", target_has_atomic = "8")))] + let _ = (data, reason); } /// Records `quantity` lost items for `category` and `reason`. diff --git a/sentry-core/src/client/envelope_sender.rs b/sentry-core/src/client/envelope_sender.rs index 689160f5..be86fa92 100644 --- a/sentry-core/src/client/envelope_sender.rs +++ b/sentry-core/src/client/envelope_sender.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::Duration; use sentry_types::protocol::v7::client_report::{ - Category as ClientReportCategory, Reason as ClientReportReason, + Category as ClientReportCategory, LossSource, Reason as ClientReportReason, }; use sentry_types::protocol::v7::EnvelopeItem; @@ -80,6 +80,10 @@ impl EnvelopeSender { } } + pub(super) fn record_lost_data(&self, data: &L, reason: ClientReportReason) { + self.client_report_aggregator.record_lost_data(data, reason); + } + /// Records `quantity` lost items for `category` and `reason`. pub(super) fn record_loss( &self, diff --git a/sentry-core/src/client/mod.rs b/sentry-core/src/client/mod.rs index f70b62cc..ba1792e2 100644 --- a/sentry-core/src/client/mod.rs +++ b/sentry-core/src/client/mod.rs @@ -16,7 +16,7 @@ use crate::protocol::SessionUpdate; use crate::transport::TransportOptions; use rand::random; use sentry_types::protocol::v7::client_report::{ - Category as ClientReportCategory, Reason as ClientReportReason, + Category as ClientReportCategory, LossSource, Reason as ClientReportReason, }; use sentry_types::random_uuid; @@ -467,6 +467,10 @@ impl Client { event_id } + pub(crate) fn record_lost_data(&self, data: &L, reason: ClientReportReason) { + self.envelope_sender.record_lost_data(data, reason); + } + /// Records `quantity` lost items for `category` and `reason`. fn record_loss( &self, diff --git a/sentry-core/src/performance.rs b/sentry-core/src/performance.rs index 5416746e..a5d405e3 100644 --- a/sentry-core/src/performance.rs +++ b/sentry-core/src/performance.rs @@ -4,6 +4,8 @@ use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::SystemTime; +#[cfg(feature = "client")] +use sentry_types::protocol::v7::client_report::Reason as ClientReportReason; use sentry_types::protocol::v7::SpanId; use crate::{protocol, Hub}; @@ -855,6 +857,11 @@ impl Transaction { // Discard `Transaction` unless sampled. if !inner.sampled { + if let Some(transaction) = inner.transaction.take() { + if let Some(client) = inner.client.as_ref() { + client.record_lost_data(&transaction, ClientReportReason::SampleRate); + } + } return; } @@ -1139,6 +1146,8 @@ impl Span { if let Some(transaction) = inner.transaction.as_mut() { if transaction.spans.len() <= MAX_SPANS { transaction.spans.push(span.clone()); + } else if let Some(client) = inner.client.as_ref() { + client.record_lost_data(&*span, ClientReportReason::BufferOverflow); } } }} diff --git a/sentry-core/tests/client_reports.rs b/sentry-core/tests/client_reports.rs index c350dc19..499119ec 100644 --- a/sentry-core/tests/client_reports.rs +++ b/sentry-core/tests/client_reports.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use sentry_core::protocol::client_report::Reason; use sentry_core::protocol::{EnvelopeItem, Event}; use sentry_core::test::TestTransport; -use sentry_core::{Client, ClientOptions, Envelope, Integration, Scope}; +use sentry_core::{Client, ClientOptions, Envelope, Hub, Integration, Scope, TransactionContext}; struct DroppingIntegration; @@ -27,7 +27,7 @@ fn client_with_options(transport: Arc, options: ClientOptions) -> }) } -fn assert_client_report(envelope: &Envelope, reason: Reason) { +fn assert_client_report(envelope: &Envelope, expected: serde_json::Value) { let client_report = envelope .items() .find_map(|item| match item { @@ -37,10 +37,7 @@ fn assert_client_report(envelope: &Envelope, reason: Reason) { .expect("envelope should contain a client report"); let value = serde_json::to_value(client_report).unwrap(); - assert_eq!( - value["discarded_events"], - serde_json::json!([{ "category": "error", "reason": reason, "quantity": 1 }]) - ); + assert_eq!(value["discarded_events"], expected); } fn assert_drop_records_client_report(options: ClientOptions, capture: F, reason: Reason) @@ -55,7 +52,10 @@ where let envelopes = transport.fetch_and_clear_envelopes(); assert_eq!(envelopes.len(), 1); - assert_client_report(&envelopes[0], reason); + assert_client_report( + &envelopes[0], + serde_json::json!([{ "category": "error", "reason": reason, "quantity": 1 }]), + ); } #[test] @@ -116,3 +116,70 @@ fn client_report_records_sample_rate_drop() { Reason::SampleRate, ); } + +#[test] +fn client_report_records_unsampled_transaction_and_spans() { + let transport = TestTransport::new(); + let client = Arc::new(client_with_options( + transport.clone(), + ClientOptions { + traces_sample_rate: 0.0, + ..Default::default() + }, + )); + + Hub::run( + Arc::new(Hub::new(Some(client.clone()), Arc::new(Default::default()))), + || { + let transaction = sentry_core::start_transaction(TransactionContext::new("tx", "op")); + transaction.start_child("child", "one").finish(); + transaction.start_child("child", "two").finish(); + transaction.finish(); + }, + ); + client.send_envelope(Envelope::new()); + + let envelopes = transport.fetch_and_clear_envelopes(); + assert_eq!(envelopes.len(), 1); + assert_client_report( + &envelopes[0], + serde_json::json!([ + { "category": "transaction", "reason": "sample_rate", "quantity": 1 }, + { "category": "span", "reason": "sample_rate", "quantity": 3 }, + ]), + ); +} + +#[test] +fn client_report_records_transaction_span_cap_drop() { + // Keep in sync with `MAX_SPANS` in `sentry-core/src/performance.rs`. + const MAX_SPANS: usize = 1_000; + + let transport = TestTransport::new(); + let client = Arc::new(client_with_options( + transport.clone(), + ClientOptions { + traces_sample_rate: 1.0, + ..Default::default() + }, + )); + + Hub::run( + Arc::new(Hub::new(Some(client.clone()), Arc::new(Default::default()))), + || { + let transaction = sentry_core::start_transaction(TransactionContext::new("tx", "op")); + for _ in 0..=MAX_SPANS { + transaction.start_child("child", "kept").finish(); + } + transaction.start_child("child", "dropped").finish(); + }, + ); + client.send_envelope(Envelope::new()); + + let envelopes = transport.fetch_and_clear_envelopes(); + assert_eq!(envelopes.len(), 1); + assert_client_report( + &envelopes[0], + serde_json::json!([{ "category": "span", "reason": "buffer_overflow", "quantity": 1 }]), + ); +} diff --git a/sentry-core/tests/metrics.rs b/sentry-core/tests/metrics.rs index a7432acd..01635a12 100644 --- a/sentry-core/tests/metrics.rs +++ b/sentry-core/tests/metrics.rs @@ -368,8 +368,9 @@ fn metrics_span_id_from_active_span() { .expect("expected one envelope"); let item = envelope .into_items() + .filter(|item| !matches!(item, EnvelopeItem::ClientReport(_))) .try_into_only_item() - .expect("expected one item"); + .expect("expected one non-client-report item"); let mut metrics = item.into_metrics().expect("expected metrics item"); let metric = metrics.pop().expect("expected one metric"); diff --git a/sentry-types/src/protocol/client_report/envelope_losses.rs b/sentry-types/src/protocol/client_report/envelope_losses.rs index d65d716d..48f72054 100644 --- a/sentry-types/src/protocol/client_report/envelope_losses.rs +++ b/sentry-types/src/protocol/client_report/envelope_losses.rs @@ -4,7 +4,7 @@ use std::mem; use crate::protocol::v7::{ Attachment, ClientReport, Envelope, EnvelopeItem, Event, ItemContainer, Log, Metric, - MonitorCheckIn, SessionAggregateItem, SessionAggregates, SessionUpdate, Transaction, + MonitorCheckIn, SessionAggregateItem, SessionAggregates, SessionUpdate, Span, Transaction, }; use super::{relay_size, Category}; @@ -113,6 +113,12 @@ impl LossSource for ItemContainer { } } +impl LossSource for Span { + fn losses(&self) -> impl Iterator + '_ { + span_losses(self) + } +} + /// Returns an iterator over the lost items in an envelope item, if it is dropped. fn envelope_item_losses(envelope_item: &EnvelopeItem) -> ItemLossIter { match envelope_item { @@ -236,6 +242,11 @@ fn metric_losses(metrics: &[Metric]) -> ItemLossIter { ]) } +/// A span always results in a loss of a single span. +fn span_losses(_span: &Span) -> ItemLossIter { + ItemLossIter::new([ItemLoss::new(Category::Span, 1)]) +} + impl ItemLossIter { /// Creates an iterator from zero, one, or two [`ItemLoss`] values. fn new(value: T) -> Self @@ -276,7 +287,7 @@ impl From<[ItemLoss; 2]> for ItemLossIter { mod private { use super::{ Attachment, ClientReport, Envelope, EnvelopeItem, Event, ItemContainer, MonitorCheckIn, - SessionAggregates, SessionUpdate, Transaction, + SessionAggregates, SessionUpdate, Span, Transaction, }; /// Prevents downstream implementations of [`LossSource`](super::LossSource). @@ -292,4 +303,5 @@ mod private { impl Sealed for MonitorCheckIn {} impl Sealed for ClientReport {} impl Sealed for ItemContainer {} + impl Sealed for Span {} } diff --git a/sentry-types/src/protocol/client_report/mod.rs b/sentry-types/src/protocol/client_report/mod.rs index 4a60d487..91a170f2 100644 --- a/sentry-types/src/protocol/client_report/mod.rs +++ b/sentry-types/src/protocol/client_report/mod.rs @@ -47,6 +47,8 @@ indexed_enum! { RatelimitBackoff, /// An internal queue overflowed (e.g. the transport queue). QueueOverflow, + /// An SDK internal buffer overflowed. + BufferOverflow, /// An event was dropped by an event processor. EventProcessor, /// An event was dropped by a `before_send` callback.