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