1use anyhow::Result;
6use colored::*;
7use std::collections::HashMap;
8use std::io::{self, Write};
9
10use crate::telemetry::{TelemetryConfig, TelemetryStore, TELEMETRY_INFO};
11
12pub fn telemetry_opt_in() -> Result<()> {
14 let mut config = TelemetryConfig::load()?;
15
16 if config.enabled {
17 println!(
18 "{} Telemetry is already {}",
19 "[OK]".green().bold(),
20 "enabled".green()
21 );
22 } else {
23 config.opt_in()?;
24 println!(
25 "{} Telemetry {} - thank you for helping improve Chasm!",
26 "[OK]".green().bold(),
27 "enabled".green().bold()
28 );
29 }
30
31 println!();
32 println!(
33 "To see what data is collected, run: {}",
34 "chasm telemetry info".cyan()
35 );
36
37 Ok(())
38}
39
40pub fn telemetry_opt_out() -> Result<()> {
42 let mut config = TelemetryConfig::load()?;
43
44 if !config.enabled {
45 println!(
46 "{} Telemetry is already {}",
47 "[OK]".green().bold(),
48 "disabled".yellow()
49 );
50 } else {
51 config.opt_out()?;
52 println!(
53 "{} Telemetry {} - no usage data will be collected",
54 "[OK]".green().bold(),
55 "disabled".yellow().bold()
56 );
57 }
58
59 println!();
60 println!(
61 "You can re-enable at any time with: {}",
62 "chasm telemetry opt-in".cyan()
63 );
64
65 Ok(())
66}
67
68pub fn telemetry_info() -> Result<()> {
70 let config = TelemetryConfig::load()?;
71
72 let status = if config.enabled {
73 "ENABLED (collecting anonymous data)".green().to_string()
74 } else {
75 "DISABLED (not collecting data)".yellow().to_string()
76 };
77
78 let info = TELEMETRY_INFO
79 .replace("{installation_id}", &config.installation_id)
80 .replace("{status}", &status);
81
82 println!("{}", "[TELEMETRY INFO]".cyan().bold());
83 println!("{}", info);
84
85 if let Some(changed_at) = config.preference_changed_at {
87 let dt = chrono::DateTime::from_timestamp(changed_at, 0)
88 .map(|d| d.format("%Y-%m-%d %H:%M:%S UTC").to_string())
89 .unwrap_or_else(|| "Unknown".to_string());
90 println!("Preference last changed: {}", dt.dimmed());
91 }
92
93 println!();
94 println!(
95 "Use {} to change your preference",
96 "chasm telemetry --help".cyan()
97 );
98
99 Ok(())
100}
101
102pub fn telemetry_reset() -> Result<()> {
104 let mut config = TelemetryConfig::load()?;
105 let old_id = config.installation_id.clone();
106
107 config.reset_id()?;
108
109 println!("{} Telemetry ID reset", "[OK]".green().bold());
110 println!();
111 println!("Old ID: {}", old_id.dimmed().strikethrough());
112 println!("New ID: {}", config.installation_id.green());
113
114 Ok(())
115}
116
117pub fn telemetry_record(
119 category: &str,
120 event: &str,
121 data_json: Option<&str>,
122 kv_pairs: &[(String, String)],
123 tags: Vec<String>,
124 context: Option<&str>,
125 verbose: bool,
126) -> Result<()> {
127 let store = TelemetryStore::new()?;
128
129 let mut data: HashMap<String, serde_json::Value> = if let Some(json_str) = data_json {
131 serde_json::from_str(json_str).map_err(|e| {
132 anyhow::anyhow!(
133 "Invalid JSON data: {}. Use valid JSON or --kv key=value pairs",
134 e
135 )
136 })?
137 } else {
138 HashMap::new()
139 };
140
141 for (key, value) in kv_pairs {
143 let json_value =
145 serde_json::from_str(value).unwrap_or(serde_json::Value::String(value.clone()));
146 data.insert(key.clone(), json_value);
147 }
148
149 let record = store.record(category, event, data, tags, context.map(|s| s.to_string()))?;
150
151 println!(
152 "{} Recorded telemetry event: {}",
153 "[OK]".green().bold(),
154 format!("{}:{}", record.category, record.event).cyan()
155 );
156
157 if verbose {
158 println!();
159 println!("Record ID: {}", record.id.dimmed());
160 println!("Timestamp: {}", record.timestamp_iso);
161 println!("Category: {}", record.category.cyan());
162 println!("Event: {}", record.event);
163 if !record.tags.is_empty() {
164 println!("Tags: {}", record.tags.join(", ").yellow());
165 }
166 if let Some(ctx) = &record.context {
167 println!("Context: {}", ctx);
168 }
169 if !record.data.is_empty() {
170 println!("Data:");
171 let pretty = serde_json::to_string_pretty(&record.data).unwrap_or_default();
172 for line in pretty.lines() {
173 println!(" {}", line);
174 }
175 }
176 }
177
178 Ok(())
179}
180
181pub fn telemetry_show(
183 category: Option<&str>,
184 event: Option<&str>,
185 tag: Option<&str>,
186 limit: usize,
187 format: &str,
188 after: Option<&str>,
189 before: Option<&str>,
190) -> Result<()> {
191 let store = TelemetryStore::new()?;
192
193 let after_ts = after.and_then(|d| parse_date_to_timestamp(d));
195 let before_ts = before.and_then(|d| parse_date_to_timestamp(d));
196
197 let records = store.read_records(category, event, tag, after_ts, before_ts, Some(limit))?;
198
199 if records.is_empty() {
200 println!("{} No telemetry records found", "[INFO]".cyan());
201 return Ok(());
202 }
203
204 match format {
205 "json" => {
206 let json = serde_json::to_string_pretty(&records)?;
207 println!("{}", json);
208 }
209 "jsonl" => {
210 for record in &records {
211 println!("{}", serde_json::to_string(record)?);
212 }
213 }
214 _ => {
215 println!(
217 "{} Showing {} telemetry records",
218 "[TELEMETRY]".cyan().bold(),
219 records.len().to_string().green()
220 );
221 println!();
222
223 for record in &records {
224 let time_short = &record.timestamp_iso[..19];
225 let tags_str = if record.tags.is_empty() {
226 String::new()
227 } else {
228 format!(" [{}]", record.tags.join(", ").yellow())
229 };
230
231 println!(
232 "{} {} {}{}",
233 time_short.dimmed(),
234 record.category.cyan(),
235 record.event.white().bold(),
236 tags_str
237 );
238
239 if !record.data.is_empty() {
240 let data_str = serde_json::to_string(&record.data).unwrap_or_default();
241 let display = if data_str.len() > 80 {
243 format!("{}...", &data_str[..77])
244 } else {
245 data_str
246 };
247 println!(" {}", display.dimmed());
248 }
249 }
250
251 let total = store.count_records()?;
252 if total > limit {
253 println!();
254 println!(
255 "Showing {} of {} total records. Use {} to see more.",
256 limit,
257 total,
258 "-n <limit>".cyan()
259 );
260 }
261 }
262 }
263
264 Ok(())
265}
266
267pub fn telemetry_export(
269 output: &str,
270 format: &str,
271 category: Option<&str>,
272 with_metadata: bool,
273) -> Result<()> {
274 let store = TelemetryStore::new()?;
275
276 let count = store.export_records(output, format, category, with_metadata)?;
277
278 if count == 0 {
279 println!("{} No records to export", "[INFO]".yellow());
280 } else {
281 println!(
282 "{} Exported {} records to {}",
283 "[OK]".green().bold(),
284 count.to_string().cyan(),
285 output.green()
286 );
287
288 if with_metadata {
289 println!(" Installation ID included in export");
290 }
291 }
292
293 Ok(())
294}
295
296pub fn telemetry_clear(force: bool, older_than: Option<u32>) -> Result<()> {
298 let store = TelemetryStore::new()?;
299 let count = store.count_records()?;
300
301 if count == 0 {
302 println!("{} No telemetry records to clear", "[INFO]".cyan());
303 return Ok(());
304 }
305
306 let message = if let Some(days) = older_than {
307 format!(
308 "Clear {} telemetry records older than {} days?",
309 count, days
310 )
311 } else {
312 format!("Clear all {} telemetry records?", count)
313 };
314
315 if !force {
316 print!("{} [y/N] ", message.yellow());
317 io::stdout().flush()?;
318
319 let mut input = String::new();
320 io::stdin().read_line(&mut input)?;
321
322 if !input.trim().eq_ignore_ascii_case("y") {
323 println!("Cancelled");
324 return Ok(());
325 }
326 }
327
328 let removed = store.clear_records(older_than)?;
329
330 println!(
331 "{} Cleared {} telemetry records",
332 "[OK]".green().bold(),
333 removed.to_string().cyan()
334 );
335
336 Ok(())
337}
338
339pub fn telemetry_config(
341 endpoint: Option<&str>,
342 api_key: Option<&str>,
343 enable_remote: bool,
344 disable_remote: bool,
345) -> Result<()> {
346 let mut config = TelemetryConfig::load()?;
347 let mut changed = false;
348
349 if let Some(ep) = endpoint {
350 config.set_remote_endpoint(Some(ep.to_string()))?;
351 println!(
352 "{} Remote endpoint set to: {}",
353 "[OK]".green().bold(),
354 ep.cyan()
355 );
356 changed = true;
357 }
358
359 if let Some(key) = api_key {
360 config.set_remote_api_key(Some(key.to_string()))?;
361 println!(
363 "{} API key configured ({})",
364 "[OK]".green().bold(),
365 format!("{}...", &key[..key.len().min(8)]).dimmed()
366 );
367 changed = true;
368 }
369
370 if enable_remote {
371 config.set_remote_enabled(true)?;
372 println!("{} Remote telemetry enabled", "[OK]".green().bold());
373 changed = true;
374 }
375
376 if disable_remote {
377 config.set_remote_enabled(false)?;
378 println!("{} Remote telemetry disabled", "[OK]".green().bold());
379 changed = true;
380 }
381
382 let config = TelemetryConfig::load()?;
384
385 if !changed {
386 println!("{}", "[REMOTE TELEMETRY CONFIG]".cyan().bold());
388 println!();
389 println!(
390 "Endpoint: {}",
391 config
392 .remote_endpoint
393 .as_deref()
394 .unwrap_or("(not configured)")
395 .cyan()
396 );
397 println!(
398 "API Key: {}",
399 if config.remote_api_key.is_some() {
400 "(configured)".green().to_string()
401 } else {
402 "(not configured)".yellow().to_string()
403 }
404 );
405 println!(
406 "Remote Send: {}",
407 if config.remote_enabled {
408 "ENABLED".green().bold().to_string()
409 } else {
410 "DISABLED".yellow().to_string()
411 }
412 );
413
414 if config.is_remote_enabled() {
415 println!();
416 println!(
417 "{} Ready to sync. Use {}",
418 "[✓]".green(),
419 "chasm telemetry sync".cyan()
420 );
421 } else if config.remote_endpoint.is_some() && config.remote_api_key.is_some() {
422 println!();
423 println!(
424 "{} Configured but disabled. Use {}",
425 "[!]".yellow(),
426 "--enable-remote".cyan()
427 );
428 } else {
429 println!();
430 println!("To configure:");
431 println!(
432 " {} {}",
433 "chasm telemetry config".cyan(),
434 "--endpoint <URL> --api-key <KEY> --enable-remote"
435 );
436 }
437 }
438
439 Ok(())
440}
441
442pub fn telemetry_sync(limit: Option<usize>, clear_after: bool) -> Result<()> {
444 let store = TelemetryStore::new()?;
445
446 let count = store.count_records()?;
447 if count == 0 {
448 println!("{} No telemetry records to sync", "[INFO]".cyan());
449 return Ok(());
450 }
451
452 println!(
453 "{} Syncing {} telemetry records to remote server...",
454 "[SYNC]".cyan().bold(),
455 limit.unwrap_or(count).min(count).to_string().green()
456 );
457
458 let result = store.sync_to_remote(limit)?;
459
460 if result.success {
461 println!(
462 "{} Successfully sent {} records",
463 "[OK]".green().bold(),
464 result.records_sent.to_string().cyan()
465 );
466
467 if clear_after && result.records_sent > 0 {
468 let cleared = store.clear_records(None)?;
469 println!(" Cleared {} local records", cleared.to_string().dimmed());
470 }
471 } else {
472 println!(
473 "{} Sync failed: {}",
474 "[ERROR]".red().bold(),
475 result.error.unwrap_or_else(|| "Unknown error".to_string())
476 );
477 }
478
479 Ok(())
480}
481
482pub fn telemetry_test() -> Result<()> {
484 let config = TelemetryConfig::load()?;
485
486 if config.remote_endpoint.is_none() {
487 println!("{} Remote endpoint not configured", "[ERROR]".red().bold());
488 println!(
489 " Use: {}",
490 "chasm telemetry config --endpoint <URL>".cyan()
491 );
492 return Ok(());
493 }
494
495 let endpoint = config.remote_endpoint.as_ref().unwrap();
496 println!(
497 "{} Testing connection to {}",
498 "[TEST]".cyan().bold(),
499 endpoint
500 );
501
502 let client = reqwest::blocking::Client::builder()
504 .timeout(std::time::Duration::from_secs(10))
505 .build()
506 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {}", e))?;
507
508 let health_url = format!("{}/health", endpoint.trim_end_matches('/'));
509 let response = client.get(&health_url).send();
510
511 match response {
512 Ok(resp) => {
513 if resp.status().is_success() {
514 let body: serde_json::Value = resp.json().unwrap_or_default();
515 println!("{} Connection successful!", "[OK]".green().bold());
516 println!();
517 println!(
518 "Server status: {}",
519 body.get("status")
520 .and_then(|v| v.as_str())
521 .unwrap_or("unknown")
522 .green()
523 );
524 if let Some(env) = body.get("environment").and_then(|v| v.as_str()) {
525 println!("Environment: {}", env);
526 }
527
528 if config.remote_api_key.is_some() {
530 println!();
531 println!("{} API key configured", "[✓]".green());
532 } else {
533 println!();
534 println!(
535 "{} API key not configured (required for syncing)",
536 "[!]".yellow()
537 );
538 }
539 } else {
540 println!(
541 "{} Server returned: HTTP {}",
542 "[WARN]".yellow().bold(),
543 resp.status()
544 );
545 }
546 }
547 Err(e) => {
548 println!("{} Connection failed: {}", "[ERROR]".red().bold(), e);
549 println!();
550 println!("Please check:");
551 println!(" • The endpoint URL is correct");
552 println!(" • The server is running");
553 println!(" • Your network connection");
554 }
555 }
556
557 Ok(())
558}
559
560fn parse_date_to_timestamp(date_str: &str) -> Option<i64> {
562 chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
563 .ok()
564 .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp())
565}