stynx_code_services/analytics/
mod.rs1use async_trait::async_trait;
2use stynx_code_errors::AppResult;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use tokio::io::AsyncWriteExt;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AnalyticsEvent {
10 pub name: String,
11 pub properties: HashMap<String, String>,
12 pub timestamp: u64,
13}
14
15#[async_trait]
16pub trait AnalyticsService: Send + Sync {
17 async fn track_event(&self, name: &str, properties: HashMap<String, String>) -> AppResult<()>;
18 async fn flush(&self) -> AppResult<()>;
19}
20
21pub struct LocalAnalytics {
22 path: PathBuf,
23}
24
25impl LocalAnalytics {
26 pub fn new() -> Self {
27 let path = home_claude_dir().join("analytics.jsonl");
28 Self { path }
29 }
30
31 pub fn with_path(path: PathBuf) -> Self {
32 Self { path }
33 }
34}
35
36fn home_claude_dir() -> PathBuf {
37 stynx_code_config::home_dir()
38 .unwrap_or_else(|| PathBuf::from("."))
39 .join(".claude")
40}
41
42fn now_secs() -> u64 {
43 std::time::SystemTime::now()
44 .duration_since(std::time::UNIX_EPOCH)
45 .unwrap_or_default()
46 .as_secs()
47}
48
49#[async_trait]
50impl AnalyticsService for LocalAnalytics {
51 async fn track_event(&self, name: &str, properties: HashMap<String, String>) -> AppResult<()> {
52 let event = AnalyticsEvent {
53 name: name.to_string(),
54 properties,
55 timestamp: now_secs(),
56 };
57
58 if let Some(parent) = self.path.parent() {
59 tokio::fs::create_dir_all(parent)
60 .await
61 .map_err(|e| -> stynx_code_errors::AppError {
62 anyhow::anyhow!("failed to create analytics dir: {e}").into()
63 })?;
64 }
65
66 let mut line = serde_json::to_string(&event)?;
67 line.push('\n');
68
69 let mut file = tokio::fs::OpenOptions::new()
70 .create(true)
71 .append(true)
72 .open(&self.path)
73 .await
74 .map_err(|e| -> stynx_code_errors::AppError {
75 anyhow::anyhow!("failed to open analytics file: {e}").into()
76 })?;
77
78 file.write_all(line.as_bytes())
79 .await
80 .map_err(|e| -> stynx_code_errors::AppError {
81 anyhow::anyhow!("failed to write analytics event: {e}").into()
82 })?;
83
84 tracing::info!(event_name = name, "tracked analytics event");
85 Ok(())
86 }
87
88 async fn flush(&self) -> AppResult<()> {
89
90 Ok(())
91 }
92}