telemetry_kit/
builder.rs

1//! Builder for TelemetryKit
2
3use crate::error::{Result, TelemetryError};
4use crate::telemetry::TelemetryKit;
5
6#[cfg(feature = "sync")]
7use crate::sync::SyncConfig;
8
9#[cfg(feature = "sync")]
10use crate::auto_sync::AutoSyncConfig;
11
12#[cfg(feature = "privacy")]
13use crate::privacy::PrivacyConfig;
14
15use std::path::PathBuf;
16
17/// Builder for configuring telemetry
18#[derive(Debug, Default)]
19pub struct TelemetryBuilder {
20    service_name: Option<String>,
21    service_version: Option<String>,
22    db_path: Option<PathBuf>,
23
24    #[cfg(feature = "sync")]
25    sync_config: Option<SyncConfig>,
26
27    #[cfg(feature = "sync")]
28    auto_sync_enabled: bool,
29
30    #[cfg(feature = "sync")]
31    auto_sync_config: AutoSyncConfig,
32
33    #[cfg(feature = "privacy")]
34    privacy_config: Option<PrivacyConfig>,
35}
36
37impl TelemetryBuilder {
38    /// Create a new telemetry builder
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Set the service name (required)
44    pub fn service_name(mut self, name: impl Into<String>) -> Result<Self> {
45        let name_str = name.into();
46
47        // Validate service name format
48        if !name_str
49            .chars()
50            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
51        {
52            return Err(TelemetryError::invalid_config(
53                "service_name",
54                &format!("'{}' contains invalid characters. Use only lowercase letters, numbers, dashes, and underscores (e.g., 'my-app', 'cli_tool')", name_str)
55            ));
56        }
57
58        self.service_name = Some(name_str);
59        Ok(self)
60    }
61
62    /// Set the service version (recommended)
63    pub fn service_version(mut self, version: impl Into<String>) -> Self {
64        self.service_version = Some(version.into());
65        self
66    }
67
68    /// Set custom database path for event storage
69    pub fn db_path(mut self, path: impl Into<PathBuf>) -> Self {
70        self.db_path = Some(path.into());
71        self
72    }
73
74    /// Configure sync settings
75    #[cfg(feature = "sync")]
76    pub fn sync(mut self, config: SyncConfig) -> Self {
77        self.sync_config = Some(config);
78        self
79    }
80
81    /// Enable automatic background syncing (enabled by default)
82    #[cfg(feature = "sync")]
83    pub fn auto_sync(mut self, enabled: bool) -> Self {
84        self.auto_sync_enabled = enabled;
85        self
86    }
87
88    /// Set the auto-sync interval in seconds (default: 60)
89    #[cfg(feature = "sync")]
90    pub fn sync_interval(mut self, seconds: u64) -> Self {
91        self.auto_sync_config.interval = seconds;
92        self
93    }
94
95    /// Configure whether to sync on shutdown (default: true)
96    #[cfg(feature = "sync")]
97    pub fn sync_on_shutdown(mut self, enabled: bool) -> Self {
98        self.auto_sync_config.sync_on_shutdown = enabled;
99        self
100    }
101
102    /// Shorthand for setting sync credentials
103    #[cfg(feature = "sync")]
104    pub fn with_sync_credentials(
105        mut self,
106        org_id: impl Into<String>,
107        app_id: impl Into<String>,
108        token: impl Into<String>,
109        secret: impl Into<String>,
110    ) -> Result<Self> {
111        let config = SyncConfig::builder()
112            .org_id(org_id)?
113            .app_id(app_id)?
114            .token(token)
115            .secret(secret)
116            .build()?;
117
118        self.sync_config = Some(config);
119        Ok(self)
120    }
121
122    /// Configure privacy settings
123    #[cfg(feature = "privacy")]
124    pub fn privacy(mut self, config: PrivacyConfig) -> Self {
125        self.privacy_config = Some(config);
126        self
127    }
128
129    /// Shorthand for enabling strict privacy mode (GDPR-compliant)
130    #[cfg(feature = "privacy")]
131    pub fn strict_privacy(mut self) -> Self {
132        self.privacy_config = Some(PrivacyConfig::strict());
133        self
134    }
135
136    /// Shorthand for minimal privacy mode
137    #[cfg(feature = "privacy")]
138    pub fn minimal_privacy(mut self) -> Self {
139        self.privacy_config = Some(PrivacyConfig::minimal());
140        self
141    }
142
143    /// Require user consent before tracking
144    #[cfg(feature = "privacy")]
145    pub fn consent_required(mut self, required: bool) -> Self {
146        let config = self.privacy_config.unwrap_or_default();
147        self.privacy_config = Some(PrivacyConfig {
148            consent_required: required,
149            ..config
150        });
151        self
152    }
153
154    /// Set data retention period in days (0 = forever)
155    #[cfg(feature = "privacy")]
156    pub fn data_retention(mut self, days: u32) -> Self {
157        let config = self.privacy_config.unwrap_or_default();
158        self.privacy_config = Some(PrivacyConfig {
159            data_retention_days: days,
160            ..config
161        });
162        self
163    }
164
165    /// Enable or disable path sanitization
166    #[cfg(feature = "privacy")]
167    pub fn sanitize_paths(mut self, enabled: bool) -> Self {
168        let config = self.privacy_config.unwrap_or_default();
169        self.privacy_config = Some(PrivacyConfig {
170            sanitize_paths: enabled,
171            ..config
172        });
173        self
174    }
175
176    /// Enable or disable email sanitization
177    #[cfg(feature = "privacy")]
178    pub fn sanitize_emails(mut self, enabled: bool) -> Self {
179        let config = self.privacy_config.unwrap_or_default();
180        self.privacy_config = Some(PrivacyConfig {
181            sanitize_emails: enabled,
182            ..config
183        });
184        self
185    }
186
187    /// Prompt for user consent interactively before building
188    ///
189    /// This will show an interactive consent dialog on the first run (when consent status is Unknown).
190    /// On subsequent runs, it will use the stored consent preference.
191    ///
192    /// This method is only available when both 'privacy' and 'cli' features are enabled.
193    ///
194    /// # Example
195    ///
196    /// ```no_run
197    /// # use telemetry_kit::prelude::*;
198    /// # #[tokio::main]
199    /// # async fn main() -> Result<()> {
200    /// let telemetry = TelemetryKit::builder()
201    ///     .service_name("my-app")?
202    ///     .service_version("1.0.0")
203    ///     .prompt_for_consent()?  // Shows interactive prompt on first run
204    ///     .build()?;
205    /// # Ok(())
206    /// # }
207    /// ```
208    #[cfg(all(feature = "privacy", feature = "cli"))]
209    pub fn prompt_for_consent(mut self) -> Result<Self> {
210        use crate::privacy::PrivacyManager;
211
212        let service_name = self
213            .service_name
214            .as_ref()
215            .ok_or_else(|| TelemetryError::missing_field("service_name"))?
216            .clone();
217
218        let service_version = self
219            .service_version
220            .as_ref()
221            .map(|s| s.as_str())
222            .unwrap_or("unknown");
223
224        // Create a privacy manager with current config
225        let config = self.privacy_config.clone().unwrap_or_default();
226        let manager = PrivacyManager::new(config.clone(), &service_name)?;
227
228        // Prompt for consent
229        let _consent_granted = manager.prompt_for_consent(&service_name, service_version)?;
230
231        // Set consent_required to true to respect the user's choice
232        self.privacy_config = Some(PrivacyConfig {
233            consent_required: true,
234            ..config
235        });
236
237        Ok(self)
238    }
239
240    /// Prompt for user consent with minimal message
241    ///
242    /// Similar to `prompt_for_consent` but shows a shorter, one-line prompt.
243    ///
244    /// This method is only available when both 'privacy' and 'cli' features are enabled.
245    #[cfg(all(feature = "privacy", feature = "cli"))]
246    pub fn prompt_minimal(mut self) -> Result<Self> {
247        use crate::privacy::PrivacyManager;
248
249        let service_name = self
250            .service_name
251            .as_ref()
252            .ok_or_else(|| TelemetryError::missing_field("service_name"))?
253            .clone();
254
255        // Create a privacy manager with current config
256        let config = self.privacy_config.clone().unwrap_or_default();
257        let manager = PrivacyManager::new(config.clone(), &service_name)?;
258
259        // Prompt for consent
260        let _consent_granted = manager.prompt_minimal(&service_name)?;
261
262        // Set consent_required to true to respect the user's choice
263        self.privacy_config = Some(PrivacyConfig {
264            consent_required: true,
265            ..config
266        });
267
268        Ok(self)
269    }
270
271    /// Build the TelemetryKit instance
272    pub fn build(self) -> Result<TelemetryKit> {
273        let service_name = self
274            .service_name
275            .ok_or_else(|| TelemetryError::missing_field("service_name"))?;
276
277        let service_version = self
278            .service_version
279            .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
280
281        // Determine database path
282        let db_path = if let Some(path) = self.db_path {
283            path
284        } else {
285            // Default: ~/.telemetry-kit/<service_name>.db
286            let mut path = dirs::home_dir()
287                .ok_or_else(|| TelemetryError::invalid_config(
288                    "database_path",
289                    "Cannot determine home directory. Please set an explicit database path with .db_path()"
290                ))?;
291            path.push(".telemetry-kit");
292            path.push(format!("{}.db", service_name));
293            path
294        };
295
296        TelemetryKit::new(
297            service_name,
298            service_version,
299            db_path,
300            #[cfg(feature = "sync")]
301            self.sync_config,
302            #[cfg(feature = "sync")]
303            self.auto_sync_enabled,
304            #[cfg(feature = "sync")]
305            self.auto_sync_config,
306            #[cfg(feature = "privacy")]
307            self.privacy_config,
308        )
309    }
310}