Skip to main content

sentry_actix/
lib.rs

1//! This crate adds a middleware for [`actix-web`](https://actix.rs/) that captures errors and
2//! report them to `Sentry`.
3//!
4//! To use this middleware just configure Sentry and then add it to your actix web app as a
5//! middleware.  Because actix is generally working with non sendable objects and highly concurrent
6//! this middleware creates a new Hub per request.
7//!
8//! # Example
9//!
10//! ```no_run
11//! use std::io;
12//!
13//! use actix_web::{get, App, Error, HttpRequest, HttpServer};
14//!
15//! #[get("/")]
16//! async fn failing(_req: HttpRequest) -> Result<String, Error> {
17//!     Err(io::Error::new(io::ErrorKind::Other, "An error happens here").into())
18//! }
19//!
20//! fn main() -> io::Result<()> {
21//!     let _guard = sentry::init(
22//!         sentry::ClientOptions::new().maybe_release(sentry::release_name!()),
23//!     );
24//!     std::env::set_var("RUST_BACKTRACE", "1");
25//!
26//!     let runtime = tokio::runtime::Builder::new_multi_thread()
27//!         .enable_all()
28//!         .build()?;
29//!     runtime.block_on(async move {
30//!         HttpServer::new(|| {
31//!             App::new()
32//!                 .wrap(sentry_actix::Sentry::new())
33//!                 .service(failing)
34//!         })
35//!         .bind("127.0.0.1:3001")?
36//!         .run()
37//!         .await
38//!     })
39//! }
40//! ```
41//!
42//! # Using Release Health
43//!
44//! The actix middleware will automatically start a new session for each request
45//! when `auto_session_tracking` is enabled and the client is configured to
46//! use `SessionMode::Request`.
47//!
48//! ```
49//! let _sentry = sentry::init(
50//!     sentry::ClientOptions::new()
51//!         .maybe_release(sentry::release_name!())
52//!         .session_mode(sentry::SessionMode::Request)
53//!         .auto_session_tracking(true),
54//! );
55//! ```
56//!
57//! # Reusing the Hub
58//!
59//! This integration will automatically create a new per-request Hub from the main Hub, and update the
60//! current Hub instance. For example, the following in the handler or in any of the subsequent
61//! middleware will capture a message in the current request's Hub:
62//!
63//! ```
64//! sentry::capture_message("Something is not well", sentry::Level::Warning);
65//! ```
66//!
67//! It is recommended to register the Sentry middleware as the last, i.e. the first to be executed
68//! when processing a request, so that the rest of the processing will run with the correct Hub.
69
70#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
71#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
72#![allow(deprecated)]
73#![allow(clippy::type_complexity)]
74
75use std::borrow::Cow;
76use std::pin::Pin;
77use std::rc::Rc;
78use std::sync::Arc;
79
80use actix_http::header::{self, HeaderMap};
81use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
82use actix_web::http::StatusCode;
83use actix_web::Error;
84use bytes::{Bytes, BytesMut};
85use futures_util::future::{ok, Future, Ready};
86use futures_util::{FutureExt as _, TryStreamExt as _};
87
88use sentry_core::protocol::{self, ClientSdkPackage, Event, Request};
89use sentry_core::utils::{is_sensitive_header, scrub_pii_from_url};
90use sentry_core::MaxRequestBodySize;
91use sentry_core::{Hub, SentryFutureExt};
92
93/// A helper construct that can be used to reconfigure and build the middleware.
94pub struct SentryBuilder {
95    middleware: Sentry,
96}
97
98impl SentryBuilder {
99    /// Finishes the building and returns a middleware
100    pub fn finish(self) -> Sentry {
101        self.middleware
102    }
103
104    /// Tells the middleware to start a new performance monitoring transaction for each request.
105    #[must_use]
106    pub fn start_transaction(mut self, start_transaction: bool) -> Self {
107        self.middleware.start_transaction = start_transaction;
108        self
109    }
110
111    /// Reconfigures the middleware so that it uses a specific hub instead of the default one.
112    #[must_use]
113    pub fn with_hub(mut self, hub: Arc<Hub>) -> Self {
114        self.middleware.hub = Some(hub);
115        self
116    }
117
118    /// Reconfigures the middleware so that it uses a specific hub instead of the default one.
119    #[must_use]
120    pub fn with_default_hub(mut self) -> Self {
121        self.middleware.hub = None;
122        self
123    }
124
125    /// If configured the sentry id is attached to a X-Sentry-Event header.
126    #[must_use]
127    pub fn emit_header(mut self, val: bool) -> Self {
128        self.middleware.emit_header = val;
129        self
130    }
131
132    /// Enables or disables error reporting.
133    ///
134    /// The default is to report all errors.
135    #[must_use]
136    pub fn capture_server_errors(mut self, val: bool) -> Self {
137        self.middleware.capture_server_errors = val;
138        self
139    }
140}
141
142/// Reports certain failures to Sentry.
143#[derive(Clone)]
144pub struct Sentry {
145    hub: Option<Arc<Hub>>,
146    emit_header: bool,
147    capture_server_errors: bool,
148    start_transaction: bool,
149}
150
151impl Sentry {
152    /// Creates a new sentry middleware.
153    pub fn new() -> Self {
154        Sentry {
155            hub: None,
156            emit_header: false,
157            capture_server_errors: true,
158            start_transaction: false,
159        }
160    }
161
162    /// Creates a new sentry middleware which starts a new performance monitoring transaction for each request.
163    pub fn with_transaction() -> Sentry {
164        Sentry {
165            start_transaction: true,
166            ..Sentry::default()
167        }
168    }
169
170    /// Creates a new middleware builder.
171    pub fn builder() -> SentryBuilder {
172        Sentry::new().into_builder()
173    }
174
175    /// Converts the middleware into a builder.
176    pub fn into_builder(self) -> SentryBuilder {
177        SentryBuilder { middleware: self }
178    }
179}
180
181impl Default for Sentry {
182    fn default() -> Self {
183        Sentry::new()
184    }
185}
186
187impl<S, B> Transform<S, ServiceRequest> for Sentry
188where
189    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
190    S::Future: 'static,
191{
192    type Response = ServiceResponse<B>;
193    type Error = Error;
194    type Transform = SentryMiddleware<S>;
195    type InitError = ();
196    type Future = Ready<Result<Self::Transform, Self::InitError>>;
197
198    fn new_transform(&self, service: S) -> Self::Future {
199        ok(SentryMiddleware {
200            service: Rc::new(service),
201            inner: self.clone(),
202        })
203    }
204}
205
206/// The middleware for individual services.
207pub struct SentryMiddleware<S> {
208    service: Rc<S>,
209    inner: Sentry,
210}
211
212fn should_capture_request_body(
213    headers: &HeaderMap,
214    with_pii: bool,
215    max_request_body_size: MaxRequestBodySize,
216) -> bool {
217    let is_chunked = headers
218        .get(header::TRANSFER_ENCODING)
219        .and_then(|h| h.to_str().ok())
220        .map(|transfer_encoding| transfer_encoding.contains("chunked"))
221        .unwrap_or(false);
222
223    let is_valid_content_type = with_pii
224        || headers
225            .get(header::CONTENT_TYPE)
226            .and_then(|h| h.to_str().ok())
227            .is_some_and(|content_type| {
228                matches!(
229                    content_type,
230                    "application/json" | "application/x-www-form-urlencoded"
231                )
232            });
233
234    let is_within_size_limit = headers
235        .get(header::CONTENT_LENGTH)
236        .and_then(|h| h.to_str().ok())
237        .and_then(|content_length| content_length.parse::<usize>().ok())
238        .map(|content_length| max_request_body_size.is_within_size_limit(content_length))
239        .unwrap_or(false);
240
241    !is_chunked && is_valid_content_type && is_within_size_limit
242}
243
244/// Extract a body from the HTTP request
245async fn body_from_http(req: &mut ServiceRequest) -> actix_web::Result<Bytes> {
246    let stream = req.extract::<actix_web::web::Payload>().await?;
247    let body = stream.try_collect::<BytesMut>().await?.freeze();
248
249    // put copy of payload back into request for downstream to read
250    req.set_payload(actix_web::dev::Payload::from(body.clone()));
251
252    Ok(body)
253}
254
255async fn capture_request_body(req: &mut ServiceRequest) -> String {
256    match body_from_http(req).await {
257        Ok(request_body) => String::from_utf8_lossy(&request_body).into_owned(),
258        Err(_) => String::new(),
259    }
260}
261
262impl<S, B> Service<ServiceRequest> for SentryMiddleware<S>
263where
264    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
265    S::Future: 'static,
266{
267    type Response = ServiceResponse<B>;
268    type Error = Error;
269    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
270
271    fn poll_ready(
272        &self,
273        cx: &mut std::task::Context<'_>,
274    ) -> std::task::Poll<Result<(), Self::Error>> {
275        self.service.poll_ready(cx)
276    }
277
278    fn call(&self, req: ServiceRequest) -> Self::Future {
279        let inner = self.inner.clone();
280        let hub = Arc::new(Hub::new_from_top(
281            inner.hub.clone().unwrap_or_else(Hub::main),
282        ));
283
284        let client = hub.client();
285
286        let max_request_body_size = client
287            .as_ref()
288            .map(|client| client.options().max_request_body_size)
289            .unwrap_or(MaxRequestBodySize::None);
290
291        #[cfg(feature = "release-health")]
292        {
293            let track_sessions = client.as_ref().is_some_and(|client| {
294                let options = client.options();
295                options.auto_session_tracking
296                    && options.session_mode == sentry_core::SessionMode::Request
297            });
298            if track_sessions {
299                hub.start_session();
300            }
301        }
302
303        let with_pii = client
304            .as_ref()
305            .is_some_and(|client| client.options().send_default_pii);
306
307        let mut sentry_req = sentry_request_from_http(&req, with_pii);
308        let name = transaction_name_from_http(&req);
309
310        let transaction = if inner.start_transaction {
311            let headers = req.headers().iter().flat_map(|(header, value)| {
312                value.to_str().ok().map(|value| (header.as_str(), value))
313            });
314
315            let ctx = sentry_core::TransactionContext::continue_from_headers(
316                &name,
317                "http.server",
318                headers,
319            );
320
321            let transaction = hub.start_transaction(ctx);
322            transaction.set_request(sentry_req.clone());
323            transaction.set_origin("auto.http.actix");
324            Some(transaction)
325        } else {
326            None
327        };
328
329        let svc = self.service.clone();
330        async move {
331            let mut req = req;
332
333            if should_capture_request_body(req.headers(), with_pii, max_request_body_size) {
334                sentry_req.data = Some(capture_request_body(&mut req).await);
335            }
336
337            let parent_span = hub.configure_scope(|scope| {
338                let parent_span = scope.get_span();
339                if let Some(transaction) = transaction.as_ref() {
340                    scope.set_span(Some(transaction.clone().into()));
341                } else {
342                    scope.set_transaction((!inner.start_transaction).then_some(&name));
343                }
344                scope.add_event_processor(move |event| Some(process_event(event, &sentry_req)));
345                parent_span
346            });
347
348            let fut = Hub::run(hub.clone(), || svc.call(req)).bind_hub(hub.clone());
349            let mut res: Self::Response = match fut.await {
350                Ok(res) => res,
351                Err(e) => {
352                    // Errors returned by middleware, and possibly other lower level errors
353                    if inner.capture_server_errors && e.error_response().status().is_server_error()
354                    {
355                        hub.capture_error(&e);
356                    }
357
358                    if let Some(transaction) = transaction {
359                        if transaction.get_status().is_none() {
360                            let status = protocol::SpanStatus::UnknownError;
361                            transaction.set_status(status);
362                        }
363                        transaction.finish();
364                        hub.configure_scope(|scope| scope.set_span(parent_span));
365                    }
366                    return Err(e);
367                }
368            };
369
370            // Response errors
371            if inner.capture_server_errors && res.response().status().is_server_error() {
372                if let Some(e) = res.response().error() {
373                    let event_id = hub.capture_error(e);
374
375                    if inner.emit_header {
376                        res.response_mut().headers_mut().insert(
377                            "x-sentry-event".parse().unwrap(),
378                            event_id.simple().to_string().parse().unwrap(),
379                        );
380                    }
381                }
382            }
383
384            if let Some(transaction) = transaction {
385                if transaction.get_status().is_none() {
386                    let status = map_status(res.status());
387                    transaction.set_status(status);
388                }
389                transaction.finish();
390                hub.configure_scope(|scope| scope.set_span(parent_span));
391            }
392
393            Ok(res)
394        }
395        .boxed_local()
396    }
397}
398
399fn map_status(status: StatusCode) -> protocol::SpanStatus {
400    match status {
401        StatusCode::UNAUTHORIZED => protocol::SpanStatus::Unauthenticated,
402        StatusCode::FORBIDDEN => protocol::SpanStatus::PermissionDenied,
403        StatusCode::NOT_FOUND => protocol::SpanStatus::NotFound,
404        StatusCode::TOO_MANY_REQUESTS => protocol::SpanStatus::ResourceExhausted,
405        status if status.is_client_error() => protocol::SpanStatus::InvalidArgument,
406        StatusCode::NOT_IMPLEMENTED => protocol::SpanStatus::Unimplemented,
407        StatusCode::SERVICE_UNAVAILABLE => protocol::SpanStatus::Unavailable,
408        status if status.is_server_error() => protocol::SpanStatus::InternalError,
409        StatusCode::CONFLICT => protocol::SpanStatus::AlreadyExists,
410        status if status.is_success() => protocol::SpanStatus::Ok,
411        _ => protocol::SpanStatus::UnknownError,
412    }
413}
414
415/// Extract a transaction name from the HTTP request
416fn transaction_name_from_http(req: &ServiceRequest) -> String {
417    let path_part = req.match_pattern().unwrap_or_else(|| "<none>".to_string());
418    format!("{} {}", req.method(), path_part)
419}
420
421/// Build a Sentry request struct from the HTTP request
422fn sentry_request_from_http(request: &ServiceRequest, with_pii: bool) -> Request {
423    let mut sentry_req = Request {
424        url: format!(
425            "{}://{}{}",
426            request.connection_info().scheme(),
427            request.connection_info().host(),
428            request.uri()
429        )
430        .parse()
431        .ok()
432        .map(scrub_pii_from_url),
433        method: Some(request.method().to_string()),
434        headers: request
435            .headers()
436            .iter()
437            .filter(|(_, v)| !v.is_sensitive())
438            .filter(|(k, _)| with_pii || !is_sensitive_header(k.as_str()))
439            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
440            .collect(),
441        ..Default::default()
442    };
443
444    // If PII is enabled, include the remote address
445    if with_pii {
446        if let Some(remote) = request.connection_info().remote_addr() {
447            sentry_req.env.insert("REMOTE_ADDR".into(), remote.into());
448        }
449    };
450
451    sentry_req
452}
453
454/// Add request data to a Sentry event
455fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> {
456    // Request
457    if event.request.is_none() {
458        event.request = Some(request.clone());
459    }
460
461    // SDK
462    if let Some(sdk) = event.sdk.take() {
463        let mut sdk = sdk.into_owned();
464        sdk.packages.push(ClientSdkPackage {
465            name: "sentry-actix".into(),
466            version: env!("CARGO_PKG_VERSION").into(),
467        });
468        event.sdk = Some(Cow::Owned(sdk));
469    }
470    event
471}
472
473#[cfg(test)]
474mod tests {
475    use std::io;
476
477    use actix_web::body::BoxBody;
478    use actix_web::test::{call_service, init_service, TestRequest};
479    use actix_web::{get, web, App, HttpRequest, HttpResponse};
480    use futures::executor::block_on;
481
482    use futures::future::join_all;
483    use sentry::Level;
484    use sentry_core::protocol::{Context, EnvelopeItem, TraceContext};
485    use sentry_core::Envelope;
486
487    use super::*;
488
489    fn _assert_hub_no_events() {
490        if Hub::current().last_event_id().is_some() {
491            panic!("Current hub should not have had any events.");
492        }
493    }
494
495    fn _assert_hub_has_events() {
496        Hub::current()
497            .last_event_id()
498            .expect("Current hub should have had events.");
499    }
500
501    /// Test explicit events sent to the current Hub inside an Actix service.
502    #[actix_web::test]
503    async fn test_explicit_events() {
504        let events = sentry::test::with_captured_events(|| {
505            block_on(async {
506                let service = || {
507                    // Current Hub should have no events
508                    _assert_hub_no_events();
509
510                    sentry::capture_message("Message", Level::Warning);
511
512                    // Current Hub should have the event
513                    _assert_hub_has_events();
514
515                    HttpResponse::Ok()
516                };
517
518                let app = init_service(
519                    App::new()
520                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
521                        .service(web::resource("/test").to(service)),
522                )
523                .await;
524
525                // Call the service twice (sequentially) to ensure the middleware isn't sticky
526                for _ in 0..2 {
527                    let req = TestRequest::get().uri("/test").to_request();
528                    let res = call_service(&app, req).await;
529                    assert!(res.status().is_success());
530                }
531            })
532        });
533
534        assert_eq!(events.len(), 2);
535        for event in events {
536            let request = event.request.expect("Request should be set.");
537            assert_eq!(event.transaction, Some("GET /test".into()));
538            assert_eq!(event.message, Some("Message".into()));
539            assert_eq!(event.level, Level::Warning);
540            assert_eq!(request.method, Some("GET".into()));
541        }
542    }
543
544    /// Test transaction name HTTP verb.
545    #[actix_web::test]
546    async fn test_match_pattern() {
547        let events = sentry::test::with_captured_events(|| {
548            block_on(async {
549                let service = |_name: String| {
550                    // Current Hub should have no events
551                    _assert_hub_no_events();
552
553                    sentry::capture_message("Message", Level::Warning);
554
555                    // Current Hub should have the event
556                    _assert_hub_has_events();
557
558                    HttpResponse::Ok()
559                };
560
561                let app = init_service(
562                    App::new()
563                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
564                        .service(web::resource("/test/{name}").route(web::post().to(service))),
565                )
566                .await;
567
568                // Call the service twice (sequentially) to ensure the middleware isn't sticky
569                for _ in 0..2 {
570                    let req = TestRequest::post().uri("/test/fake_name").to_request();
571                    let res = call_service(&app, req).await;
572                    assert!(res.status().is_success());
573                }
574            })
575        });
576
577        assert_eq!(events.len(), 2);
578        for event in events {
579            let request = event.request.expect("Request should be set.");
580            assert_eq!(event.transaction, Some("POST /test/{name}".into()));
581            assert_eq!(event.message, Some("Message".into()));
582            assert_eq!(event.level, Level::Warning);
583            assert_eq!(request.method, Some("POST".into()));
584        }
585    }
586
587    /// Ensures errors returned in the Actix service trigger an event.
588    #[actix_web::test]
589    async fn test_response_errors() {
590        let events = sentry::test::with_captured_events(|| {
591            block_on(async {
592                #[get("/test")]
593                async fn failing(_req: HttpRequest) -> Result<String, Error> {
594                    // Current hub should have no events
595                    _assert_hub_no_events();
596
597                    Err(io::Error::other("Test Error").into())
598                }
599
600                let app = init_service(
601                    App::new()
602                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
603                        .service(failing),
604                )
605                .await;
606
607                // Call the service twice (sequentially) to ensure the middleware isn't sticky
608                for _ in 0..2 {
609                    let req = TestRequest::get().uri("/test").to_request();
610                    let res = call_service(&app, req).await;
611                    assert!(res.status().is_server_error());
612                }
613            })
614        });
615
616        assert_eq!(events.len(), 2);
617        for event in events {
618            let request = event.request.expect("Request should be set.");
619            assert_eq!(event.transaction, Some("GET /test".into())); // Transaction name is the matcher of the route
620            assert_eq!(event.message, None);
621            assert_eq!(event.exception.values[0].ty, String::from("Custom"));
622            assert_eq!(event.exception.values[0].value, Some("Test Error".into()));
623            assert_eq!(event.level, Level::Error);
624            assert_eq!(request.method, Some("GET".into()));
625        }
626    }
627
628    /// Ensures client errors (4xx) returned by service are not captured.
629    #[actix_web::test]
630    async fn test_service_client_errors_discarded() {
631        let events = sentry::test::with_captured_events(|| {
632            block_on(async {
633                let service = HttpResponse::NotFound;
634
635                let app = init_service(
636                    App::new()
637                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
638                        .service(web::resource("/test").to(service)),
639                )
640                .await;
641
642                let req = TestRequest::get().uri("/test").to_request();
643                let res = call_service(&app, req).await;
644                assert!(res.status().is_client_error());
645            })
646        });
647
648        assert!(events.is_empty());
649    }
650
651    /// Ensures client errors (4xx) returned by middleware are not captured.
652    #[actix_web::test]
653    async fn test_middleware_client_errors_discarded() {
654        let events = sentry::test::with_captured_events(|| {
655            block_on(async {
656                async fn hello_world() -> HttpResponse {
657                    HttpResponse::Ok().body("Hello, world!")
658                }
659
660                let app = init_service(
661                    App::new()
662                        .wrap_fn(|_, _| async {
663                            Err(actix_web::error::ErrorNotFound("Not found"))
664                                as Result<ServiceResponse<BoxBody>, _>
665                        })
666                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
667                        .service(web::resource("/test").to(hello_world)),
668                )
669                .await;
670
671                let req = TestRequest::get().uri("/test").to_request();
672                let res = app.call(req).await;
673                assert!(res.is_err());
674                assert!(res.unwrap_err().error_response().status().is_client_error());
675            })
676        });
677
678        assert!(events.is_empty());
679    }
680
681    /// Ensures server errors (5xx) returned by middleware are captured.
682    #[actix_web::test]
683    async fn test_middleware_server_errors_captured() {
684        let events = sentry::test::with_captured_events(|| {
685            block_on(async {
686                async fn hello_world() -> HttpResponse {
687                    HttpResponse::Ok().body("Hello, world!")
688                }
689
690                let app = init_service(
691                    App::new()
692                        .wrap_fn(|_, _| async {
693                            Err(actix_web::error::ErrorInternalServerError("Server error"))
694                                as Result<ServiceResponse<BoxBody>, _>
695                        })
696                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
697                        .service(web::resource("/test").to(hello_world)),
698                )
699                .await;
700
701                let req = TestRequest::get().uri("/test").to_request();
702                let res = app.call(req).await;
703                assert!(res.is_err());
704                assert!(res.unwrap_err().error_response().status().is_server_error());
705            })
706        });
707
708        assert_eq!(events.len(), 1);
709    }
710
711    fn trace_context_from_single_transaction(envelopes: &[Envelope]) -> TraceContext {
712        let [envelope] = envelopes else {
713            panic!("Expected exactly one envelope");
714        };
715
716        let mut items = envelope.items();
717        let Some(EnvelopeItem::Transaction(transaction)) = items.next() else {
718            panic!("Expected a transaction envelope item");
719        };
720        assert!(items.next().is_none(), "expected only one envelope item");
721
722        match transaction.contexts.get("trace") {
723            Some(Context::Trace(trace)) => *trace.clone(),
724            unexpected => panic!("expected trace context, got {unexpected:#?}"),
725        }
726    }
727
728    fn run_request_with_org_ids(incoming_org_id: &str, client_org_id: &str) -> Vec<Envelope> {
729        sentry::test::with_captured_envelopes_options(
730            || {
731                block_on(async {
732                    let app = init_service(
733                        App::new()
734                            .wrap(
735                                Sentry::builder()
736                                    .with_hub(Hub::current())
737                                    .start_transaction(true)
738                                    .finish(),
739                            )
740                            .service(web::resource("/test").to(HttpResponse::Ok)),
741                    )
742                    .await;
743
744                    let req = TestRequest::get()
745                        .uri("/test")
746                        .insert_header((
747                            "sentry-trace",
748                            "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
749                        ))
750                        .insert_header(("baggage", format!("sentry-org_id={incoming_org_id}")))
751                        .to_request();
752                    let res = call_service(&app, req).await;
753                    assert!(res.status().is_success());
754                })
755            },
756            sentry::ClientOptions::new()
757                .org_id(client_org_id.parse().unwrap())
758                .strict_trace_continuation(true)
759                .traces_sample_rate(1.0),
760        )
761    }
762
763    #[actix_web::test]
764    async fn test_transaction_continues_matching_org_id() {
765        let envelopes = run_request_with_org_ids("42", "42");
766        let trace = trace_context_from_single_transaction(&envelopes);
767        assert_eq!(
768            trace.trace_id.to_string(),
769            "09e04486820349518ac7b5d2adbf6ba5"
770        );
771        assert_eq!(
772            trace.parent_span_id.map(|span_id| span_id.to_string()),
773            Some("9cf635fa5b870b3a".to_owned())
774        );
775    }
776
777    #[actix_web::test]
778    async fn test_transaction_rejects_mismatched_org_id() {
779        let envelopes = run_request_with_org_ids("43", "42");
780        let trace = trace_context_from_single_transaction(&envelopes);
781        assert_ne!(
782            trace.trace_id.to_string(),
783            "09e04486820349518ac7b5d2adbf6ba5"
784        );
785        assert_eq!(trace.parent_span_id, None);
786    }
787
788    /// Ensures transaction name can be overridden in handler scope.
789    #[actix_web::test]
790    async fn test_override_transaction_name() {
791        let events = sentry::test::with_captured_events(|| {
792            block_on(async {
793                #[get("/test")]
794                async fn original_transaction(_req: HttpRequest) -> Result<String, Error> {
795                    // Override transaction name
796                    sentry::configure_scope(|scope| scope.set_transaction(Some("new_transaction")));
797                    Err(io::Error::other("Test Error").into())
798                }
799
800                let app = init_service(
801                    App::new()
802                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
803                        .service(original_transaction),
804                )
805                .await;
806
807                let req = TestRequest::get().uri("/test").to_request();
808                let res = call_service(&app, req).await;
809                assert!(res.status().is_server_error());
810            })
811        });
812
813        assert_eq!(events.len(), 1);
814        let event = events[0].clone();
815        let request = event.request.expect("Request should be set.");
816        assert_eq!(event.transaction, Some("new_transaction".into())); // Transaction name is overridden by handler
817        assert_eq!(event.message, None);
818        assert_eq!(event.exception.values[0].ty, String::from("Custom"));
819        assert_eq!(event.exception.values[0].value, Some("Test Error".into()));
820        assert_eq!(event.level, Level::Error);
821        assert_eq!(request.method, Some("GET".into()));
822    }
823
824    #[cfg(feature = "release-health")]
825    #[actix_web::test]
826    async fn test_track_session() {
827        let envelopes = sentry::test::with_captured_envelopes_options(
828            || {
829                block_on(async {
830                    #[get("/")]
831                    async fn hello() -> impl actix_web::Responder {
832                        String::from("Hello there!")
833                    }
834
835                    let middleware = Sentry::builder().with_hub(Hub::current()).finish();
836
837                    let app = init_service(App::new().wrap(middleware).service(hello)).await;
838
839                    for _ in 0..5 {
840                        let req = TestRequest::get().uri("/").to_request();
841                        call_service(&app, req).await;
842                    }
843                })
844            },
845            sentry::ClientOptions::new()
846                .release("some-release")
847                .session_mode(sentry::SessionMode::Request)
848                .auto_session_tracking(true),
849        );
850        assert_eq!(envelopes.len(), 1);
851
852        let mut items = envelopes[0].items();
853        if let Some(sentry::protocol::EnvelopeItem::SessionAggregates(aggregate)) = items.next() {
854            let aggregates = &aggregate.aggregates;
855
856            assert_eq!(aggregates[0].distinct_id, None);
857            assert_eq!(aggregates[0].exited, 5);
858        } else {
859            panic!("expected session");
860        }
861        assert_eq!(items.next(), None);
862    }
863
864    /// Tests that the per-request Hub is used in the handler and both sides of the roundtrip
865    /// through middleware
866    #[actix_web::test]
867    async fn test_middleware_and_handler_use_correct_hub() {
868        sentry::test::with_captured_events(|| {
869            block_on(async {
870                sentry::capture_message("message outside", Level::Error);
871
872                let handler = || {
873                    // an event was captured in the middleware
874                    assert!(Hub::current().last_event_id().is_some());
875                    sentry::capture_message("second message", Level::Error);
876                    HttpResponse::Ok()
877                };
878
879                let app = init_service(
880                    App::new()
881                        .wrap_fn(|req, srv| {
882                            // the event captured outside the per-request Hub is not there
883                            assert!(Hub::current().last_event_id().is_none());
884
885                            let event_id = sentry::capture_message("first message", Level::Error);
886
887                            srv.call(req).map(move |res| {
888                                // a different event was captured in the handler
889                                assert!(Hub::current().last_event_id().is_some());
890                                assert_ne!(Some(event_id), Hub::current().last_event_id());
891                                res
892                            })
893                        })
894                        .wrap(Sentry::builder().with_hub(Hub::current()).finish())
895                        .service(web::resource("/test").to(handler)),
896                )
897                .await;
898
899                // test with multiple requests in parallel
900                let mut futures = Vec::new();
901                for _ in 0..16 {
902                    let req = TestRequest::get().uri("/test").to_request();
903                    futures.push(call_service(&app, req));
904                }
905
906                join_all(futures).await;
907            })
908        });
909    }
910}