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