syncable_cli/telemetry/
client.rs

1use crate::config::types::Config;
2use crate::telemetry::user::UserId;
3use reqwest::Client;
4use serde_json::json;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::sync::Mutex;
9
10// PostHog API endpoint and key - Using EU endpoint to match your successful curl test
11const POSTHOG_API_ENDPOINT: &str = "https://eu.i.posthog.com/capture/";
12const POSTHOG_PROJECT_API_KEY: &str = "phc_t5zrCHU3yiU52lcUfOP3SiCSxdhJcmB2I3m06dGTk2D";
13
14pub struct TelemetryClient {
15    user_id: UserId,
16    http_client: Client,
17    pending_tasks: Arc<Mutex<Vec<tokio::task::JoinHandle<()>>>>,
18}
19
20impl TelemetryClient {
21    pub async fn new(_config: &Config) -> Result<Self, Box<dyn std::error::Error>> {
22        let user_id = UserId::load_or_create()?;
23        let http_client = Client::new();
24        let pending_tasks = Arc::new(Mutex::new(Vec::new()));
25
26        Ok(Self {
27            user_id,
28            http_client,
29            pending_tasks,
30        })
31    }
32
33    // Helper function to create common properties
34    fn create_common_properties(&self) -> HashMap<String, serde_json::Value> {
35        let mut properties = HashMap::new();
36        properties.insert("version".to_string(), json!(env!("CARGO_PKG_VERSION")));
37        properties.insert("os".to_string(), json!(std::env::consts::OS));
38        properties.insert("personal_id".to_string(), json!(rand::random::<u32>()));
39        properties.insert("distinct_id".to_string(), json!(self.user_id.id.clone()));
40        properties
41    }
42
43    pub fn track_event(&self, name: &str, properties: HashMap<String, serde_json::Value>) {
44        let mut event_properties = self.create_common_properties();
45
46        // Merge provided properties
47        for (key, value) in properties {
48            event_properties.insert(key, value);
49        }
50
51        let event_name = name.to_string();
52        let client = self.http_client.clone();
53        let pending_tasks = self.pending_tasks.clone();
54
55        log::debug!("Tracking event: {}", event_name);
56
57        // Send the event asynchronously
58        let handle = tokio::spawn(async move {
59            // Create the event payload according to PostHog API
60            let payload = json!({
61                "api_key": POSTHOG_PROJECT_API_KEY,
62                "event": event_name,
63                "properties": event_properties,
64                "timestamp": chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
65            });
66
67            log::debug!("Sending telemetry payload: {:?}", payload);
68
69            match client
70                .post(POSTHOG_API_ENDPOINT)
71                .json(&payload)
72                .send()
73                .await
74            {
75                Ok(response) => {
76                    if response.status().is_success() {
77                        log::debug!("Successfully sent telemetry event: {}", event_name);
78                    } else {
79                        let status = response.status();
80                        let body = response
81                            .text()
82                            .await
83                            .unwrap_or_else(|_| "Unknown error".to_string());
84                        log::warn!(
85                            "Failed to send telemetry event '{}': HTTP {} - {}",
86                            event_name,
87                            status,
88                            body
89                        );
90                    }
91                }
92                Err(e) => {
93                    log::warn!("Failed to send telemetry event '{}': {}", event_name, e);
94                }
95            }
96        });
97
98        // Keep track of the task
99        let pending_tasks_clone = pending_tasks.clone();
100        tokio::spawn(async move {
101            pending_tasks_clone.lock().await.push(handle);
102        });
103    }
104
105    // Specific methods for the actual commands
106    pub fn track_analyze(&self, properties: HashMap<String, serde_json::Value>) {
107        self.track_event("analyze", properties);
108    }
109
110    pub fn track_generate(&self, properties: HashMap<String, serde_json::Value>) {
111        self.track_event("generate", properties);
112    }
113
114    pub fn track_validate(&self, properties: HashMap<String, serde_json::Value>) {
115        self.track_event("validate", properties);
116    }
117
118    pub fn track_support(&self, properties: HashMap<String, serde_json::Value>) {
119        self.track_event("support", properties);
120    }
121
122    pub fn track_dependencies(&self, properties: HashMap<String, serde_json::Value>) {
123        self.track_event("dependencies", properties);
124    }
125
126    // Updated to accept properties
127    pub fn track_vulnerabilities(&self, properties: HashMap<String, serde_json::Value>) {
128        self.track_event("Vulnerability Scan", properties);
129    }
130
131    // Updated to accept properties
132    pub fn track_security(&self, properties: HashMap<String, serde_json::Value>) {
133        self.track_event("Security Scan", properties);
134    }
135
136    pub fn track_tools(&self, properties: HashMap<String, serde_json::Value>) {
137        self.track_event("tools", properties);
138    }
139
140    // Existing specific methods for events
141    pub fn track_security_scan(&self) {
142        // Deprecated: Use track_security with properties instead
143    }
144
145    // Updated to accept properties
146    pub fn track_analyze_folder(&self, properties: HashMap<String, serde_json::Value>) {
147        self.track_event("Analyze Folder", properties);
148    }
149
150    pub fn track_vulnerability_scan(&self) {
151        // Deprecated: Use track_vulnerabilities with properties instead
152    }
153
154    // Flush method to ensure all events are sent before the program exits
155    pub async fn flush(&self) {
156        // Collect all pending tasks
157        let mut tasks = Vec::new();
158        {
159            let mut pending_tasks = self.pending_tasks.lock().await;
160            tasks.extend(pending_tasks.drain(..));
161        }
162
163        // Wait for all tasks to complete
164        if !tasks.is_empty() {
165            log::debug!("Waiting for {} telemetry tasks to complete", tasks.len());
166            futures_util::future::join_all(tasks).await;
167        }
168
169        // Give a bit more time for network requests to complete
170        tokio::time::sleep(Duration::from_millis(500)).await;
171    }
172}