1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum Level {
11 Debug,
12 Info,
13 Warning,
14 Error,
15 Fatal,
16}
17
18impl Default for Level {
19 fn default() -> Self {
20 Self::Error
21 }
22}
23
24impl std::fmt::Display for Level {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::Debug => write!(f, "debug"),
28 Self::Info => write!(f, "info"),
29 Self::Warning => write!(f, "warning"),
30 Self::Error => write!(f, "error"),
31 Self::Fatal => write!(f, "fatal"),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct StackFrame {
39 pub filename: String,
40 pub function: String,
41 pub lineno: u32,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub colno: Option<u32>,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub context_line: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub pre_context: Option<Vec<String>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub post_context: Option<Vec<String>>,
50 #[serde(default = "default_true")]
51 pub in_app: bool,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub module: Option<String>,
54}
55
56fn default_true() -> bool {
57 true
58}
59
60impl StackFrame {
61 pub fn new(filename: impl Into<String>, function: impl Into<String>, lineno: u32) -> Self {
62 Self {
63 filename: filename.into(),
64 function: function.into(),
65 lineno,
66 colno: None,
67 context_line: None,
68 pre_context: None,
69 post_context: None,
70 in_app: true,
71 module: None,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ExceptionInfo {
79 #[serde(rename = "type")]
80 pub error_type: String,
81 pub value: String,
82 #[serde(default)]
83 pub stacktrace: Vec<StackFrame>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub module: Option<String>,
86}
87
88impl ExceptionInfo {
89 pub fn new(error_type: impl Into<String>, value: impl Into<String>) -> Self {
90 Self {
91 error_type: error_type.into(),
92 value: value.into(),
93 stacktrace: Vec::new(),
94 module: None,
95 }
96 }
97
98 pub fn with_stacktrace(mut self, stacktrace: Vec<StackFrame>) -> Self {
99 self.stacktrace = stacktrace;
100 self
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Breadcrumb {
107 pub category: String,
108 pub message: String,
109 #[serde(default)]
110 pub level: Level,
111 pub timestamp: DateTime<Utc>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub data: Option<HashMap<String, serde_json::Value>>,
114}
115
116impl Breadcrumb {
117 pub fn new(category: impl Into<String>, message: impl Into<String>) -> Self {
118 Self {
119 category: category.into(),
120 message: message.into(),
121 level: Level::Info,
122 timestamp: Utc::now(),
123 data: None,
124 }
125 }
126
127 pub fn with_level(mut self, level: Level) -> Self {
128 self.level = level;
129 self
130 }
131
132 pub fn with_data(mut self, data: HashMap<String, serde_json::Value>) -> Self {
133 self.data = Some(data);
134 self
135 }
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140pub struct UserContext {
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub id: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub email: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub username: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub ip_address: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub extra: Option<HashMap<String, serde_json::Value>>,
151}
152
153impl UserContext {
154 pub fn new() -> Self {
155 Self::default()
156 }
157
158 pub fn with_id(mut self, id: impl Into<String>) -> Self {
159 self.id = Some(id.into());
160 self
161 }
162
163 pub fn with_email(mut self, email: impl Into<String>) -> Self {
164 self.email = Some(email.into());
165 self
166 }
167
168 pub fn with_username(mut self, username: impl Into<String>) -> Self {
169 self.username = Some(username.into());
170 self
171 }
172}
173
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176pub struct RequestContext {
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub url: Option<String>,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub method: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub headers: Option<HashMap<String, String>>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub query_string: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub data: Option<serde_json::Value>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub client_ip: Option<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct RuntimeInfo {
194 pub name: String,
195 pub version: String,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct SdkInfo {
201 pub name: String,
202 pub version: String,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct ErrorEvent {
208 pub event_id: String,
209 pub timestamp: DateTime<Utc>,
210 pub level: Level,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub exception: Option<ExceptionInfo>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub message: Option<String>,
215 #[serde(default = "default_platform")]
216 pub platform: String,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub sdk: Option<SdkInfo>,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub runtime: Option<RuntimeInfo>,
221 #[serde(skip_serializing_if = "Option::is_none")]
222 pub request: Option<RequestContext>,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub user: Option<UserContext>,
225 #[serde(default)]
226 pub tags: HashMap<String, String>,
227 #[serde(default)]
228 pub extra: HashMap<String, serde_json::Value>,
229 #[serde(default)]
230 pub breadcrumbs: Vec<Breadcrumb>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub fingerprint: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub environment: Option<String>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub release: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub server_name: Option<String>,
239}
240
241fn default_platform() -> String {
242 "rust".to_string()
243}
244
245impl ErrorEvent {
246 pub fn new(event_id: impl Into<String>, level: Level) -> Self {
247 Self {
248 event_id: event_id.into(),
249 timestamp: Utc::now(),
250 level,
251 exception: None,
252 message: None,
253 platform: "rust".to_string(),
254 sdk: Some(SdkInfo {
255 name: "bugwatch-rust".to_string(),
256 version: env!("CARGO_PKG_VERSION").to_string(),
257 }),
258 runtime: Some(RuntimeInfo {
259 name: "rust".to_string(),
260 version: rustc_version(),
261 }),
262 request: None,
263 user: None,
264 tags: HashMap::new(),
265 extra: HashMap::new(),
266 breadcrumbs: Vec::new(),
267 fingerprint: None,
268 environment: None,
269 release: None,
270 server_name: None,
271 }
272 }
273}
274
275fn rustc_version() -> String {
276 option_env!("RUSTC_VERSION")
278 .unwrap_or("unknown")
279 .to_string()
280}
281
282#[derive(Debug, Clone)]
284pub struct BugwatchOptions {
285 pub api_key: String,
286 pub endpoint: String,
287 pub environment: Option<String>,
288 pub release: Option<String>,
289 pub server_name: Option<String>,
290 pub debug: bool,
291 pub max_breadcrumbs: usize,
292 pub sample_rate: f64,
293 pub attach_stacktrace: bool,
294}
295
296impl BugwatchOptions {
297 pub fn new(api_key: impl Into<String>) -> Self {
298 Self {
299 api_key: api_key.into(),
300 endpoint: "https://api.bugwatch.dev".to_string(),
301 environment: None,
302 release: None,
303 server_name: None,
304 debug: false,
305 max_breadcrumbs: 100,
306 sample_rate: 1.0,
307 attach_stacktrace: true,
308 }
309 }
310
311 pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
312 self.endpoint = endpoint.into();
313 self
314 }
315
316 pub fn with_environment(mut self, environment: impl Into<String>) -> Self {
317 self.environment = Some(environment.into());
318 self
319 }
320
321 pub fn with_release(mut self, release: impl Into<String>) -> Self {
322 self.release = Some(release.into());
323 self
324 }
325
326 pub fn with_debug(mut self, debug: bool) -> Self {
327 self.debug = debug;
328 self
329 }
330
331 pub fn with_sample_rate(mut self, sample_rate: f64) -> Self {
332 self.sample_rate = sample_rate.clamp(0.0, 1.0);
333 self
334 }
335}
336
337impl Default for BugwatchOptions {
338 fn default() -> Self {
339 Self::new("")
340 }
341}