telemetry.rs

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