1use 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#[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 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn service_name(mut self, name: impl Into<String>) -> Result<Self> {
45 let name_str = name.into();
46
47 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 pub fn service_version(mut self, version: impl Into<String>) -> Self {
64 self.service_version = Some(version.into());
65 self
66 }
67
68 pub fn db_path(mut self, path: impl Into<PathBuf>) -> Self {
70 self.db_path = Some(path.into());
71 self
72 }
73
74 #[cfg(feature = "sync")]
76 pub fn sync(mut self, config: SyncConfig) -> Self {
77 self.sync_config = Some(config);
78 self
79 }
80
81 #[cfg(feature = "sync")]
83 pub fn auto_sync(mut self, enabled: bool) -> Self {
84 self.auto_sync_enabled = enabled;
85 self
86 }
87
88 #[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 #[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 #[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 #[cfg(feature = "privacy")]
124 pub fn privacy(mut self, config: PrivacyConfig) -> Self {
125 self.privacy_config = Some(config);
126 self
127 }
128
129 #[cfg(feature = "privacy")]
131 pub fn strict_privacy(mut self) -> Self {
132 self.privacy_config = Some(PrivacyConfig::strict());
133 self
134 }
135
136 #[cfg(feature = "privacy")]
138 pub fn minimal_privacy(mut self) -> Self {
139 self.privacy_config = Some(PrivacyConfig::minimal());
140 self
141 }
142
143 #[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 #[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 #[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 #[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 #[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 let config = self.privacy_config.clone().unwrap_or_default();
226 let manager = PrivacyManager::new(config.clone(), &service_name)?;
227
228 let _consent_granted = manager.prompt_for_consent(&service_name, service_version)?;
230
231 self.privacy_config = Some(PrivacyConfig {
233 consent_required: true,
234 ..config
235 });
236
237 Ok(self)
238 }
239
240 #[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 let config = self.privacy_config.clone().unwrap_or_default();
257 let manager = PrivacyManager::new(config.clone(), &service_name)?;
258
259 let _consent_granted = manager.prompt_minimal(&service_name)?;
261
262 self.privacy_config = Some(PrivacyConfig {
264 consent_required: true,
265 ..config
266 });
267
268 Ok(self)
269 }
270
271 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 let db_path = if let Some(path) = self.db_path {
283 path
284 } else {
285 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}