telemetry.rs

  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(&current_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}