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