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