Skip to main content

stynx_code_services/analytics/
mod.rs

1use 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}