telemetry.rs

  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}