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