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