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