Skip to content

Commit 11aeb31

Browse files
feat(client-reports): Add transport loss recorder
Add a public `ClientReportRecorder` handle for transports to record lost Sentry data without exposing the full client-report aggregator. Pass the recorder through `TransportOptions` when the SDK client builds a transport. Recorded losses are aggregated into future `client_report` envelope items, so transports can report drops without sending extra requests. Keep backwards-compatible transport construction paths working by using a no-op recorder when no client-report aggregator is available. Built-in transports will start calling the recorder in follow-up PRs. Resolves [#1148](#1148) Resolves [RUST-223](https://linear.app/getsentry/issue/RUST-223)
1 parent 65568db commit 11aeb31

7 files changed

Lines changed: 119 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Added [`TransportFactory::create_transport_with_options`](https://docs.rs/sentry-core/latest/sentry_core/trait.TransportFactory.html#method.create_transport_with_options), which constructs transports from [`TransportOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.TransportOptions.html) instead of full [`ClientOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html) ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).
88
- Added transport-specific options types and `with_options` constructors for built-in HTTP transports, including `ReqwestHttpTransportOptions`, `CurlHttpTransportOptions`, `UreqHttpTransportOptions`, and `EmbeddedSVCHttpTransportOptions` ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).
99
- Added client report protocol types in `sentry-types`, including [`ClientReport`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/struct.ClientReport.html), [`ClientReportItem`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/struct.ClientReportItem.html), [`DataCategory`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/enum.DataCategory.html), and [`DiscardReason`](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/enum.DiscardReason.html), plus support for serializing [`client_report` envelope items](https://docs.rs/sentry-types/latest/sentry_types/protocol/v7/enum.EnvelopeItem.html#variant.ClientReport) ([#1144](https://github.com/getsentry/sentry-rust/pull/1144)).
10+
- Added [`ClientReportRecorder`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientReportRecorder.html), which modern [`TransportFactory`](https://docs.rs/sentry-core/latest/sentry_core/trait.TransportFactory.html) implementations receive via [`TransportOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.TransportOptions.html). Transports can use this recorder to record discarded Sentry data; the SDK aggregates these reported losses and automatically sends them in a future envelope ([#1158](https://github.com/getsentry/sentry-rust/pull/1158)).
1011

1112
### Behavior Changes
1213

sentry-core/src/client/client_reports/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use sentry_types::protocol::v7::{ClientReport, DataCategory, DiscardReason};
1515
use self::inner::ClientReportAggregatorInner;
1616

1717
mod inner;
18+
mod recorder;
19+
20+
pub use self::recorder::ClientReportRecorder;
1821

1922
/// Aggregates counts for lost data that should be reported in client reports.
2023
///
@@ -49,7 +52,6 @@ impl ClientReportAggregator {
4952
/// This method updates aggregate counters only. The loss is not sent until a later call to
5053
/// [`Self::take_pending_report`] drains the counters and returns a [`ClientReport`] for an
5154
/// outgoing envelope. A `quantity` of zero is ignored.
52-
#[expect(dead_code, reason = "we will add calls in a follow-up PR")]
5355
pub(crate) fn record_loss(&self, category: DataCategory, reason: DiscardReason, quantity: u64) {
5456
#[cfg(all(target_has_atomic = "64", target_has_atomic = "8"))]
5557
self.inner.record_loss(category, reason, quantity);
@@ -74,4 +76,9 @@ impl ClientReportAggregator {
7476
None
7577
}
7678
}
79+
80+
/// Creates a [`ClientReportRecorder`] which records into this aggregator.
81+
pub(super) fn recorder(&self) -> ClientReportRecorder {
82+
ClientReportRecorder::new(self)
83+
}
7784
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Contains the [`ClientReportRecorder`] type, which allows recording data losses.
2+
//!
3+
//! This type is `pub` to allow transports, which are defined outside the `sentry-core` crate, to
4+
//! record lost events, without giving full access to the underlying [`ClientReportAggregator`].
5+
6+
use std::sync::{Arc, Weak};
7+
8+
use sentry_types::protocol::v7::{DataCategory, DiscardReason};
9+
10+
use super::{ClientReportAggregator, ClientReportAggregatorInner};
11+
12+
/// A handle for recording lost Sentry data.
13+
///
14+
/// Lost items recorded here will be aggregated into a [client report] and eventually sent to
15+
/// Sentry. We attempt to send client reports with a future envelope, so recording lost events
16+
/// should not lead to increased requests to Sentry.
17+
///
18+
/// Cloning has [`Arc`]-like semantics in the sense that clones record into the same client report
19+
/// aggregator.
20+
///
21+
/// As client reports require atomics for aggregation, this struct's methods are no-ops on
22+
/// platforms which lack support for 8-bit and/or 64-bit atomic operations.
23+
///
24+
/// [client report]: https://develop.sentry.dev/sdk/telemetry/client-reports/
25+
#[derive(Debug, Clone)]
26+
pub struct ClientReportRecorder {
27+
/// The inner aggregator.
28+
///
29+
/// As the recorder only records losses, but cannot retrieve them, it does not make sense for
30+
/// the recorder to keep the underlying aggregator alive.
31+
///
32+
/// We therefore store `inner` as a [`Weak`] so that we do not keep the aggregator alive.
33+
///
34+
/// In practice, we would expect the recorder not to outlive the underlying aggregator, but in
35+
/// case it happens, it makes sense to make the `Weak` relationship explicit.
36+
#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
37+
inner: Weak<ClientReportAggregatorInner>,
38+
}
39+
40+
impl ClientReportRecorder {
41+
/// Record `quantity` lost items, of the given `category`, discarded for the given `reason`.
42+
pub fn record_loss(&self, category: DataCategory, reason: DiscardReason, quantity: u64) {
43+
#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
44+
if let Some(aggregator) = self.aggregator() {
45+
aggregator.record_loss(category, reason, quantity);
46+
}
47+
#[cfg(not(all(target_has_atomic = "8", target_has_atomic = "64")))]
48+
let _ = (category, reason, quantity);
49+
}
50+
51+
/// Creates a new no-op [`ClientReportRecorder`].
52+
///
53+
/// This is used in backwards-compatibility code to handle the case where we might not have an
54+
/// aggregator.
55+
///
56+
/// To get a useful [`ClientReportRecorder`], use [`ClientReportAggregator::recorder`].
57+
pub(crate) fn new_no_op() -> Self {
58+
Self { inner: Weak::new() }
59+
}
60+
61+
/// Create a new [`ClientReportRecorder`] which records into the given
62+
/// [`ClientReportAggregator`].
63+
#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
64+
pub(super) fn new(aggregator: &ClientReportAggregator) -> Self {
65+
#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
66+
{
67+
let ClientReportAggregator {
68+
inner: aggregator_inner,
69+
} = aggregator;
70+
71+
let inner = Arc::downgrade(aggregator_inner);
72+
Self { inner }
73+
}
74+
#[cfg(not(all(target_has_atomic = "8", target_has_atomic = "64")))]
75+
{
76+
let _ = aggregator;
77+
Self {}
78+
}
79+
}
80+
81+
/// Helper to obtain the [`ClientReportAggregator`] we record into, if still alive.
82+
///
83+
/// This works by upgrading the [`Weak`] pointer to the [`ClientReportAggregatorInner`] stored
84+
/// in `self.inner`, then wrapping it in a [`ClientReportAggregator`].
85+
#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
86+
fn aggregator(&self) -> Option<ClientReportAggregator> {
87+
self.inner
88+
.upgrade()
89+
.map(|inner| ClientReportAggregator { inner })
90+
}
91+
}

sentry-core/src/client/envelope_sender.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::time::Duration;
1010
use sentry_types::protocol::v7::EnvelopeItem;
1111

1212
use self::slot::TransportSlot;
13-
use super::client_reports::ClientReportAggregator;
13+
use super::client_reports::{ClientReportAggregator, ClientReportRecorder};
1414
use crate::{Envelope, Transport};
1515

1616
/// Sends envelopes through the client's transport and tracks lost data.
@@ -65,10 +65,11 @@ impl EnvelopeSender {
6565
/// Creates a sender using the transport returned by the provided builder callback.
6666
pub(super) fn new<F>(transport_builder: F) -> Self
6767
where
68-
F: FnOnce() -> Arc<dyn Transport>,
68+
F: FnOnce(ClientReportRecorder) -> Arc<dyn Transport>,
6969
{
7070
let client_report_aggregator = ClientReportAggregator::new();
71-
let transport_slot = TransportSlot::new(transport_builder());
71+
let recorder = client_report_aggregator.recorder();
72+
let transport_slot = TransportSlot::new(transport_builder(recorder));
7273

7374
Self {
7475
transport_slot,

sentry-core/src/client/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ mod batcher;
4141
mod client_reports;
4242
mod envelope_sender;
4343

44+
pub use self::client_reports::ClientReportRecorder;
4445
pub(crate) use self::envelope_sender::EnvelopeSender;
4546

4647
impl<T: Into<ClientOptions>> From<T> for Client {
@@ -604,13 +605,14 @@ fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
604605
} = client_options;
605606

606607
match (dsn.as_ref(), transport_factory.as_ref()) {
607-
(Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|| {
608+
(Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|client_report_recorder| {
608609
let options = TransportOptions {
609610
dsn: dsn.clone(),
610611
user_agent: user_agent.clone(),
611612
http_proxy: http_proxy.clone(),
612613
https_proxy: https_proxy.clone(),
613614
accept_invalid_certs: *accept_invalid_certs,
615+
client_report_recorder,
614616
};
615617

616618
transport_factory.create_transport_with_options(options)

sentry-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ pub use crate::clientoptions::MaxRequestBodySize;
152152
#[cfg(feature = "client")]
153153
pub use crate::{client::Client, hub_impl::SwitchGuard as HubSwitchGuard};
154154

155+
#[cfg(feature = "client")]
156+
pub use crate::client::ClientReportRecorder;
157+
155158
// test utilities
156159
#[cfg(feature = "test")]
157160
pub mod test;

sentry-core/src/transport/options.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::borrow::Cow;
55
use sentry_types::Dsn;
66

77
use crate::ClientOptions;
8+
#[cfg(feature = "client")]
9+
use crate::ClientReportRecorder;
810

911
/// Options for a transport.
1012
#[derive(Debug)]
@@ -20,6 +22,9 @@ pub struct TransportOptions {
2022
pub https_proxy: Option<Cow<'static, str>>,
2123
/// Whether TLS certificate validation should be disabled.
2224
pub accept_invalid_certs: bool,
25+
/// A handle for recording lost Sentry data.
26+
#[cfg(feature = "client")]
27+
pub client_report_recorder: ClientReportRecorder,
2328
}
2429

2530
impl TransportOptions {
@@ -46,6 +51,8 @@ impl TransportOptions {
4651
http_proxy: http_proxy.clone(),
4752
https_proxy: https_proxy.clone(),
4853
accept_invalid_certs: *accept_invalid_certs,
54+
#[cfg(feature = "client")]
55+
client_report_recorder: ClientReportRecorder::new_no_op(),
4956
})
5057
}
5158

@@ -63,6 +70,8 @@ impl TransportOptions {
6370
http_proxy,
6471
https_proxy,
6572
accept_invalid_certs,
73+
#[cfg(feature = "client")]
74+
client_report_recorder: _,
6675
} = self;
6776

6877
let dsn = Some(dsn);

0 commit comments

Comments
 (0)