af_sentry/
lib.rs

1// Copyright © 2020 Alexandra Frydl
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7pub use sentry::{ClientInitGuard, ClientOptions, IntoDsn, User};
8
9use af_core::prelude::*;
10use std::collections::BTreeMap;
11
12/// The type of the [`Error::fingerprint`] field.
13pub type Fingerprint = Cow<'static, [Cow<'static, str>]>;
14
15/// Initializes Sentry with the given DSN or [`ClientOptions`].
16pub fn init(options: impl Into<ClientOptions>) -> ClientInitGuard {
17  let options = options.into();
18
19  sentry::init(options)
20}
21
22/// Creates a sentry error.
23#[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
38/// Returns `true` if error reporting is enabled.
39///
40/// Error reporting is enabled if [`init()`] has been called with a valid DSN.
41pub fn is_enabled() -> bool {
42  sentry::Hub::with(|hub| hub.client().map(|c| c.is_enabled()).unwrap_or_default())
43}
44
45/// A sentry error.
46///
47/// Errors are automatically reported when dropped.
48#[derive(Debug)]
49pub struct Error {
50  /// A short description of the error.
51  pub description: String,
52  /// A detailed description of the error.
53  pub detail: String,
54  /// The fingerprint of the error.
55  ///
56  /// Errors with the same fingerprint are grouped together. The default groups
57  /// by [`type`] and [`ClientOptions::environment`].
58  pub fingerprint: Fingerprint,
59  /// The type of the error.
60  pub type_name: String,
61  /// Additional tags to apply to the error.
62  pub tags: BTreeMap<String, String>,
63  /// User data to send with the error.
64  pub user: User,
65  /// The UUID of the event.
66  pub uuid: Uuid,
67}
68
69const DEFAULT_FINGERPRINT: Fingerprint =
70  Cow::Borrowed(&[Cow::Borrowed("{{ type }}"), Cow::Borrowed("{{ tags.environment }}")]);
71
72impl Error {
73  /// Creates a new error with the given type.
74  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  /// Sets the short description of the error.
87  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  /// Sets the detailed description of the error.
113  pub fn set_detail(&mut self, detail: impl ToString) {
114    self.detail = detail.to_string();
115  }
116
117  /// Adds extra tagged information.
118  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  /// Sets the short description of the error.
123  pub fn with_description(mut self, description: impl ToString) -> Self {
124    self.set_description(description);
125    self
126  }
127
128  /// Sets the detailed description of the error.
129  pub fn with_detail(mut self, detail: impl ToString) -> Self {
130    self.set_detail(detail);
131    self
132  }
133
134  /// Sets the fingerprint used to group the error.
135  pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
136    self.fingerprint = fingerprint;
137    self
138  }
139
140  /// Adds extra tagged information.
141  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  /// Adds user information.
147  pub fn with_user(mut self, user: User) -> Self {
148    self.user = user;
149    self
150  }
151
152  /// Adds user ID information.
153  pub fn with_user_id(mut self, id: impl ToString) -> Self {
154    self.user.id = Some(id.to_string());
155    self
156  }
157
158  /// Reports this error to sentry.
159  ///
160  /// Equivalent to dropping the error, but returns the error [`Uuid`].
161  pub fn report(mut self) -> Uuid {
162    let uuid = self.report_mut();
163    mem::forget(self);
164    uuid
165  }
166
167  /// Reports this error to sentry.
168  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}