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#![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
94pub struct SentryBuilder {
96 middleware: Sentry,
97}
98
99impl SentryBuilder {
100 pub fn finish(self) -> Sentry {
102 self.middleware
103 }
104
105 #[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 #[must_use]
114 pub fn with_hub(mut self, hub: Arc<Hub>) -> Self {
115 self.middleware.hub = Some(hub);
116 self
117 }
118
119 #[must_use]
121 pub fn with_default_hub(mut self) -> Self {
122 self.middleware.hub = None;
123 self
124 }
125
126 #[must_use]
128 pub fn emit_header(mut self, val: bool) -> Self {
129 self.middleware.emit_header = val;
130 self
131 }
132
133 #[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#[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 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 pub fn with_transaction() -> Sentry {
165 Sentry {
166 start_transaction: true,
167 ..Sentry::default()
168 }
169 }
170
171 pub fn builder() -> SentryBuilder {
173 Sentry::new().into_builder()
174 }
175
176 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
207pub 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
245async 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 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 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 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
416fn 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
422fn 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 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
455fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> {
457 if event.request.is_none() {
459 event.request = Some(request.clone());
460 }
461
462 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 #[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 _assert_hub_no_events();
510
511 sentry::capture_message("Message", Level::Warning);
512
513 _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 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 #[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 _assert_hub_no_events();
553
554 sentry::capture_message("Message", Level::Warning);
555
556 _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 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 #[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 _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 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())); 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 #[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 #[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 #[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 #[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 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())); 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 #[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 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 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 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 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}