Skip to main content

docspec_http/telemetry/
sentry.rs

1//! Sentry telemetry backend internals.
2
3use core::str::FromStr as _;
4
5/// Initializes the Sentry SDK with sanitized client options.
6pub(in crate::telemetry) fn init_sentry(
7    data_source_name: &str,
8) -> Option<::sentry::ClientInitGuard> {
9    sentry::types::Dsn::from_str(data_source_name).map_or_else(
10        |_| {
11            eprintln!("warning: invalid Sentry DSN format; Sentry disabled");
12            None
13        },
14        |parsed_data_source_name| Some(sentry::init(client_options(parsed_data_source_name))),
15    )
16}
17
18fn client_options(parsed_data_source_name: sentry::types::Dsn) -> sentry::ClientOptions {
19    sentry::ClientOptions {
20        dsn: Some(parsed_data_source_name),
21        release: sentry::release_name!(),
22        environment: Some(env_environment()),
23        sample_rate: env_sample_rate(),
24        traces_sample_rate: env_traces_sample_rate(),
25        send_default_pii: false,
26        attach_stacktrace: true,
27        before_send: Some(std::sync::Arc::new(before_send)),
28        ..Default::default()
29    }
30}
31
32fn env_environment() -> std::borrow::Cow<'static, str> {
33    match std::env::var("SENTRY_ENVIRONMENT") {
34        Ok(value) if !value.is_empty() => std::borrow::Cow::Owned(value),
35        _ => std::borrow::Cow::Borrowed("production"),
36    }
37}
38
39fn env_sample_rate() -> f32 {
40    super::env_rate("SENTRY_SAMPLE_RATE", 1.0)
41}
42
43fn env_traces_sample_rate() -> f32 {
44    super::env_rate("SENTRY_TRACES_SAMPLE_RATE", 0.0)
45}
46
47fn before_send(
48    mut event: sentry::protocol::Event<'static>,
49) -> Option<sentry::protocol::Event<'static>> {
50    if let Some(request) = event.request.as_mut() {
51        let _ = request.data.take();
52    }
53    event
54        .extra
55        .retain(|extra_key, _| !extra_key.to_lowercase().contains("body"));
56    Some(event)
57}
58
59/// Returns the configured Sentry tracing layer.
60pub(in crate::telemetry) fn tracing_layer<S>() -> sentry::integrations::tracing::SentryLayer<S>
61where
62    S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
63{
64    sentry::integrations::tracing::layer().event_filter(|metadata| {
65        use sentry::integrations::tracing::EventFilter;
66
67        match *metadata.level() {
68            tracing::Level::ERROR => EventFilter::Event,
69            tracing::Level::WARN | tracing::Level::INFO | tracing::Level::DEBUG => {
70                EventFilter::Breadcrumb
71            }
72            tracing::Level::TRACE => EventFilter::Ignore,
73        }
74    })
75}
76
77/// Returns the Sentry tower hub binding layer.
78pub(in crate::telemetry) fn tower_new_layer(
79) -> sentry::integrations::tower::NewSentryLayer<axum::http::Request<axum::body::Body>> {
80    sentry::integrations::tower::NewSentryLayer::new_from_top()
81}
82
83/// Returns the Sentry HTTP request enrichment layer.
84pub(in crate::telemetry) fn tower_http_layer() -> sentry::integrations::tower::SentryHttpLayer {
85    sentry::integrations::tower::SentryHttpLayer::new()
86}
87
88#[cfg(test)]
89mod tests {
90    use std::sync::Mutex;
91
92    static ENV_MUTEX: Mutex<()> = Mutex::new(());
93
94    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
95        match ENV_MUTEX.lock() {
96            Ok(guard) => guard,
97            Err(poisoned) => poisoned.into_inner(),
98        }
99    }
100
101    #[test]
102    fn before_send_strips_request_data() {
103        let event: sentry::protocol::Event<'static> = sentry::protocol::Event {
104            request: Some(sentry::protocol::Request {
105                data: Some(String::from("# secret document body")),
106                ..sentry::protocol::Request::default()
107            }),
108            ..sentry::protocol::Event::default()
109        };
110
111        let stripped = super::before_send(event);
112
113        assert!(matches!(
114            stripped
115                .as_ref()
116                .and_then(|stripped_event| stripped_event.request.as_ref()),
117            Some(request) if request.data.is_none()
118        ));
119    }
120
121    #[test]
122    fn before_send_strips_body_keys_in_extra() {
123        let mut event: sentry::protocol::Event<'static> = sentry::protocol::Event::default();
124        event
125            .extra
126            .insert(String::from("request_body"), serde_json::json!("secret"));
127        event
128            .extra
129            .insert(String::from("response_body"), serde_json::json!("secret"));
130        event
131            .extra
132            .insert(String::from("other"), serde_json::json!("kept"));
133
134        let stripped = super::before_send(event);
135
136        assert!(
137            matches!(stripped, Some(stripped_event) if stripped_event.extra.len() == 1 && stripped_event.extra.contains_key("other"))
138        );
139    }
140
141    #[test]
142    fn env_sample_rate_clamps_high() {
143        let _env_guard = lock_env();
144        std::env::set_var("SENTRY_SAMPLE_RATE", "5.0");
145
146        assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
147
148        std::env::remove_var("SENTRY_SAMPLE_RATE");
149    }
150
151    #[test]
152    fn env_sample_rate_clamps_low() {
153        let _env_guard = lock_env();
154        std::env::set_var("SENTRY_SAMPLE_RATE", "-0.5");
155
156        assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(0.0));
157
158        std::env::remove_var("SENTRY_SAMPLE_RATE");
159    }
160
161    #[test]
162    fn env_sample_rate_defaults_to_one() {
163        let _env_guard = lock_env();
164        std::env::remove_var("SENTRY_SAMPLE_RATE");
165
166        assert_eq!(super::env_sample_rate().to_bits(), f32::to_bits(1.0));
167    }
168
169    #[test]
170    fn env_traces_sample_rate_defaults_to_zero() {
171        let _env_guard = lock_env();
172        std::env::remove_var("SENTRY_TRACES_SAMPLE_RATE");
173
174        assert_eq!(super::env_traces_sample_rate().to_bits(), f32::to_bits(0.0));
175    }
176
177    #[test]
178    fn env_environment_defaults_to_production() {
179        let _env_guard = lock_env();
180        std::env::remove_var("SENTRY_ENVIRONMENT");
181
182        assert_eq!(super::env_environment(), "production");
183    }
184
185    #[test]
186    fn init_sentry_returns_none_on_invalid_dsn() {
187        assert!(super::init_sentry("not-a-dsn").is_none());
188    }
189
190    #[test]
191    fn client_options_enables_attach_stacktrace() -> Result<(), Box<dyn core::error::Error>> {
192        let _env_guard = lock_env();
193        let dsn: sentry::types::Dsn = "https://public@example.com/1".parse()?;
194        let opts = super::client_options(dsn);
195        if opts.attach_stacktrace {
196            Ok(())
197        } else {
198            Err("attach_stacktrace should be true".into())
199        }
200    }
201}