1use crate::http::HttpClient;
2use db::Db;
3use gpui::{
4 executor::Background,
5 serde_json::{self, value::Map, Value},
6 AppContext, Task,
7};
8use isahc::Request;
9use lazy_static::lazy_static;
10use parking_lot::Mutex;
11use serde::Serialize;
12use serde_json::json;
13use settings::ReleaseChannel;
14use std::{
15 io::Write,
16 mem,
17 path::PathBuf,
18 sync::Arc,
19 time::{Duration, SystemTime, UNIX_EPOCH},
20};
21use tempfile::NamedTempFile;
22use util::{post_inc, ResultExt, TryFutureExt};
23use uuid::Uuid;
24
25pub struct Telemetry {
26 http_client: Arc<dyn HttpClient>,
27 executor: Arc<Background>,
28 state: Mutex<TelemetryState>,
29}
30
31#[derive(Default)]
32struct TelemetryState {
33 metrics_id: Option<Arc<str>>,
34 device_id: Option<Arc<str>>,
35 app: &'static str,
36 app_version: Option<Arc<str>>,
37 release_channel: Option<&'static str>,
38 os_version: Option<Arc<str>>,
39 os_name: &'static str,
40 queue: Vec<MixpanelEvent>,
41 next_event_id: usize,
42 flush_task: Option<Task<()>>,
43 log_file: Option<NamedTempFile>,
44}
45
46const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
47const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
48
49lazy_static! {
50 static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
51 .ok()
52 .or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
53}
54
55#[derive(Serialize, Debug)]
56struct MixpanelEvent {
57 event: String,
58 properties: MixpanelEventProperties,
59}
60
61#[derive(Serialize, Debug)]
62struct MixpanelEventProperties {
63 // Mixpanel required fields
64 #[serde(skip_serializing_if = "str::is_empty")]
65 token: &'static str,
66 time: u128,
67 distinct_id: Option<Arc<str>>,
68 #[serde(rename = "$insert_id")]
69 insert_id: usize,
70 // Custom fields
71 #[serde(skip_serializing_if = "Option::is_none", flatten)]
72 event_properties: Option<Map<String, Value>>,
73 #[serde(rename = "OS Name")]
74 os_name: &'static str,
75 #[serde(rename = "OS Version")]
76 os_version: Option<Arc<str>>,
77 #[serde(rename = "Release Channel")]
78 release_channel: Option<&'static str>,
79 #[serde(rename = "App Version")]
80 app_version: Option<Arc<str>>,
81 #[serde(rename = "Signed In")]
82 signed_in: bool,
83 #[serde(rename = "App")]
84 app: &'static str,
85}
86
87#[derive(Serialize)]
88struct MixpanelEngageRequest {
89 #[serde(rename = "$token")]
90 token: &'static str,
91 #[serde(rename = "$distinct_id")]
92 distinct_id: Arc<str>,
93 #[serde(rename = "$set")]
94 set: Value,
95}
96
97#[cfg(debug_assertions)]
98const MAX_QUEUE_LEN: usize = 1;
99
100#[cfg(not(debug_assertions))]
101const MAX_QUEUE_LEN: usize = 10;
102
103#[cfg(debug_assertions)]
104const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
105
106#[cfg(not(debug_assertions))]
107const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
108
109impl Telemetry {
110 pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
111 let platform = cx.platform();
112 let release_channel = if cx.has_global::<ReleaseChannel>() {
113 Some(cx.global::<ReleaseChannel>().name())
114 } else {
115 None
116 };
117 let this = Arc::new(Self {
118 http_client: client,
119 executor: cx.background().clone(),
120 state: Mutex::new(TelemetryState {
121 os_version: platform.os_version().ok().map(|v| v.to_string().into()),
122 os_name: platform.os_name().into(),
123 app: "Zed",
124 app_version: platform.app_version().ok().map(|v| v.to_string().into()),
125 release_channel,
126 device_id: None,
127 metrics_id: None,
128 queue: Default::default(),
129 flush_task: Default::default(),
130 next_event_id: 0,
131 log_file: None,
132 }),
133 });
134
135 if MIXPANEL_TOKEN.is_some() {
136 this.executor
137 .spawn({
138 let this = this.clone();
139 async move {
140 if let Some(tempfile) = NamedTempFile::new().log_err() {
141 this.state.lock().log_file = Some(tempfile);
142 }
143 }
144 })
145 .detach();
146 }
147
148 this
149 }
150
151 pub fn log_file_path(&self) -> Option<PathBuf> {
152 Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
153 }
154
155 pub fn start(self: &Arc<Self>, db: Db) {
156 let this = self.clone();
157 self.executor
158 .spawn(
159 async move {
160 let (device_id, is_first_time_start) =
161 if let Ok(Some(device_id)) = db.read_kvp("device_id") {
162 (device_id, false)
163 } else {
164 let device_id = Uuid::new_v4().to_string();
165 db.write_kvp("device_id", &device_id)?;
166 (device_id, true)
167 };
168
169 this.report_start_app(is_first_time_start);
170
171 let device_id: Arc<str> = device_id.into();
172 let mut state = this.state.lock();
173 state.device_id = Some(device_id.clone());
174 for event in &mut state.queue {
175 event
176 .properties
177 .distinct_id
178 .get_or_insert_with(|| device_id.clone());
179 }
180 if !state.queue.is_empty() {
181 drop(state);
182 this.flush();
183 }
184
185 anyhow::Ok(())
186 }
187 .log_err(),
188 )
189 .detach();
190 }
191
192 pub fn set_authenticated_user_info(
193 self: &Arc<Self>,
194 metrics_id: Option<String>,
195 is_staff: bool,
196 ) {
197 let this = self.clone();
198 let mut state = self.state.lock();
199 let device_id = state.device_id.clone();
200 let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
201 state.metrics_id = metrics_id.clone();
202 drop(state);
203
204 if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
205 self.executor
206 .spawn(
207 async move {
208 let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
209 token,
210 distinct_id: device_id,
211 set: json!({ "Staff": is_staff, "ID": metrics_id }),
212 }])?;
213 let request = Request::post(MIXPANEL_ENGAGE_URL)
214 .header("Content-Type", "application/json")
215 .body(json_bytes.into())?;
216 this.http_client.send(request).await?;
217 Ok(())
218 }
219 .log_err(),
220 )
221 .detach();
222 }
223 }
224
225 pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
226 let mut state = self.state.lock();
227 let event = MixpanelEvent {
228 event: kind.to_string(),
229 properties: MixpanelEventProperties {
230 token: "",
231 time: SystemTime::now()
232 .duration_since(UNIX_EPOCH)
233 .unwrap()
234 .as_millis(),
235 distinct_id: state.device_id.clone(),
236 insert_id: post_inc(&mut state.next_event_id),
237 event_properties: if let Value::Object(properties) = properties {
238 Some(properties)
239 } else {
240 None
241 },
242 os_name: state.os_name,
243 os_version: state.os_version.clone(),
244 release_channel: state.release_channel,
245 app_version: state.app_version.clone(),
246 signed_in: state.metrics_id.is_some(),
247 app: state.app,
248 },
249 };
250 state.queue.push(event);
251 if state.device_id.is_some() {
252 if state.queue.len() >= MAX_QUEUE_LEN {
253 drop(state);
254 self.flush();
255 } else {
256 let this = self.clone();
257 let executor = self.executor.clone();
258 state.flush_task = Some(self.executor.spawn(async move {
259 executor.timer(DEBOUNCE_INTERVAL).await;
260 this.flush();
261 }));
262 }
263 }
264 }
265
266 pub fn report_start_app(self: &Arc<Self>, is_first_time_start: bool) {
267 self.report_event(
268 "Start App",
269 json!({ "is_first_time_start": is_first_time_start }),
270 )
271 }
272
273 fn flush(self: &Arc<Self>) {
274 let mut state = self.state.lock();
275 let mut events = mem::take(&mut state.queue);
276 state.flush_task.take();
277 drop(state);
278
279 if let Some(token) = MIXPANEL_TOKEN.as_ref() {
280 let this = self.clone();
281 self.executor
282 .spawn(
283 async move {
284 let mut json_bytes = Vec::new();
285
286 if let Some(file) = &mut this.state.lock().log_file {
287 let file = file.as_file_mut();
288 for event in &mut events {
289 json_bytes.clear();
290 serde_json::to_writer(&mut json_bytes, event)?;
291 file.write_all(&json_bytes)?;
292 file.write(b"\n")?;
293
294 event.properties.token = token;
295 }
296 }
297
298 json_bytes.clear();
299 serde_json::to_writer(&mut json_bytes, &events)?;
300 let request = Request::post(MIXPANEL_EVENTS_URL)
301 .header("Content-Type", "application/json")
302 .body(json_bytes.into())?;
303 this.http_client.send(request).await?;
304 Ok(())
305 }
306 .log_err(),
307 )
308 .detach();
309 }
310 }
311}