1pub use sentry::{ClientInitGuard, ClientOptions, IntoDsn, User};
8
9use af_core::prelude::*;
10use std::collections::BTreeMap;
11
12pub type Fingerprint = Cow<'static, [Cow<'static, str>]>;
14
15pub fn init(options: impl Into<ClientOptions>) -> ClientInitGuard {
17 let options = options.into();
18
19 sentry::init(options)
20}
21
22#[macro_export]
24macro_rules! error {
25 ($type_name:expr, $format:literal, $($args:tt)+) => {
26 $crate::Error::new($type_name).with_description(format_args!($format, $($args)+))
27 };
28
29 ($type_name:expr, $($args:tt)+) => {
30 $crate::Error::new($type_name).with_description($($args)+)
31 };
32
33 ($($args:tt)+) => {
34 $crate::Error::new($($args)*)
35 };
36}
37
38pub fn is_enabled() -> bool {
42 sentry::Hub::with(|hub| hub.client().map(|c| c.is_enabled()).unwrap_or_default())
43}
44
45#[derive(Debug)]
49pub struct Error {
50 pub description: String,
52 pub detail: String,
54 pub fingerprint: Fingerprint,
59 pub type_name: String,
61 pub tags: BTreeMap<String, String>,
63 pub user: User,
65 pub uuid: Uuid,
67}
68
69const DEFAULT_FINGERPRINT: Fingerprint =
70 Cow::Borrowed(&[Cow::Borrowed("{{ type }}"), Cow::Borrowed("{{ tags.environment }}")]);
71
72impl Error {
73 pub fn new(type_name: impl Into<String>) -> Self {
75 Self {
76 description: default(),
77 detail: default(),
78 fingerprint: DEFAULT_FINGERPRINT,
79 type_name: type_name.into(),
80 tags: default(),
81 user: default(),
82 uuid: Uuid::new(),
83 }
84 }
85
86 pub fn set_description(&mut self, description: impl ToString) {
88 let mut description = description.to_string();
89
90 if self.detail.is_empty() {
91 self.detail = description.clone();
92 }
93
94 if let Some(i) = description.find('\n') {
95 description.truncate(i);
96 description.truncate(description.trim_end().len());
97 }
98
99 if description.ends_with(':') {
100 description.pop();
101 description.push('.');
102 }
103
104 if description.len() > 256 {
105 description.truncate(255);
106 description.push('…');
107 }
108
109 self.description = description;
110 }
111
112 pub fn set_detail(&mut self, detail: impl ToString) {
114 self.detail = detail.to_string();
115 }
116
117 pub fn set_tag(&mut self, name: impl Into<String>, value: impl ToString) {
119 self.tags.insert(name.into(), value.to_string());
120 }
121
122 pub fn with_description(mut self, description: impl ToString) -> Self {
124 self.set_description(description);
125 self
126 }
127
128 pub fn with_detail(mut self, detail: impl ToString) -> Self {
130 self.set_detail(detail);
131 self
132 }
133
134 pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
136 self.fingerprint = fingerprint;
137 self
138 }
139
140 pub fn with_tag(mut self, name: impl Into<String>, value: impl ToString) -> Self {
142 self.set_tag(name, value);
143 self
144 }
145
146 pub fn with_user(mut self, user: User) -> Self {
148 self.user = user;
149 self
150 }
151
152 pub fn with_user_id(mut self, id: impl ToString) -> Self {
154 self.user.id = Some(id.to_string());
155 self
156 }
157
158 pub fn report(mut self) -> Uuid {
162 let uuid = self.report_mut();
163 mem::forget(self);
164 uuid
165 }
166
167 fn report_mut(&mut self) -> Uuid {
169 let mut event = sentry::protocol::Event::new();
170
171 if !self.detail.is_empty() {
172 event.message = Some(mem::take(&mut self.detail));
173 }
174
175 event.exception.values.push(sentry::protocol::Exception {
176 ty: mem::take(&mut self.type_name),
177 value: Some(mem::take(&mut self.description)),
178 ..default()
179 });
180
181 event.fingerprint = mem::replace(&mut self.fingerprint, DEFAULT_FINGERPRINT);
182
183 mem::swap(&mut event.tags, &mut self.tags);
184
185 if let Some(env) = event.tags.remove("environment") {
186 event.environment = Some(env.into());
187 }
188
189 event.user = Some(mem::take(&mut self.user));
190
191 sentry::capture_event(event).into()
192 }
193}
194
195impl Drop for Error {
196 fn drop(&mut self) {
197 self.report_mut();
198 }
199}