Skip to main content

aptabase_rs/
lib.rs

1mod client;
2mod config;
3mod dispatcher;
4mod sys;
5
6use std::{panic::PanicHookInfo, sync::Arc, time::Duration};
7
8pub use client::{AptabaseClient, new_session_id};
9use config::Config;
10use serde_json::json;
11
12#[derive(Default, Debug, Clone)]
13pub struct InitOptions {
14    pub host: Option<String>,
15    pub flush_interval: Option<Duration>,
16}
17
18/// The Aptabase client builder
19pub struct Builder {
20    app_key: String,
21    app_version: String,
22    session_id: Option<String>,
23    enable_polling: bool,
24    panic_hook: Option<PanicHook>,
25    options: InitOptions,
26}
27
28pub type PanicHook =
29    Box<dyn Fn(&AptabaseClient, &PanicHookInfo<'_>, String) + 'static + Sync + Send>;
30
31fn get_panic_message(info: &PanicHookInfo) -> String {
32    let payload = info.payload();
33    if let Some(s) = payload.downcast_ref::<&str>() {
34        return s.to_string();
35    } else if let Some(s) = payload.downcast_ref::<String>() {
36        return s.to_string();
37    }
38
39    format!("{:?}", payload)
40}
41
42impl Builder {
43    /// Creates a new builder.
44    pub fn new(app_key: &str, app_version: &str) -> Self {
45        Self {
46            app_key: app_key.into(),
47            app_version: app_version.into(),
48            session_id: None,
49            enable_polling: false,
50            panic_hook: None,
51            options: Default::default(),
52        }
53    }
54
55    /// Sets custom options to use for the Aptabase client.
56    pub fn with_options(mut self, opts: InitOptions) -> Self {
57        self.options = opts;
58        self
59    }
60
61    /// Sets the initial session ID.
62    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
63        self.session_id = Some(session_id.into());
64        self
65    }
66
67    /// Sets whether polling should be enabled.
68    pub fn with_polling(mut self, enable: bool) -> Self {
69        self.enable_polling = enable;
70        self
71    }
72
73    /// Sets a custom panic hook.
74    pub fn with_panic_hook(mut self, hook: PanicHook) -> Self {
75        self.panic_hook = Some(hook);
76        self
77    }
78
79    /// Enables the default panic hook.
80    pub fn with_default_panic_hook(self) -> Self {
81        self.with_panic_hook(Box::new(|client, info, message| {
82            let location = info
83                .location()
84                .map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
85                .unwrap_or_default();
86            let _ = client.track_event(
87                "panic",
88                Some(json!({
89                    "info": format!("{} ({})", message, location),
90                })),
91            );
92        }))
93    }
94
95    /// Builds and initializes the client
96    pub fn build(self) -> Arc<AptabaseClient> {
97        let cfg = Config::new(self.app_key, self.session_id, self.options);
98        let client = Arc::new(AptabaseClient::new(&cfg, self.app_version));
99
100        if self.enable_polling {
101            client.start_polling(cfg.flush_interval);
102        }
103
104        if let Some(hook) = self.panic_hook {
105            let default_panic = std::panic::take_hook();
106            let hook_client = client.clone();
107            std::panic::set_hook(Box::new(move |info| {
108                let msg = get_panic_message(info);
109                hook(&hook_client, info, msg);
110                hook_client.flush_blocking();
111                default_panic(info);
112            }));
113        }
114
115        client
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn builder_uses_supplied_session_id() {
125        // Act
126        let client = Builder::new("A-DEV-123", "test")
127            .with_session_id("persisted-session")
128            .build();
129
130        // Assert
131        assert_eq!(client.eval_session_id(), "persisted-session");
132    }
133
134    #[test]
135    fn builder_treats_empty_session_id_as_absent() {
136        // Act
137        let client = Builder::new("A-DEV-123", "test")
138            .with_session_id("")
139            .build();
140
141        // Assert
142        assert!(!client.eval_session_id().is_empty());
143    }
144}