1use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
2use gpui::{executor::Background, serde_json, AppContext, Task};
3use lazy_static::lazy_static;
4use parking_lot::Mutex;
5use serde::Serialize;
6use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
7use sysinfo::{
8 CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
9};
10use tempfile::NamedTempFile;
11use util::http::HttpClient;
12use util::{channel::ReleaseChannel, TryFutureExt};
13
14pub struct Telemetry {
15 http_client: Arc<dyn HttpClient>,
16 executor: Arc<Background>,
17 state: Mutex<TelemetryState>,
18}
19
20#[derive(Default)]
21struct TelemetryState {
22 metrics_id: Option<Arc<str>>, // Per logged-in user
23 installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
24 session_id: Option<Arc<str>>, // Per app launch
25 app_version: Option<Arc<str>>,
26 release_channel: Option<&'static str>,
27 os_name: &'static str,
28 os_version: Option<Arc<str>>,
29 architecture: &'static str,
30 clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
31 flush_clickhouse_events_task: Option<Task<()>>,
32 log_file: Option<NamedTempFile>,
33 is_staff: Option<bool>,
34}
35
36const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
37
38lazy_static! {
39 static ref CLICKHOUSE_EVENTS_URL: String =
40 format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
41}
42
43#[derive(Serialize, Debug)]
44struct ClickhouseEventRequestBody {
45 token: &'static str,
46 installation_id: Option<Arc<str>>,
47 session_id: Option<Arc<str>>,
48 is_staff: Option<bool>,
49 app_version: Option<Arc<str>>,
50 os_name: &'static str,
51 os_version: Option<Arc<str>>,
52 architecture: &'static str,
53 release_channel: Option<&'static str>,
54 events: Vec<ClickhouseEventWrapper>,
55}
56
57#[derive(Serialize, Debug)]
58struct ClickhouseEventWrapper {
59 signed_in: bool,
60 #[serde(flatten)]
61 event: ClickhouseEvent,
62}
63
64#[derive(Serialize, Debug)]
65#[serde(rename_all = "snake_case")]
66pub enum AssistantKind {
67 Panel,
68 Inline,
69}
70
71#[derive(Serialize, Debug)]
72#[serde(tag = "type")]
73pub enum ClickhouseEvent {
74 Editor {
75 operation: &'static str,
76 file_extension: Option<String>,
77 vim_mode: bool,
78 copilot_enabled: bool,
79 copilot_enabled_for_language: bool,
80 },
81 Copilot {
82 suggestion_id: Option<String>,
83 suggestion_accepted: bool,
84 file_extension: Option<String>,
85 },
86 Call {
87 operation: &'static str,
88 room_id: Option<u64>,
89 channel_id: Option<u64>,
90 },
91 Assistant {
92 conversation_id: Option<String>,
93 kind: AssistantKind,
94 model: &'static str,
95 },
96 Cpu {
97 usage_as_percentage: f32,
98 core_count: u32,
99 },
100 Memory {
101 memory_in_bytes: u64,
102 virtual_memory_in_bytes: u64,
103 },
104}
105
106#[cfg(debug_assertions)]
107const MAX_QUEUE_LEN: usize = 1;
108
109#[cfg(not(debug_assertions))]
110const MAX_QUEUE_LEN: usize = 10;
111
112#[cfg(debug_assertions)]
113const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
114
115#[cfg(not(debug_assertions))]
116const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
117
118impl Telemetry {
119 pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
120 let platform = cx.platform();
121 let release_channel = if cx.has_global::<ReleaseChannel>() {
122 Some(cx.global::<ReleaseChannel>().display_name())
123 } else {
124 None
125 };
126 // TODO: Replace all hardware stuff with nested SystemSpecs json
127 let this = Arc::new(Self {
128 http_client: client,
129 executor: cx.background().clone(),
130 state: Mutex::new(TelemetryState {
131 os_name: platform.os_name().into(),
132 os_version: platform.os_version().ok().map(|v| v.to_string().into()),
133 architecture: env::consts::ARCH,
134 app_version: platform.app_version().ok().map(|v| v.to_string().into()),
135 release_channel,
136 installation_id: None,
137 metrics_id: None,
138 session_id: None,
139 clickhouse_events_queue: Default::default(),
140 flush_clickhouse_events_task: Default::default(),
141 log_file: None,
142 is_staff: None,
143 }),
144 });
145
146 this
147 }
148
149 pub fn log_file_path(&self) -> Option<PathBuf> {
150 Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
151 }
152
153 pub fn start(
154 self: &Arc<Self>,
155 installation_id: Option<String>,
156 session_id: String,
157 cx: &mut AppContext,
158 ) {
159 let mut state = self.state.lock();
160 state.installation_id = installation_id.map(|id| id.into());
161 state.session_id = Some(session_id.into());
162 let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
163 drop(state);
164
165 if has_clickhouse_events {
166 self.flush_clickhouse_events();
167 }
168
169 let this = self.clone();
170 cx.spawn(|mut cx| async move {
171 // Avoiding calling `System::new_all()`, as there have been crashes related to it
172 let refresh_kind = RefreshKind::new()
173 .with_memory() // For memory usage
174 .with_processes(ProcessRefreshKind::everything()) // For process usage
175 .with_cpu(CpuRefreshKind::everything()); // For core count
176
177 let mut system = System::new_with_specifics(refresh_kind);
178
179 // Avoiding calling `refresh_all()`, just update what we need
180 system.refresh_specifics(refresh_kind);
181
182 loop {
183 // Waiting some amount of time before the first query is important to get a reasonable value
184 // https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
185 const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
186 smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
187
188 system.refresh_specifics(refresh_kind);
189
190 let current_process = Pid::from_u32(std::process::id());
191 let Some(process) = system.processes().get(¤t_process) else {
192 let process = current_process;
193 log::error!("Failed to find own process {process:?} in system process table");
194 // TODO: Fire an error telemetry event
195 return;
196 };
197
198 let memory_event = ClickhouseEvent::Memory {
199 memory_in_bytes: process.memory(),
200 virtual_memory_in_bytes: process.virtual_memory(),
201 };
202
203 let cpu_event = ClickhouseEvent::Cpu {
204 usage_as_percentage: process.cpu_usage(),
205 core_count: system.cpus().len() as u32,
206 };
207
208 let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
209
210 this.report_clickhouse_event(memory_event, telemetry_settings);
211 this.report_clickhouse_event(cpu_event, telemetry_settings);
212 }
213 })
214 .detach();
215 }
216
217 pub fn set_authenticated_user_info(
218 self: &Arc<Self>,
219 metrics_id: Option<String>,
220 is_staff: bool,
221 cx: &AppContext,
222 ) {
223 if !settings::get::<TelemetrySettings>(cx).metrics {
224 return;
225 }
226
227 let mut state = self.state.lock();
228 let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
229 state.metrics_id = metrics_id.clone();
230 state.is_staff = Some(is_staff);
231 drop(state);
232 }
233
234 pub fn report_clickhouse_event(
235 self: &Arc<Self>,
236 event: ClickhouseEvent,
237 telemetry_settings: TelemetrySettings,
238 ) {
239 if !telemetry_settings.metrics {
240 return;
241 }
242
243 let mut state = self.state.lock();
244 let signed_in = state.metrics_id.is_some();
245 state
246 .clickhouse_events_queue
247 .push(ClickhouseEventWrapper { signed_in, event });
248
249 if state.installation_id.is_some() {
250 if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
251 drop(state);
252 self.flush_clickhouse_events();
253 } else {
254 let this = self.clone();
255 let executor = self.executor.clone();
256 state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
257 executor.timer(DEBOUNCE_INTERVAL).await;
258 this.flush_clickhouse_events();
259 }));
260 }
261 }
262 }
263
264 pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
265 self.state.lock().metrics_id.clone()
266 }
267
268 pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
269 self.state.lock().installation_id.clone()
270 }
271
272 pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
273 self.state.lock().is_staff
274 }
275
276 fn flush_clickhouse_events(self: &Arc<Self>) {
277 let mut state = self.state.lock();
278 let mut events = mem::take(&mut state.clickhouse_events_queue);
279 state.flush_clickhouse_events_task.take();
280 drop(state);
281
282 let this = self.clone();
283 self.executor
284 .spawn(
285 async move {
286 let mut json_bytes = Vec::new();
287
288 if let Some(file) = &mut this.state.lock().log_file {
289 let file = file.as_file_mut();
290 for event in &mut events {
291 json_bytes.clear();
292 serde_json::to_writer(&mut json_bytes, event)?;
293 file.write_all(&json_bytes)?;
294 file.write(b"\n")?;
295 }
296 }
297
298 {
299 let state = this.state.lock();
300 let request_body = ClickhouseEventRequestBody {
301 token: ZED_SECRET_CLIENT_TOKEN,
302 installation_id: state.installation_id.clone(),
303 session_id: state.session_id.clone(),
304 is_staff: state.is_staff.clone(),
305 app_version: state.app_version.clone(),
306 os_name: state.os_name,
307 os_version: state.os_version.clone(),
308 architecture: state.architecture,
309
310 release_channel: state.release_channel,
311 events,
312 };
313 json_bytes.clear();
314 serde_json::to_writer(&mut json_bytes, &request_body)?;
315 }
316
317 this.http_client
318 .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
319 .await?;
320 anyhow::Ok(())
321 }
322 .log_err(),
323 )
324 .detach();
325 }
326}