sentry_tunnel/
lib.rs

1mod config;
2mod error;
3mod handler;
4
5pub use config::SentryTunnelConfig;
6pub use error::SentryTunnelError;
7pub use handler::handle_sentry_tunnel_inner;
8
9#[cfg(feature = "extension")]
10use axum::{
11    body::Bytes,
12    extract::State,
13    http::StatusCode,
14    routing::post,
15    Router,
16};
17#[cfg(feature = "extension")]
18use std::sync::Arc;
19
20#[cfg(feature = "extension")]
21#[cfg_attr(feature = "utoipa", utoipa::path(
22    post,
23    path = "/tunnel",
24    request_body = Vec<u8>,
25    responses(
26        (status = 200, description = "Successfully tunneled to Sentry"),
27        (status = 400, description = "Bad request - invalid envelope or DSN"),
28        (status = 500, description = "Internal server error - failed to tunnel")
29    ),
30    tag = "sentry"
31))]
32/// Create Sentry tunnel route handler
33pub async fn sentry_tunnel_handler(
34    State(config): State<Arc<SentryTunnelConfig>>,
35    body: Bytes,
36) -> Result<StatusCode, SentryTunnelError> {
37    handler::handle_sentry_tunnel(config, body).await
38}
39
40#[cfg(feature = "extension")]
41/// Extension methods for Router
42pub trait SentryTunnelExt {
43    /// Add Sentry tunnel route to Router
44    fn sentry_tunnel(self, config: SentryTunnelConfig) -> Self;
45}
46
47#[cfg(feature = "extension")]
48impl SentryTunnelExt for Router {
49    fn sentry_tunnel(self, config: SentryTunnelConfig) -> Self {
50        let path = config.path.clone();
51        let config = Arc::new(config);
52
53        self.route(
54            &path,
55            post(sentry_tunnel_handler).with_state(config),
56        )
57    }
58}
59
60#[cfg(feature = "standalone")]
61/// Create a standalone Sentry tunnel service
62pub fn create_sentry_tunnel_service(config: SentryTunnelConfig) -> Router {
63    Router::new().sentry_tunnel(config)
64}
65
66#[cfg(feature = "utoipa")]
67/// Implementation of SentryTunnelExt for utoipa-axum OpenApiRouter
68impl<S> SentryTunnelExt for utoipa_axum::router::OpenApiRouter<S>
69where
70    S: Clone + Send + Sync + 'static,
71{
72    fn sentry_tunnel(self, config: SentryTunnelConfig) -> Self {
73        let path = config.path.clone();
74        let config_arc = Arc::new(config);
75
76        // Add the route using the standard router method
77        // The utoipa::path annotation is already applied to the handler
78        self.route(&path, post(sentry_tunnel_handler).with_state(config_arc))
79    }
80}
81
82/// Builder pattern for creating configuration
83pub struct SentryTunnelBuilder {
84    config: SentryTunnelConfig,
85}
86
87impl SentryTunnelBuilder {
88    /// Create a new builder
89    pub fn new(sentry_host: impl Into<String>) -> Self {
90        Self {
91            config: SentryTunnelConfig::new(sentry_host, vec![]),
92        }
93    }
94
95    /// Add an allowed project ID
96    pub fn allow_project_id(mut self, project_id: impl Into<String>) -> Self {
97        self.config.allowed_project_ids.push(project_id.into());
98        self
99    }
100
101    /// Add multiple allowed project IDs
102    pub fn allow_project_ids<I, S>(mut self, project_ids: I) -> Self
103    where
104        I: IntoIterator<Item = S>,
105        S: Into<String>,
106    {
107        self.config.allowed_project_ids.extend(
108            project_ids.into_iter().map(|s| s.into())
109        );
110        self
111    }
112
113    /// Set the path
114    pub fn path(mut self, path: impl Into<String>) -> Self {
115        self.config.path = path.into();
116        self
117    }
118
119    /// Set the timeout
120    pub fn timeout_secs(mut self, timeout: u64) -> Self {
121        self.config.timeout_secs = timeout;
122        self
123    }
124
125    /// Build the configuration
126    pub fn build(self) -> SentryTunnelConfig {
127        self.config
128    }
129}