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