Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions sentry-core/src/client/client_reports/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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<L: LossSource>(&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`.
Expand Down
6 changes: 5 additions & 1 deletion sentry-core/src/client/envelope_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -80,6 +80,10 @@ impl EnvelopeSender {
}
}

pub(super) fn record_lost_data<L: LossSource>(&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,
Expand Down
6 changes: 5 additions & 1 deletion sentry-core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -467,6 +467,10 @@ impl Client {
event_id
}

pub(crate) fn record_lost_data<L: LossSource>(&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,
Expand Down
9 changes: 9 additions & 0 deletions sentry-core/src/performance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
}
}
Comment thread
lcian marked this conversation as resolved.
return;
}

Expand Down Expand Up @@ -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);
}
}
}}
Expand Down
81 changes: 74 additions & 7 deletions sentry-core/tests/client_reports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -27,7 +27,7 @@ fn client_with_options(transport: Arc<TestTransport>, 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 {
Expand All @@ -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<F>(options: ClientOptions, capture: F, reason: Reason)
Expand All @@ -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]
Expand Down Expand Up @@ -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,

@lcian lcian Jun 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct behavior? Basically if tracing is fully disabled, we still report everything as dropped due to sample_rate?
This seems a bit surprising, if I se traces_sample_rate: 0.0 it just means I don't wanna use tracing at all and it's not like I'm "dropping data".
However it's entirely possible that other SDKs work like this, in which case we should just align with them.

..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 }]),
);
}
3 changes: 2 additions & 1 deletion sentry-core/tests/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
16 changes: 14 additions & 2 deletions sentry-types/src/protocol/client_report/envelope_losses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -113,6 +113,12 @@ impl LossSource for ItemContainer {
}
}

impl LossSource for Span {
fn losses(&self) -> impl Iterator<Item = ItemLoss> + '_ {
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 {
Expand Down Expand Up @@ -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<T>(value: T) -> Self
Expand Down Expand Up @@ -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).
Expand All @@ -292,4 +303,5 @@ mod private {
impl Sealed for MonitorCheckIn {}
impl Sealed for ClientReport {}
impl Sealed for ItemContainer {}
impl Sealed for Span {}
}
2 changes: 2 additions & 0 deletions sentry-types/src/protocol/client_report/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading