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