foundations_sentry/
hook.rs1use crate::SentrySettings;
4use foundations::ratelimit::StaticQuantaClock;
5use governor::{Quota, RateLimiter};
6use std::borrow::Cow;
7use std::num::NonZeroU32;
8use std::sync::Arc;
9
10type Fingerprint = Cow<'static, str>;
11
12const SENTRY_LIMITER_CLEANUP_QUOTA: Quota =
15 Quota::per_hour(NonZeroU32::new(3).unwrap()).allow_burst(NonZeroU32::new(1).unwrap());
16
17pub fn install_hook_with_settings(
26 options: &mut sentry_core::ClientOptions,
27 settings: &SentrySettings,
28) {
29 let rate_limiter = settings.max_events_per_second.map(|rl| {
30 RateLimiter::dashmap_with_clock(Quota::per_second(rl), StaticQuantaClock::default())
31 });
32
33 let previous = options.before_send.take();
34
35 options.before_send = Some(Arc::new(move |mut event| {
36 if let Some(limiter) = &rate_limiter {
37 foundations::ratelimit!(SENTRY_LIMITER_CLEANUP_QUOTA; limiter.retain_recent());
38
39 let fp = extract_fingerprint(&event);
40 if limiter.check_key(&fp).is_err() {
41 return None;
42 }
43 }
44
45 if let Some(prev) = &previous {
46 event = prev(event)?;
47 }
48
49 super::metrics::sentry::events_total(event.level).inc();
50
51 Some(event)
52 }));
53}
54
55fn extract_fingerprint(event: &sentry_core::protocol::Event<'static>) -> Fingerprint {
63 use sentry_core::Level;
64
65 let explicit_fp = &event.fingerprint;
67 if !explicit_fp.is_empty() && !is_sentry_default_fingerprint(explicit_fp) {
68 if let [fp] = explicit_fp.as_ref() {
69 return fp.clone();
71 }
72 return explicit_fp.join("::").into();
73 }
74
75 if let Some(msg) = &event.message {
77 return msg.clone().into();
78 }
79
80 if let Some(exc) = event.exception.first() {
82 if let Some(val) = &exc.value {
83 return val.clone().into();
84 }
85 if !exc.ty.is_empty() {
86 return exc.ty.clone().into();
87 }
88 }
89
90 Cow::Borrowed(match event.level {
92 Level::Debug => "level::debug",
93 Level::Info => "level::info",
94 Level::Warning => "level::warning",
95 Level::Error => "level::error",
96 Level::Fatal => "level::fatal",
97 })
98}
99
100fn is_sentry_default_fingerprint(fp: &[Cow<'_, str>]) -> bool {
102 if let [fp] = fp {
103 return matches!(fp.as_ref(), "{{ default }}" | "{{default}}");
104 }
105 false
106}