strut_sentry/
config.rs

1use humantime::parse_duration;
2use secure_string::SecureString;
3use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
4use serde::{Deserialize, Deserializer};
5use std::fmt::Formatter;
6use std::time::Duration;
7use strut_factory::impl_deserialize_field;
8
9/// Represents the application-level configuration section that covers everything
10/// related to Sentry integration.
11///
12/// This config comes with a custom [`Deserialize`] implementation, to support more
13/// human-oriented textual configuration.
14#[derive(Debug, Clone, PartialEq)]
15pub struct SentryConfig {
16    dsn: SecureString,
17    debug: bool,
18    sample_rate: f32,
19    traces_sample_rate: f32,
20    max_breadcrumbs: usize,
21    attach_stacktrace: bool,
22    shutdown_timeout: Duration,
23}
24
25impl SentryConfig {
26    /// Returns the Sentry DSN (Data Source Name), which acts like a connection
27    /// string. This value tells the app where to send error reports.
28    pub fn dsn(&self) -> &SecureString {
29        &self.dsn
30    }
31
32    /// Indicates whether Sentry debug mode is enabled.
33    ///
34    /// When `true`, the Sentry client will log internal operations (e.g., failed
35    /// event deliveries). Useful during development or troubleshooting.
36    pub fn debug(&self) -> bool {
37        self.debug
38    }
39
40    /// Returns the sample rate for error event reporting (0.0 to 1.0).
41    ///
42    /// For example, a value of 1.0 means all errors will be reported; 0.5 means
43    /// only half (randomly selected). Helps control how much data is sent.
44    pub fn sample_rate(&self) -> f32 {
45        self.sample_rate
46    }
47
48    /// Returns the traces sample rate (0.0 to 1.0), which controls performance
49    /// tracing.
50    ///
51    /// This affects how often spans and transaction traces are sent to Sentry.
52    /// Higher values give more observability but can increase overhead.
53    pub fn traces_sample_rate(&self) -> f32 {
54        self.traces_sample_rate
55    }
56
57    /// Returns the maximum number of breadcrumbs (context logs) stored per event.
58    ///
59    /// Breadcrumbs are small logs (like “user clicked button”) that help
60    /// reconstruct what happened before an error. This setting limits how many
61    /// of those are retained.
62    pub fn max_breadcrumbs(&self) -> usize {
63        self.max_breadcrumbs
64    }
65
66    /// Indicates whether stack traces should be automatically attached to events.
67    ///
68    /// When `true`, errors and certain logs will include call stacks to help
69    /// identify where they originated. This improves debugging but adds overhead.
70    pub fn attach_stacktrace(&self) -> bool {
71        self.attach_stacktrace
72    }
73
74    /// Returns the maximum time allowed to send any remaining events before
75    /// shutdown.
76    ///
77    /// On application exit, Sentry will attempt to flush queued events.
78    /// This timeout defines how long it should wait before giving up.
79    pub fn shutdown_timeout(&self) -> Duration {
80        self.shutdown_timeout
81    }
82}
83
84impl Default for SentryConfig {
85    fn default() -> Self {
86        Self {
87            dsn: Self::default_dsn(),
88            debug: Self::default_debug(),
89            sample_rate: Self::default_sample_rate(),
90            traces_sample_rate: Self::default_traces_sample_rate(),
91            max_breadcrumbs: Self::default_max_breadcrumbs(),
92            attach_stacktrace: Self::default_attach_stacktrace(),
93            shutdown_timeout: Self::default_shutdown_timeout(),
94        }
95    }
96}
97
98impl SentryConfig {
99    fn default_dsn() -> SecureString {
100        "".into()
101    }
102
103    fn default_debug() -> bool {
104        false
105    }
106
107    fn default_sample_rate() -> f32 {
108        1.0
109    }
110
111    fn default_traces_sample_rate() -> f32 {
112        0.0
113    }
114
115    fn default_max_breadcrumbs() -> usize {
116        64
117    }
118
119    fn default_attach_stacktrace() -> bool {
120        false
121    }
122
123    fn default_shutdown_timeout() -> Duration {
124        Duration::from_secs(2)
125    }
126}
127
128impl AsRef<SentryConfig> for SentryConfig {
129    fn as_ref(&self) -> &SentryConfig {
130        self
131    }
132}
133
134const _: () = {
135    impl<'de> Deserialize<'de> for SentryConfig {
136        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137        where
138            D: Deserializer<'de>,
139        {
140            deserializer.deserialize_any(SentryConfigVisitor)
141        }
142    }
143
144    struct SentryConfigVisitor;
145
146    impl<'de> Visitor<'de> for SentryConfigVisitor {
147        type Value = SentryConfig;
148
149        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
150            formatter.write_str("a map of Sentry integration configuration or a string Sentry DSN")
151        }
152
153        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
154        where
155            E: Error,
156        {
157            Ok(SentryConfig {
158                dsn: SecureString::from(value),
159                ..SentryConfig::default()
160            })
161        }
162
163        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
164        where
165            E: Error,
166        {
167            Ok(SentryConfig {
168                dsn: SecureString::from(value),
169                ..SentryConfig::default()
170            })
171        }
172
173        fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
174        where
175            A: MapAccess<'de>,
176        {
177            let mut dsn = None;
178            let mut debug = None;
179            let mut sample_rate = None;
180            let mut traces_sample_rate = None;
181            let mut max_breadcrumbs = None;
182            let mut attach_stacktrace = None;
183            let mut shutdown_timeout = None;
184
185            while let Some(key) = map.next_key()? {
186                match key {
187                    SentryConfigField::dsn => key.poll(&mut map, &mut dsn)?,
188                    SentryConfigField::debug => key.poll(&mut map, &mut debug)?,
189                    SentryConfigField::sample_rate => key.poll(&mut map, &mut sample_rate)?,
190                    SentryConfigField::traces_sample_rate => {
191                        key.poll(&mut map, &mut traces_sample_rate)?
192                    }
193                    SentryConfigField::max_breadcrumbs => {
194                        key.poll(&mut map, &mut max_breadcrumbs)?
195                    }
196                    SentryConfigField::attach_stacktrace => {
197                        key.poll(&mut map, &mut attach_stacktrace)?
198                    }
199                    SentryConfigField::shutdown_timeout => {
200                        let duration_string = map.next_value::<String>()?;
201                        let duration = parse_duration(&duration_string).map_err(Error::custom)?;
202                        shutdown_timeout = Some(duration);
203                        IgnoredAny
204                    }
205                    SentryConfigField::__ignore => map.next_value()?,
206                };
207            }
208
209            Ok(SentryConfig {
210                dsn: dsn.unwrap_or_else(SentryConfig::default_dsn),
211                debug: debug.unwrap_or_else(SentryConfig::default_debug),
212                sample_rate: sample_rate.unwrap_or_else(SentryConfig::default_sample_rate),
213                traces_sample_rate: traces_sample_rate
214                    .unwrap_or_else(SentryConfig::default_traces_sample_rate),
215                max_breadcrumbs: max_breadcrumbs
216                    .unwrap_or_else(SentryConfig::default_max_breadcrumbs),
217                attach_stacktrace: attach_stacktrace
218                    .unwrap_or_else(SentryConfig::default_attach_stacktrace),
219                shutdown_timeout: shutdown_timeout
220                    .unwrap_or_else(SentryConfig::default_shutdown_timeout),
221            })
222        }
223    }
224
225    impl_deserialize_field!(
226        SentryConfigField,
227        strut_deserialize::Slug::eq_as_slugs,
228        dsn,
229        debug,
230        sample_rate,
231        traces_sample_rate,
232        max_breadcrumbs,
233        attach_stacktrace,
234        shutdown_timeout,
235    );
236};
237
238#[cfg(test)]
239mod tests {
240    use crate::SentryConfig;
241    use pretty_assertions::assert_eq;
242    use secure_string::SecureString;
243    use std::time::Duration;
244
245    #[test]
246    fn from_empty() {
247        // Given
248        let input = "{}";
249        let expected_output = SentryConfig::default();
250
251        // When
252        let actual_output = serde_yml::from_str::<SentryConfig>(input).unwrap();
253
254        // Then
255        assert_eq!(expected_output, actual_output);
256    }
257
258    #[test]
259    fn from_string() {
260        // Given
261        let input = "some_dsn";
262        let expected_output = SentryConfig {
263            dsn: SecureString::from("some_dsn"),
264            ..SentryConfig::default()
265        };
266
267        // When
268        let actual_output = serde_yml::from_str::<SentryConfig>(input).unwrap();
269
270        // Then
271        assert_eq!(expected_output, actual_output);
272    }
273
274    #[test]
275    fn from_map_sparse() {
276        // Given
277        let input = r#"
278dsn: some_dsn
279"#;
280        let expected_output = SentryConfig {
281            dsn: SecureString::from("some_dsn"),
282            ..SentryConfig::default()
283        };
284
285        // When
286        let actual_output = serde_yml::from_str::<SentryConfig>(input).unwrap();
287
288        // Then
289        assert_eq!(expected_output, actual_output);
290    }
291
292    #[test]
293    fn from_map_full() {
294        // Given
295        let input = r#"
296dsn: some_dsn
297debug: true
298sample_rate: 0.5
299traces_sample_rate: 0.4
300max_breadcrumbs: 50
301attach_stacktrace: true
302shutdown_timeout: 1s 500ms
303"#;
304        let expected_output = SentryConfig {
305            dsn: SecureString::from("some_dsn"),
306            debug: true,
307            sample_rate: 0.5,
308            traces_sample_rate: 0.4,
309            max_breadcrumbs: 50,
310            attach_stacktrace: true,
311            shutdown_timeout: Duration::from_millis(1500),
312        };
313
314        // When
315        let actual_output = serde_yml::from_str::<SentryConfig>(input).unwrap();
316
317        // Then
318        assert_eq!(expected_output, actual_output);
319    }
320}