autopush_common/middleware/
sentry.rs

1use std::{cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc};
2
3use actix_web::{
4    dev::{Service, ServiceRequest, ServiceResponse, Transform},
5    Error, HttpMessage,
6};
7use cadence::{CountedExt, StatsdClient};
8use futures::{future::LocalBoxFuture, FutureExt};
9use futures_util::future::{ok, Ready};
10use sentry::{protocol::Event, Hub};
11
12use crate::{errors::ReportableError, tags::Tags};
13
14#[derive(Clone)]
15pub struct SentryWrapper<E> {
16    metrics: Arc<StatsdClient>,
17    metric_label: String,
18    phantom: PhantomData<E>,
19}
20
21impl<E> SentryWrapper<E> {
22    pub fn new(metrics: Arc<StatsdClient>, metric_label: String) -> Self {
23        Self {
24            metrics,
25            metric_label,
26            phantom: PhantomData,
27        }
28    }
29}
30
31impl<S, B, E> Transform<S, ServiceRequest> for SentryWrapper<E>
32where
33    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
34    S::Future: 'static,
35    E: ReportableError + actix_web::ResponseError + 'static,
36{
37    type Response = ServiceResponse<B>;
38    type Error = Error;
39    type Transform = SentryWrapperMiddleware<S, E>;
40    type InitError = ();
41    type Future = Ready<Result<Self::Transform, Self::InitError>>;
42
43    fn new_transform(&self, service: S) -> Self::Future {
44        ok(SentryWrapperMiddleware {
45            service: Rc::new(RefCell::new(service)),
46            metrics: self.metrics.clone(),
47            metric_label: self.metric_label.clone(),
48            phantom: PhantomData,
49        })
50    }
51}
52
53#[derive(Debug)]
54pub struct SentryWrapperMiddleware<S, E> {
55    service: Rc<RefCell<S>>,
56    metrics: Arc<StatsdClient>,
57    metric_label: String,
58    phantom: PhantomData<E>,
59}
60
61impl<S, B, E> Service<ServiceRequest> for SentryWrapperMiddleware<S, E>
62where
63    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
64    S::Future: 'static,
65    E: ReportableError + actix_web::ResponseError + 'static,
66{
67    type Response = ServiceResponse<B>;
68    type Error = Error;
69    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
70
71    actix_web::dev::forward_ready!(service);
72
73    fn call(&self, sreq: ServiceRequest) -> Self::Future {
74        // Set up the hub to add request data to events
75        let hub = Hub::new_from_top(Hub::main());
76        let _ = hub.push_scope();
77        let sentry_request = sentry_request_from_http(&sreq);
78        hub.configure_scope(|scope| {
79            scope.add_event_processor(Box::new(move |event| process_event(event, &sentry_request)))
80        });
81
82        // get the tag information
83        let mut tags = Tags::from_request_head(sreq.head());
84        let metrics = self.metrics.clone();
85        let metric_label = self.metric_label.clone();
86        if let Some(rtags) = sreq.request().extensions().get::<Tags>() {
87            trace!("Sentry: found tags in request: {:?}", &rtags.tags);
88            for (k, v) in rtags.tags.clone() {
89                tags.tags.insert(k, v);
90            }
91        };
92        sreq.extensions_mut().insert(tags.clone());
93
94        let fut = self.service.call(sreq);
95
96        async move {
97            let response: Self::Response = match fut.await {
98                Ok(response) => response,
99                Err(error) => {
100                    if let Some(reportable_err) = error.as_error::<E>() {
101                        // if it's not reportable, and we have access to the metrics, record it as a metric.
102                        if !reportable_err.is_sentry_event() {
103                            // The error (e.g. VapidErrorKind::InvalidKey(String)) might be too cardinal,
104                            // but we may need that information to debug a production issue. We can
105                            // add an info here, temporarily turn on info level debugging on a given server,
106                            // capture it, and then turn it off before we run out of money.
107                            maybe_emit_metrics(&metrics, &metric_label, reportable_err);
108                            debug!("Sentry: Not reporting error (service error): {:?}", error);
109                            return Err(error);
110                        }
111                    };
112                    debug!("Reporting error to Sentry (service error): {}", error);
113                    let event = event_from_actix_error::<E>(&error);
114                    let event_id = hub.capture_event(event);
115                    trace!("event_id = {}", event_id);
116                    return Err(error);
117                }
118            };
119            // Check for errors inside the response
120            if let Some(error) = response.response().error() {
121                if let Some(reportable_err) = error.as_error::<E>() {
122                    if !reportable_err.is_sentry_event() {
123                        maybe_emit_metrics(&metrics, &metric_label, reportable_err);
124                        debug!("Not reporting error (service error): {:?}", error);
125                        return Ok(response);
126                    }
127                }
128                debug!("Reporting error to Sentry (response error): {}", error);
129                let event = event_from_actix_error::<E>(error);
130                let event_id = hub.capture_event(event);
131                trace!("event_id = {}", event_id);
132            }
133            Ok(response)
134        }
135        .boxed_local()
136    }
137}
138
139/// Emit metrics when a [ReportableError::metric_label] is returned
140fn maybe_emit_metrics<E>(metrics: &StatsdClient, label_prefix: &str, err: &E)
141where
142    E: ReportableError,
143{
144    let Some(label) = err.metric_label() else {
145        return;
146    };
147    debug!("Sending error to metrics: {:?}", err);
148    let label = format!("{label_prefix}.{label}");
149    let mut builder = metrics.incr_with_tags(&label);
150    let tags = err.tags();
151    for (key, val) in &tags {
152        builder = builder.with_tag(key, val);
153    }
154    builder.send();
155}
156
157/// Build a Sentry request struct from the HTTP request
158fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Request {
159    sentry::protocol::Request {
160        url: format!(
161            "{}://{}{}",
162            request.connection_info().scheme(),
163            request.connection_info().host(),
164            request.uri()
165        )
166        .parse()
167        .ok(),
168        method: Some(request.method().to_string()),
169        headers: request
170            .headers()
171            .iter()
172            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
173            .collect(),
174        ..Default::default()
175    }
176}
177
178/// Add request data to a Sentry event
179#[allow(clippy::unnecessary_wraps)]
180fn process_event(
181    mut event: Event<'static>,
182    request: &sentry::protocol::Request,
183) -> Option<Event<'static>> {
184    if event.request.is_none() {
185        event.request = Some(request.clone());
186    }
187
188    // TODO: Use ServiceRequest::match_pattern for the event transaction.
189    //       Coming in Actix v3.
190
191    Some(event)
192}
193
194/// Convert Actix errors into a Sentry event. ReportableError is handled
195/// explicitly so the event can include a backtrace and source error
196/// information.
197fn event_from_actix_error<E>(error: &actix_web::Error) -> sentry::protocol::Event<'static>
198where
199    E: ReportableError + actix_web::ResponseError + 'static,
200{
201    // Actix errors don't have support source/cause, so to get more information
202    // about the error we need to downcast.
203    if let Some(reportable_err) = error.as_error::<E>() {
204        // Use our error and associated backtrace for the event
205        crate::sentry::event_from_error(reportable_err)
206    } else {
207        // Fallback to the Actix error
208        sentry::event_from_error(error)
209    }
210}