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