sentry_core/client/client_reports/recorder.rs
1//! Contains the [`Recorder`] 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#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
7use std::sync::{Arc, Weak};
8
9use sentry_types::protocol::v7::client_report::LossSource;
10#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
11use sentry_types::protocol::v7::client_report::Reason;
12
13use super::ClientReportAggregator;
14#[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
15use super::ClientReportAggregatorInner;
16
17/// A handle for a transport to record lost Sentry data.
18///
19/// Lost items recorded here will be aggregated into a [client report] and eventually sent to
20/// Sentry. We attempt to send client reports with a future envelope, so recording lost events
21/// should not lead to increased requests to Sentry.
22///
23/// Cloning has [`Arc`]-like semantics in the sense that clones record into the same client report
24/// aggregator.
25///
26/// As client reports require atomics for aggregation, this struct's methods are no-ops on
27/// platforms which lack support for 8-bit and/or 64-bit atomic operations.
28///
29/// [client report]: https://develop.sentry.dev/sdk/telemetry/client-reports/
30#[derive(Debug, Clone, Default)]
31pub struct Recorder {
32 /// The inner aggregator.
33 ///
34 /// As the recorder only records losses, but cannot retrieve them, it does not make sense for
35 /// the recorder to keep the underlying aggregator alive.
36 ///
37 /// We therefore store `inner` as a [`Weak`] so that we do not keep the aggregator alive.
38 ///
39 /// In practice, we would expect the recorder not to outlive the underlying aggregator, but in
40 /// case it happens, it makes sense to make the `Weak` relationship explicit.
41 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
42 inner: Weak<ClientReportAggregatorInner>,
43}
44
45/// Reasons for which a transport might drop data.
46///
47/// This is a subset of [`Reason`], as defined in [`sentry_types`] because only some of those
48/// reasons may be applicable to transports.
49#[derive(Debug, Clone, Copy)]
50#[non_exhaustive]
51pub enum TransportLossReason {
52 /// Use this reason to record an error when sending an envelope.
53 ///
54 /// This reason should be used, for example, if the server responds with a non-`2xx` HTTP
55 /// when the envelope is sent.
56 ///
57 /// However, transports **must never** record a loss when receiving an HTTP `429`
58 /// (rate-limiting) response, as the server already records a loss in this case.
59 SendError,
60 /// Used for an internal error.
61 ///
62 /// This reason should be used, for example, if an I/O error prevents the envelope from
63 /// being serialized.
64 ///
65 /// Converts to [`Reason::InternalSdkError`].
66 InternalError,
67 /// Used for a network error.
68 ///
69 /// This reason should be used, for example, if a connection timeout or DNS error prevents the
70 /// envelope from being sent.
71 ///
72 /// Converts to [`Reason::NetworkError`].
73 NetworkError,
74 /// Used when the SDK is backing off due to a rate limit.
75 ///
76 /// Converts to [`Reason::RatelimitBackoff`]
77 RatelimitBackoff,
78 /// Used when the transport queue overflows.
79 ///
80 /// Converts to [`Reason::QueueOverflow`]
81 QueueOverflow,
82}
83
84impl Recorder {
85 /// Record an envelope item lost for a given reason.
86 pub fn record_lost_data<L: LossSource>(&self, data: &L, reason: TransportLossReason) {
87 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
88 if let Some(aggregator) = self.aggregator() {
89 aggregator.record_lost_data(data, reason.into_reason());
90 }
91 #[cfg(not(all(target_has_atomic = "8", target_has_atomic = "64")))]
92 let _ = (data, reason);
93 }
94
95 /// Creates a new no-op [`Recorder`].
96 ///
97 /// This is used in backwards-compatibility code to handle the case where we might not have an
98 /// aggregator.
99 ///
100 /// To get a useful [`Recorder`], use [`ClientReportAggregator::recorder`].
101 pub(crate) fn new_no_op() -> Self {
102 Self {
103 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
104 inner: Weak::new(),
105 }
106 }
107
108 /// Create a new [`Recorder`] which records into the given
109 /// [`ClientReportAggregator`].
110 pub(super) fn new(aggregator: &ClientReportAggregator) -> Self {
111 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
112 {
113 let ClientReportAggregator {
114 inner: aggregator_inner,
115 } = aggregator;
116
117 let inner = Arc::downgrade(aggregator_inner);
118 Self { inner }
119 }
120 #[cfg(not(all(target_has_atomic = "8", target_has_atomic = "64")))]
121 {
122 let _ = aggregator;
123 Self {}
124 }
125 }
126
127 /// Helper to obtain the [`ClientReportAggregator`] we record into, if still alive.
128 ///
129 /// This works by upgrading the [`Weak`] pointer to the [`ClientReportAggregatorInner`] stored
130 /// in `self.inner`, then wrapping it in a [`ClientReportAggregator`].
131 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
132 fn aggregator(&self) -> Option<ClientReportAggregator> {
133 self.inner
134 .upgrade()
135 .map(|inner| ClientReportAggregator { inner })
136 }
137}
138
139impl TransportLossReason {
140 /// Convert to the corresponding [`Reason`].
141 #[cfg(all(target_has_atomic = "8", target_has_atomic = "64"))]
142 fn into_reason(self) -> Reason {
143 match self {
144 Self::SendError => Reason::SendError,
145 Self::InternalError => Reason::InternalSdkError,
146 Self::NetworkError => Reason::NetworkError,
147 Self::RatelimitBackoff => Reason::RatelimitBackoff,
148 Self::QueueOverflow => Reason::QueueOverflow,
149 }
150 }
151}