use std::future::Future;
use std::sync::Arc;
use sentry::{Hub, SentryFuture, SentryFutureExt};
use tokio::task::JoinHandle;
pub fn spawn_blocking<F, R>(f: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let hub = inherited_hub();
tokio::task::spawn_blocking(move || Hub::run(hub, f))
}
pub fn inherit_hub<F: Future>(future: F) -> SentryFuture<F> {
future.bind_hub(inherited_hub())
}
fn inherited_hub() -> Arc<Hub> {
Arc::new(Hub::new_from_top(Hub::current()))
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::body::to_bytes;
use actix_web::test::{TestRequest, call_service, init_service};
use actix_web::{App, HttpResponse, web};
use liboxen::error::OxenError;
use sentry::protocol::Event;
use sentry::test::TestTransport;
use sentry::{ClientOptions, Level, capture_message};
use crate::helpers::stream_with_heartbeat;
const ROUTE_PATTERN: &str = "/api/repos/{namespace}/{repo_name}";
const REQUEST_URI: &str = "/api/repos/ox/Cat-Dog-Classifier";
const ROUTE: &str = "GET /api/repos/{namespace}/{repo_name}";
async fn events_from_request<F, Fut>(handler: F) -> Vec<Event<'static>>
where
F: Fn() -> Fut + Clone + 'static,
Fut: Future<Output = HttpResponse> + 'static,
{
let transport = TestTransport::new();
let options = ClientOptions {
dsn: Some(
"https://public@sentry.invalid/1"
.parse()
.expect("the test DSN should parse"),
),
transport: Some(Arc::new(Arc::clone(&transport))),
..Default::default()
};
let hub = Arc::new(Hub::new(
Some(Arc::new(options.into())),
Arc::new(Default::default()),
));
let app = init_service(
App::new()
.route(ROUTE_PATTERN, web::get().to(handler))
.wrap(
sentry_actix::Sentry::builder()
.capture_server_errors(false)
.with_hub(hub)
.finish(),
),
)
.await;
let response = call_service(&app, TestRequest::get().uri(REQUEST_URI).to_request()).await;
assert!(response.status().is_success());
to_bytes(response.into_body())
.await
.expect("the response body should read to the end");
transport.fetch_and_clear_events()
}
#[actix_web::test]
async fn a_task_the_handler_awaits_reports_under_the_route() {
let events = events_from_request(|| async {
spawn_blocking(|| capture_message("from the blocking pool", Level::Error))
.await
.expect("the blocking task should not have panicked");
HttpResponse::Ok().finish()
})
.await;
assert_eq!(events.len(), 1);
assert_eq!(events[0].transaction.as_deref(), Some(ROUTE));
let url = events[0]
.request
.as_ref()
.and_then(|request| request.url.as_ref())
.map(ToString::to_string);
let expected_url = format!("http://localhost:8080{REQUEST_URI}");
assert_eq!(url.as_deref(), Some(expected_url.as_str()));
}
#[actix_web::test]
async fn a_task_deferred_behind_a_streaming_body_reports_under_the_route() {
let events = events_from_request(|| async {
stream_with_heartbeat(async {
spawn_blocking(|| capture_message("from the streamed body", Level::Error))
.await
.expect("the blocking task should not have panicked");
Ok::<_, OxenError>(())
})
})
.await;
assert_eq!(events.len(), 1);
assert_eq!(events[0].transaction.as_deref(), Some(ROUTE));
}
#[actix_web::test]
async fn a_bare_spawn_loses_the_route() {
let events = events_from_request(|| async {
tokio::task::spawn_blocking(|| capture_message("from the blocking pool", Level::Error))
.await
.expect("the blocking task should not have panicked");
HttpResponse::Ok().finish()
})
.await;
assert!(events.iter().all(|event| event.transaction.is_none()));
}
}