syncable_cli/telemetry/
client.rs1use 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
10const 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 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 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 let handle = tokio::spawn(async move {
59 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 let pending_tasks_clone = pending_tasks.clone();
100 tokio::spawn(async move {
101 pending_tasks_clone.lock().await.push(handle);
102 });
103 }
104
105 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 pub fn track_vulnerabilities(&self, properties: HashMap<String, serde_json::Value>) {
128 self.track_event("Vulnerability Scan", properties);
129 }
130
131 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 pub fn track_security_scan(&self) {
142 }
144
145 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 }
153
154 pub async fn flush(&self) {
156 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 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 tokio::time::sleep(Duration::from_millis(500)).await;
171 }
172}