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