Skip to content

Commit 40487b7

Browse files
authored
fix: add ReportableError::reportable_source (#500)
* fix: add ReportableError::reportable_source for event_from_error to workaround its issues w/ downcast_ref SYNC-3981
1 parent eb53bec commit 40487b7

6 files changed

Lines changed: 93 additions & 20 deletions

File tree

autoconnect/autoconnect-ws/autoconnect-ws-sm/src/error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,18 @@ pub enum SMErrorKind {
103103
#[error("Client sent too many pings too often")]
104104
ExcessivePing,
105105
}
106+
107+
#[cfg(debug_assertions)]
108+
/// Return a [SMErrorKind::Reqwest] [SMError] for tests
109+
pub async fn __test_sm_reqwest_error() -> SMError {
110+
// An easily constructed reqwest::Error
111+
let e = reqwest::Client::builder()
112+
.https_only(true)
113+
.build()
114+
.unwrap()
115+
.get("http://example.com")
116+
.send()
117+
.await
118+
.unwrap_err();
119+
SMErrorKind::Reqwest(e).into()
120+
}

autoconnect/autoconnect-ws/autoconnect-ws-sm/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ mod unidentified;
88
pub use error::SMError;
99
pub use identified::WebPushClient;
1010
pub use unidentified::UnidentifiedClient;
11+
12+
#[cfg(debug_assertions)]
13+
pub use error::__test_sm_reqwest_error;

autoconnect/autoconnect-ws/src/error.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,17 @@ impl WSError {
7272
}
7373

7474
impl ReportableError for WSError {
75-
fn backtrace(&self) -> Option<&Backtrace> {
76-
// XXX: dumb hack: return SMError's backtrace for now as our
77-
// sentry::event_from_error doesn't capture it
75+
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
7876
match &self.kind {
79-
WSErrorKind::SM(e) => e.backtrace(),
80-
_ => self.backtrace.as_ref(),
77+
WSErrorKind::SM(e) => Some(e),
78+
_ => None,
8179
}
8280
}
8381

82+
fn backtrace(&self) -> Option<&Backtrace> {
83+
self.backtrace.as_ref()
84+
}
85+
8486
fn is_sentry_event(&self) -> bool {
8587
self.kind.is_sentry_event()
8688
}
@@ -147,3 +149,31 @@ impl WSErrorKind {
147149
false
148150
}
149151
}
152+
153+
#[cfg(test)]
154+
mod tests {
155+
use autoconnect_ws_sm::__test_sm_reqwest_error;
156+
use autopush_common::sentry::event_from_error;
157+
158+
use super::{WSError, WSErrorKind};
159+
160+
#[actix_web::test]
161+
async fn sentry_event() {
162+
// A chain of errors: SMError -> WSError -> reqwest::Error -> BadScheme
163+
let e: WSError = WSErrorKind::SM(__test_sm_reqwest_error().await).into();
164+
let event = event_from_error(&e);
165+
assert_eq!(event.exception.len(), 4);
166+
167+
// Source of the reqwest::Error (BadScheme)
168+
assert_eq!(event.exception[0].stacktrace, None);
169+
// reqwest::Error
170+
assert_eq!(event.exception[1].ty, "reqwest::Error");
171+
assert_eq!(event.exception[1].stacktrace, None);
172+
// SMError w/ ReportableError::backtrace
173+
assert_eq!(event.exception[2].ty, "SMError");
174+
assert!(event.exception[2].stacktrace.is_some());
175+
// WSError
176+
assert_eq!(event.exception[3].ty, "WSError");
177+
assert_eq!(event.exception[3].stacktrace, None);
178+
}
179+
}

autoendpoint/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ impl Serialize for ApiError {
332332
}
333333

334334
impl ReportableError for ApiError {
335+
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
336+
match &self.kind {
337+
ApiErrorKind::EndpointUrl(e) => Some(e),
338+
_ => None,
339+
}
340+
}
341+
335342
fn backtrace(&self) -> Option<&Backtrace> {
336343
Some(&self.backtrace)
337344
}

autopush-common/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@ impl ApcErrorKind {
181181

182182
/// Interface for reporting our Error types to Sentry or as metrics
183183
pub trait ReportableError: std::error::Error {
184+
/// Like [Error::source] but returns the source (if any) of this error as a
185+
/// [ReportableError] if it implements the trait. Otherwise callers of this
186+
/// method will likely subsequently call [Error::source] to return the
187+
/// source (if any) as the parent [Error] trait.
188+
fn reportable_source(&self) -> Option<&(dyn ReportableError + 'static)> {
189+
None
190+
}
191+
184192
/// Return a `Backtrace` for this Error if one was captured
185193
fn backtrace(&self) -> Option<&Backtrace>;
186194

autopush-common/src/sentry.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,30 @@ pub fn client_options() -> sentry::ClientOptions {
1717
///
1818
/// `std::error::Error` doesn't support backtraces, thus `sentry::event_from_error`
1919
/// doesn't either. This function works against `ReportableError` instead to
20-
/// access its backtrace.
21-
pub fn event_from_error<E>(err: &E) -> sentry::protocol::Event<'static>
22-
where
23-
E: ReportableError + 'static,
24-
{
25-
let mut exceptions = vec![exception_from_error_with_backtrace(err)];
20+
/// extract backtraces from it and its chain of `reportable_source's.
21+
///
22+
/// A caveat of this function is that it cannot extract
23+
/// `ReportableError`s/backtraces that occur in a chain after a
24+
/// `std::error::Error` occurs: as `std::error::Error::source` only allows
25+
/// downcasting to a concrete type, not `dyn ReportableError`.
26+
pub fn event_from_error(
27+
mut reportable_err: &dyn ReportableError,
28+
) -> sentry::protocol::Event<'static> {
29+
let mut exceptions = vec![];
2630

27-
let mut source = err.source();
28-
while let Some(err) = source {
29-
let exception = if let Some(err) = err.downcast_ref::<E>() {
30-
exception_from_error_with_backtrace(err)
31-
} else {
32-
exception_from_error(err)
31+
// Gather reportable_source()'s for their backtraces
32+
loop {
33+
exceptions.push(exception_from_reportable_error(reportable_err));
34+
reportable_err = match reportable_err.reportable_source() {
35+
Some(reportable_err) => reportable_err,
36+
None => break,
3337
};
34-
exceptions.push(exception);
38+
}
39+
40+
// Then fallback to source() for remaining Errors
41+
let mut source = reportable_err.source();
42+
while let Some(err) = source {
43+
exceptions.push(exception_from_error(err));
3544
source = err.source();
3645
}
3746

@@ -45,8 +54,9 @@ where
4554

4655
/// Custom `exception_from_error` support function for `ReportableError`
4756
///
48-
/// Based moreso on sentry_failure's `exception_from_single_fail`.
49-
fn exception_from_error_with_backtrace(err: &dyn ReportableError) -> sentry::protocol::Exception {
57+
/// Based moreso on sentry_failure's `exception_from_single_fail`. Includes a
58+
/// stacktrace if available.
59+
fn exception_from_reportable_error(err: &dyn ReportableError) -> sentry::protocol::Exception {
5060
let mut exception = exception_from_error(err);
5161
if let Some(backtrace) = err.backtrace() {
5262
exception.stacktrace = sentry_backtrace::backtrace_to_stacktrace(backtrace)

0 commit comments

Comments
 (0)