Skip to main content

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}