telemetry.rs

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