1use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
2use db::kvp::KEY_VALUE_STORE;
3use gpui::{executor::Background, serde_json, AppContext, Task};
4use lazy_static::lazy_static;
5use parking_lot::Mutex;
6use serde::Serialize;
7use std::{
8 env,
9 io::Write,
10 mem,
11 path::PathBuf,
12 sync::Arc,
13 time::{Duration, SystemTime, UNIX_EPOCH},
14};
15use tempfile::NamedTempFile;
16use util::http::HttpClient;
17use util::{channel::ReleaseChannel, TryFutureExt};
18use uuid::Uuid;
19
20pub struct Telemetry {
21 http_client: Arc<dyn HttpClient>,
22 executor: Arc<Background>,
23 state: Mutex<TelemetryState>,
24}
25
26#[derive(Default)]
27struct TelemetryState {
28 metrics_id: Option<Arc<str>>, // Per logged-in user
29 installation_id: Option<Arc<str>>, // Per app installation
30 app_version: Option<Arc<str>>,
31 release_channel: Option<&'static str>,
32 os_name: &'static str,
33 os_version: Option<Arc<str>>,
34 architecture: &'static str,
35 clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
36 flush_clickhouse_events_task: Option<Task<()>>,
37 log_file: Option<NamedTempFile>,
38 is_staff: Option<bool>,
39}
40
41const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
42
43lazy_static! {
44 static ref CLICKHOUSE_EVENTS_URL: String =
45 format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
46}
47
48#[derive(Serialize, Debug)]
49struct ClickhouseEventRequestBody {
50 token: &'static str,
51 installation_id: Option<Arc<str>>,
52 app_version: Option<Arc<str>>,
53 os_name: &'static str,
54 os_version: Option<Arc<str>>,
55 architecture: &'static str,
56 release_channel: Option<&'static str>,
57 events: Vec<ClickhouseEventWrapper>,
58}
59
60#[derive(Serialize, Debug)]
61struct ClickhouseEventWrapper {
62 time: u128,
63 signed_in: bool,
64 #[serde(flatten)]
65 event: ClickhouseEvent,
66}
67
68#[derive(Serialize, Debug)]
69#[serde(tag = "type")]
70pub enum ClickhouseEvent {
71 Editor {
72 operation: &'static str,
73 file_extension: Option<String>,
74 vim_mode: bool,
75 copilot_enabled: bool,
76 copilot_enabled_for_language: bool,
77 },
78 Copilot {
79 suggestion_id: Option<String>,
80 suggestion_accepted: bool,
81 file_extension: Option<String>,
82 },
83}
84
85#[cfg(debug_assertions)]
86const MAX_QUEUE_LEN: usize = 1;
87
88#[cfg(not(debug_assertions))]
89const MAX_QUEUE_LEN: usize = 10;
90
91#[cfg(debug_assertions)]
92const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
93
94#[cfg(not(debug_assertions))]
95const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
96
97impl Telemetry {
98 pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
99 let platform = cx.platform();
100 let release_channel = if cx.has_global::<ReleaseChannel>() {
101 Some(cx.global::<ReleaseChannel>().display_name())
102 } else {
103 None
104 };
105 // TODO: Replace all hardware stuff with nested SystemSpecs json
106 let this = Arc::new(Self {
107 http_client: client,
108 executor: cx.background().clone(),
109 state: Mutex::new(TelemetryState {
110 os_name: platform.os_name().into(),
111 os_version: platform.os_version().ok().map(|v| v.to_string().into()),
112 architecture: env::consts::ARCH,
113 app_version: platform.app_version().ok().map(|v| v.to_string().into()),
114 release_channel,
115 installation_id: None,
116 metrics_id: None,
117 clickhouse_events_queue: Default::default(),
118 flush_clickhouse_events_task: Default::default(),
119 log_file: None,
120 is_staff: None,
121 }),
122 });
123
124 this
125 }
126
127 pub fn log_file_path(&self) -> Option<PathBuf> {
128 Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
129 }
130
131 pub fn start(self: &Arc<Self>) {
132 let this = self.clone();
133 self.executor
134 .spawn(
135 async move {
136 let installation_id =
137 if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
138 installation_id
139 } else {
140 let installation_id = Uuid::new_v4().to_string();
141 KEY_VALUE_STORE
142 .write_kvp("device_id".to_string(), installation_id.clone())
143 .await?;
144 installation_id
145 };
146
147 let installation_id: Arc<str> = installation_id.into();
148 let mut state = this.state.lock();
149 state.installation_id = Some(installation_id.clone());
150
151 let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
152
153 drop(state);
154
155 if has_clickhouse_events {
156 this.flush_clickhouse_events();
157 }
158
159 anyhow::Ok(())
160 }
161 .log_err(),
162 )
163 .detach();
164 }
165
166 /// This method takes the entire TelemetrySettings struct in order to force client code
167 /// to pull the struct out of the settings global. Do not remove!
168 pub fn set_authenticated_user_info(
169 self: &Arc<Self>,
170 metrics_id: Option<String>,
171 is_staff: bool,
172 cx: &AppContext,
173 ) {
174 if !settings::get::<TelemetrySettings>(cx).metrics {
175 return;
176 }
177
178 let mut state = self.state.lock();
179 let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
180 state.metrics_id = metrics_id.clone();
181 state.is_staff = Some(is_staff);
182 drop(state);
183 }
184
185 pub fn report_clickhouse_event(
186 self: &Arc<Self>,
187 event: ClickhouseEvent,
188 telemetry_settings: TelemetrySettings,
189 ) {
190 if !telemetry_settings.metrics {
191 return;
192 }
193
194 let mut state = self.state.lock();
195 let signed_in = state.metrics_id.is_some();
196 state.clickhouse_events_queue.push(ClickhouseEventWrapper {
197 time: SystemTime::now()
198 .duration_since(UNIX_EPOCH)
199 .unwrap()
200 .as_millis(),
201 signed_in,
202 event,
203 });
204
205 if state.installation_id.is_some() {
206 if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
207 drop(state);
208 self.flush_clickhouse_events();
209 } else {
210 let this = self.clone();
211 let executor = self.executor.clone();
212 state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
213 executor.timer(DEBOUNCE_INTERVAL).await;
214 this.flush_clickhouse_events();
215 }));
216 }
217 }
218 }
219
220 pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
221 self.state.lock().metrics_id.clone()
222 }
223
224 pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
225 self.state.lock().installation_id.clone()
226 }
227
228 pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
229 self.state.lock().is_staff
230 }
231
232 fn flush_clickhouse_events(self: &Arc<Self>) {
233 let mut state = self.state.lock();
234 let mut events = mem::take(&mut state.clickhouse_events_queue);
235 state.flush_clickhouse_events_task.take();
236 drop(state);
237
238 let this = self.clone();
239 self.executor
240 .spawn(
241 async move {
242 let mut json_bytes = Vec::new();
243
244 if let Some(file) = &mut this.state.lock().log_file {
245 let file = file.as_file_mut();
246 for event in &mut events {
247 json_bytes.clear();
248 serde_json::to_writer(&mut json_bytes, event)?;
249 file.write_all(&json_bytes)?;
250 file.write(b"\n")?;
251 }
252 }
253
254 {
255 let state = this.state.lock();
256 json_bytes.clear();
257 serde_json::to_writer(
258 &mut json_bytes,
259 &ClickhouseEventRequestBody {
260 token: ZED_SECRET_CLIENT_TOKEN,
261 installation_id: state.installation_id.clone(),
262 app_version: state.app_version.clone(),
263 os_name: state.os_name,
264 os_version: state.os_version.clone(),
265 architecture: state.architecture,
266
267 release_channel: state.release_channel,
268 events,
269 },
270 )?;
271 }
272
273 this.http_client
274 .post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
275 .await?;
276 anyhow::Ok(())
277 }
278 .log_err(),
279 )
280 .detach();
281 }
282}