1use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
2use chrono::{DateTime, Utc};
3use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
4use lazy_static::lazy_static;
5use parking_lot::Mutex;
6use serde::Serialize;
7use settings::Settings;
8use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
9use sysinfo::{
10 CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
11};
12use tempfile::NamedTempFile;
13use util::http::HttpClient;
14use util::{channel::ReleaseChannel, TryFutureExt};
15
16pub struct Telemetry {
17 http_client: Arc<dyn HttpClient>,
18 executor: BackgroundExecutor,
19 state: Mutex<TelemetryState>,
20}
21
22struct TelemetryState {
23 metrics_id: Option<Arc<str>>, // Per logged-in user
24 installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
25 session_id: Option<Arc<str>>, // Per app launch
26 release_channel: Option<&'static str>,
27 app_metadata: AppMetadata,
28 architecture: &'static str,
29 clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
30 flush_clickhouse_events_task: Option<Task<()>>,
31 log_file: Option<NamedTempFile>,
32 is_staff: Option<bool>,
33 first_event_datetime: Option<DateTime<Utc>>,
34}
35
36const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
37
38lazy_static! {
39 static ref CLICKHOUSE_EVENTS_URL: String =
40 format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
41}
42
43#[derive(Serialize, Debug)]
44struct ClickhouseEventRequestBody {
45 token: &'static str,
46 installation_id: Option<Arc<str>>,
47 session_id: Option<Arc<str>>,
48 is_staff: Option<bool>,
49 app_version: Option<String>,
50 os_name: &'static str,
51 os_version: Option<String>,
52 architecture: &'static str,
53 release_channel: Option<&'static str>,
54 events: Vec<ClickhouseEventWrapper>,
55}
56
57#[derive(Serialize, Debug)]
58struct ClickhouseEventWrapper {
59 signed_in: bool,
60 #[serde(flatten)]
61 event: ClickhouseEvent,
62}
63
64#[derive(Serialize, Debug)]
65#[serde(rename_all = "snake_case")]
66pub enum AssistantKind {
67 Panel,
68 Inline,
69}
70
71#[derive(Serialize, Debug)]
72#[serde(tag = "type")]
73pub enum ClickhouseEvent {
74 Editor {
75 operation: &'static str,
76 file_extension: Option<String>,
77 vim_mode: bool,
78 copilot_enabled: bool,
79 copilot_enabled_for_language: bool,
80 milliseconds_since_first_event: i64,
81 },
82 Copilot {
83 suggestion_id: Option<String>,
84 suggestion_accepted: bool,
85 file_extension: Option<String>,
86 milliseconds_since_first_event: i64,
87 },
88 Call {
89 operation: &'static str,
90 room_id: Option<u64>,
91 channel_id: Option<u64>,
92 milliseconds_since_first_event: i64,
93 },
94 Assistant {
95 conversation_id: Option<String>,
96 kind: AssistantKind,
97 model: &'static str,
98 milliseconds_since_first_event: i64,
99 },
100 Cpu {
101 usage_as_percentage: f32,
102 core_count: u32,
103 milliseconds_since_first_event: i64,
104 },
105 Memory {
106 memory_in_bytes: u64,
107 virtual_memory_in_bytes: u64,
108 milliseconds_since_first_event: i64,
109 },
110}
111
112// #[cfg(debug_assertions)]
113// const MAX_QUEUE_LEN: usize = 1;
114
115// #[cfg(not(debug_assertions))]
116const MAX_QUEUE_LEN: usize = 10;
117
118// #[cfg(debug_assertions)]
119// const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
120
121// #[cfg(not(debug_assertions))]
122const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
123
124impl Telemetry {
125 pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
126 let release_channel = if cx.has_global::<ReleaseChannel>() {
127 Some(cx.global::<ReleaseChannel>().display_name())
128 } else {
129 None
130 };
131 // TODO: Replace all hardware stuff with nested SystemSpecs json
132 let this = Arc::new(Self {
133 http_client: client,
134 executor: cx.background_executor().clone(),
135 state: Mutex::new(TelemetryState {
136 app_metadata: cx.app_metadata(),
137 architecture: env::consts::ARCH,
138 release_channel,
139 installation_id: None,
140 metrics_id: None,
141 session_id: None,
142 clickhouse_events_queue: Default::default(),
143 flush_clickhouse_events_task: Default::default(),
144 log_file: None,
145 is_staff: None,
146 first_event_datetime: None,
147 }),
148 });
149
150 this
151 }
152
153 pub fn log_file_path(&self) -> Option<PathBuf> {
154 Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
155 }
156
157 pub fn start(
158 self: &Arc<Self>,
159 installation_id: Option<String>,
160 session_id: String,
161 cx: &mut AppContext,
162 ) {
163 let mut state = self.state.lock();
164 state.installation_id = installation_id.map(|id| id.into());
165 state.session_id = Some(session_id.into());
166 let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
167 drop(state);
168
169 if has_clickhouse_events {
170 self.flush_clickhouse_events();
171 }
172
173 let this = self.clone();
174 cx.spawn(|cx| async move {
175 // Avoiding calling `System::new_all()`, as there have been crashes related to it
176 let refresh_kind = RefreshKind::new()
177 .with_memory() // For memory usage
178 .with_processes(ProcessRefreshKind::everything()) // For process usage
179 .with_cpu(CpuRefreshKind::everything()); // For core count
180
181 let mut system = System::new_with_specifics(refresh_kind);
182
183 // Avoiding calling `refresh_all()`, just update what we need
184 system.refresh_specifics(refresh_kind);
185
186 loop {
187 // Waiting some amount of time before the first query is important to get a reasonable value
188 // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
189 const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
190 smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
191
192 system.refresh_specifics(refresh_kind);
193
194 let current_process = Pid::from_u32(std::process::id());
195 let Some(process) = system.processes().get(¤t_process) else {
196 let process = current_process;
197 log::error!("Failed to find own process {process:?} in system process table");
198 // TODO: Fire an error telemetry event
199 return;
200 };
201
202 let telemetry_settings = if let Ok(telemetry_settings) =
203 cx.update(|cx| *TelemetrySettings::get_global(cx))
204 {
205 telemetry_settings
206 } else {
207 break;
208 };
209
210 this.report_memory_event(
211 telemetry_settings,
212 process.memory(),
213 process.virtual_memory(),
214 );
215 this.report_cpu_event(
216 telemetry_settings,
217 process.cpu_usage(),
218 system.cpus().len() as u32,
219 );
220 }
221 })
222 .detach();
223 }
224
225 pub fn set_authenticated_user_info(
226 self: &Arc<Self>,
227 metrics_id: Option<String>,
228 is_staff: bool,
229 cx: &AppContext,
230 ) {
231 if !TelemetrySettings::get_global(cx).metrics {
232 return;
233 }
234
235 let mut state = self.state.lock();
236 let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
237 state.metrics_id = metrics_id.clone();
238 state.is_staff = Some(is_staff);
239 drop(state);
240 }
241
242 pub fn report_editor_event(
243 self: &Arc<Self>,
244 telemetry_settings: TelemetrySettings,
245 file_extension: Option<String>,
246 vim_mode: bool,
247 operation: &'static str,
248 copilot_enabled: bool,
249 copilot_enabled_for_language: bool,
250 ) {
251 let event = ClickhouseEvent::Editor {
252 file_extension,
253 vim_mode,
254 operation,
255 copilot_enabled,
256 copilot_enabled_for_language,
257 milliseconds_since_first_event: self.milliseconds_since_first_event(),
258 };
259
260 self.report_clickhouse_event(event, telemetry_settings)
261 }
262
263 pub fn report_copilot_event(
264 self: &Arc<Self>,
265 telemetry_settings: TelemetrySettings,
266 suggestion_id: Option<String>,
267 suggestion_accepted: bool,
268 file_extension: Option<String>,
269 ) {
270 let event = ClickhouseEvent::Copilot {
271 suggestion_id,
272 suggestion_accepted,
273 file_extension,
274 milliseconds_since_first_event: self.milliseconds_since_first_event(),
275 };
276
277 self.report_clickhouse_event(event, telemetry_settings)
278 }
279
280 pub fn report_assistant_event(
281 self: &Arc<Self>,
282 telemetry_settings: TelemetrySettings,
283 conversation_id: Option<String>,
284 kind: AssistantKind,
285 model: &'static str,
286 ) {
287 let event = ClickhouseEvent::Assistant {
288 conversation_id,
289 kind,
290 model,
291 milliseconds_since_first_event: self.milliseconds_since_first_event(),
292 };
293
294 self.report_clickhouse_event(event, telemetry_settings)
295 }
296
297 pub fn report_call_event(
298 self: &Arc<Self>,
299 telemetry_settings: TelemetrySettings,
300 operation: &'static str,
301 room_id: Option<u64>,
302 channel_id: Option<u64>,
303 ) {
304 let event = ClickhouseEvent::Call {
305 operation,
306 room_id,
307 channel_id,
308 milliseconds_since_first_event: self.milliseconds_since_first_event(),
309 };
310
311 self.report_clickhouse_event(event, telemetry_settings)
312 }
313
314 pub fn report_cpu_event(
315 self: &Arc<Self>,
316 telemetry_settings: TelemetrySettings,
317 usage_as_percentage: f32,
318 core_count: u32,
319 ) {
320 let event = ClickhouseEvent::Cpu {
321 usage_as_percentage,
322 core_count,
323 milliseconds_since_first_event: self.milliseconds_since_first_event(),
324 };
325
326 self.report_clickhouse_event(event, telemetry_settings)
327 }
328
329 pub fn report_memory_event(
330 self: &Arc<Self>,
331 telemetry_settings: TelemetrySettings,
332 memory_in_bytes: u64,
333 virtual_memory_in_bytes: u64,
334 ) {
335 let event = ClickhouseEvent::Memory {
336 memory_in_bytes,
337 virtual_memory_in_bytes,
338 milliseconds_since_first_event: self.milliseconds_since_first_event(),
339 };
340
341 self.report_clickhouse_event(event, telemetry_settings)
342 }
343
344 fn milliseconds_since_first_event(&self) -> i64 {
345 let mut state = self.state.lock();
346 match state.first_event_datetime {
347 Some(first_event_datetime) => {
348 let now: DateTime<Utc> = Utc::now();
349 now.timestamp_millis() - first_event_datetime.timestamp_millis()
350 }
351 None => {
352 state.first_event_datetime = Some(Utc::now());
353 0
354 }
355 }
356 }
357
358 fn report_clickhouse_event(
359 self: &Arc<Self>,
360 event: ClickhouseEvent,
361 telemetry_settings: TelemetrySettings,
362 ) {
363 if !telemetry_settings.metrics {
364 return;
365 }
366
367 let mut state = self.state.lock();
368 let signed_in = state.metrics_id.is_some();
369 state
370 .clickhouse_events_queue
371 .push(ClickhouseEventWrapper { signed_in, event });
372
373 if state.installation_id.is_some() {
374 if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
375 drop(state);
376 self.flush_clickhouse_events();
377 } else {
378 let this = self.clone();
379 let executor = self.executor.clone();
380 state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
381 executor.timer(DEBOUNCE_INTERVAL).await;
382 this.flush_clickhouse_events();
383 }));
384 }
385 }
386 }
387
388 pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
389 self.state.lock().metrics_id.clone()
390 }
391
392 pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
393 self.state.lock().installation_id.clone()
394 }
395
396 pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
397 self.state.lock().is_staff
398 }
399
400 fn flush_clickhouse_events(self: &Arc<Self>) {
401 let mut state = self.state.lock();
402 state.first_event_datetime = None;
403 let mut events = mem::take(&mut state.clickhouse_events_queue);
404 state.flush_clickhouse_events_task.take();
405 drop(state);
406
407 let this = self.clone();
408 self.executor
409 .spawn(
410 async move {
411 let mut json_bytes = Vec::new();
412
413 if let Some(file) = &mut this.state.lock().log_file {
414 let file = file.as_file_mut();
415 for event in &mut events {
416 json_bytes.clear();
417 serde_json::to_writer(&mut json_bytes, event)?;
418 file.write_all(&json_bytes)?;
419 file.write(b"\n")?;
420 }
421 }
422
423 {
424 let state = this.state.lock();
425 let request_body = ClickhouseEventRequestBody {
426 token: ZED_SECRET_CLIENT_TOKEN,
427 installation_id: state.installation_id.clone(),
428 session_id: state.session_id.clone(),
429 is_staff: state.is_staff.clone(),
430 app_version: state
431 .app_metadata
432 .app_version
433 .map(|version| version.to_string()),
434 os_name: state.app_metadata.os_name,
435 os_version: state
436 .app_metadata
437 .os_version
438 .map(|version| version.to_string()),
439 architecture: state.architecture,
440
441 release_channel: state.release_channel,
442 events,
443 };
444 json_bytes.clear();
445 serde_json::to_writer(&mut json_bytes, &request_body)?;
446 }
447
448 this.http_client
449 .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
450 .await?;
451 anyhow::Ok(())
452 }
453 .log_err(),
454 )
455 .detach();
456 }
457}