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