Skip to main content

bugwatch/
types.rs

1//! Type definitions for Bugwatch Rust SDK.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Error severity level.
8#[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/// A single frame in a stack trace.
37#[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/// Information about an exception.
77#[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/// A breadcrumb for tracking user actions and events.
105#[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/// User information for error context.
139#[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/// HTTP request information for error context.
175#[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/// Runtime environment information.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct RuntimeInfo {
194    pub name: String,
195    pub version: String,
196}
197
198/// SDK information.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct SdkInfo {
201    pub name: String,
202    pub version: String,
203}
204
205/// Complete error event to send to Bugwatch.
206#[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    // Get rustc version at runtime
277    option_env!("RUSTC_VERSION")
278        .unwrap_or("unknown")
279        .to_string()
280}
281
282/// Configuration options for the Bugwatch client.
283#[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}