1#![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
93pub struct SentryBuilder {
95 middleware: Sentry,
96}
97
98impl SentryBuilder {
99 pub fn finish(self) -> Sentry {
101 self.middleware
102 }
103
104 #[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 #[must_use]
113 pub fn with_hub(mut self, hub: Arc<Hub>) -> Self {
114 self.middleware.hub = Some(hub);
115 self
116 }
117
118 #[must_use]
120 pub fn with_default_hub(mut self) -> Self {
121 self.middleware.hub = None;
122 self
123 }
124
125 #[must_use]
127 pub fn emit_header(mut self, val: bool) -> Self {
128 self.middleware.emit_header = val;
129 self
130 }
131
132 #[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#[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 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 pub fn with_transaction() -> Sentry {
164 Sentry {
165 start_transaction: true,
166 ..Sentry::default()
167 }
168 }
169
170 pub fn builder() -> SentryBuilder {
172 Sentry::new().into_builder()
173 }
174
175 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
206pub 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
244async 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 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 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 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
415fn 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
421fn 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 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
454fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> {
456 if event.request.is_none() {
458 event.request = Some(request.clone());
459 }
460
461 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 #[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 _assert_hub_no_events();
509
510 sentry::capture_message("Message", Level::Warning);
511
512 _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 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 #[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 _assert_hub_no_events();
552
553 sentry::capture_message("Message", Level::Warning);
554
555 _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 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 #[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 _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 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())); 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 #[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 #[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 #[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 #[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 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())); 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 #[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 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 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 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 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}