diff --git a/Cargo.lock b/Cargo.lock index bf145f0cee..cfe18755dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -945,6 +945,7 @@ dependencies = [ "async-recursion", "async-tungstenite", "collections", + "db", "futures", "gpui", "image", @@ -963,6 +964,7 @@ dependencies = [ "tiny_http", "url", "util", + "uuid 1.1.2", ] [[package]] @@ -6346,6 +6348,9 @@ name = "uuid" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom 0.2.7", +] [[package]] name = "valuable" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 5fcff565bb..f61fa1c787 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -12,6 +12,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo [dependencies] collections = { path = "../collections" } +db = { path = "../db" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } @@ -31,6 +32,7 @@ smol = "1.2.5" thiserror = "1.0.29" time = { version = "0.3", features = ["serde", "serde-well-known"] } tiny_http = "0.8" +uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" serde = { version = "*", features = ["derive"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 3d85aea3c5..5d6bef5c23 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -12,6 +12,7 @@ use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, }; +use db::Db; use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt}; use gpui::{ actions, @@ -70,7 +71,7 @@ pub fn init(client: Arc, cx: &mut MutableAppContext) { cx.add_global_action({ let client = client.clone(); move |_: &TestTelemetry, _| { - client.log_event( + client.report_event( "test_telemetry", json!({ "test_property": "test_value" @@ -334,6 +335,7 @@ impl Client { match status { Status::Connected { .. } => { + self.telemetry.set_user_id(self.user_id()); state._reconnect_task = None; } Status::ConnectionLost => { @@ -362,6 +364,7 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { + self.telemetry.set_user_id(self.user_id()); state._reconnect_task.take(); } _ => {} @@ -619,7 +622,7 @@ impl Client { credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); if read_from_keychain { - self.log_event("read_credentials_from_keychain", Default::default()); + self.report_event("read credentials from keychain", Default::default()); } } if credentials.is_none() { @@ -983,7 +986,7 @@ impl Client { .context("failed to decrypt access token")?; platform.activate(true); - telemetry.log_event("authenticate_with_browser", Default::default()); + telemetry.report_event("authenticate with browser", Default::default()); Ok(Credentials { user_id: user_id.parse()?, @@ -1050,8 +1053,12 @@ impl Client { self.peer.respond_with_error(receipt, error) } - pub fn log_event(&self, kind: &str, properties: Value) { - self.telemetry.log_event(kind, properties) + pub fn start_telemetry(&self, db: Arc) { + self.telemetry.start(db); + } + + pub fn report_event(&self, kind: &str, properties: Value) { + self.telemetry.report_event(kind, properties) } } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 7eea13a923..63da4eae5c 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,10 +1,12 @@ -use crate::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; +use crate::http::HttpClient; +use db::Db; use gpui::{ executor::Background, serde_json::{self, value::Map, Value}, AppContext, Task, }; use isahc::Request; +use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{ @@ -12,7 +14,8 @@ use std::{ sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use util::{post_inc, ResultExt}; +use util::{post_inc, ResultExt, TryFutureExt}; +use uuid::Uuid; pub struct Telemetry { client: Arc, @@ -33,7 +36,13 @@ struct TelemetryState { flush_task: Option>, } -const AMPLITUDE_EVENTS_URL: &'static str = "https//api2.amplitude.com/batch"; +const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch"; + +lazy_static! { + static ref AMPLITUDE_API_KEY: Option = option_env!("AMPLITUDE_API_KEY") + .map(|key| key.to_string()) + .or(std::env::var("AMPLITUDE_API_KEY").ok()); +} #[derive(Serialize)] struct AmplitudeEventBatch { @@ -62,6 +71,10 @@ const MAX_QUEUE_LEN: usize = 1; #[cfg(not(debug_assertions))] const MAX_QUEUE_LEN: usize = 10; +#[cfg(debug_assertions)] +const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); + +#[cfg(not(debug_assertions))] const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); impl Telemetry { @@ -93,7 +106,52 @@ impl Telemetry { }) } - pub fn log_event(self: &Arc, kind: &str, properties: Value) { + pub fn start(self: &Arc, db: Arc) { + let this = self.clone(); + self.executor + .spawn( + async move { + let device_id = if let Some(device_id) = db + .read(["device_id"])? + .into_iter() + .flatten() + .next() + .and_then(|bytes| String::from_utf8(bytes).ok()) + { + device_id + } else { + let device_id = Uuid::new_v4().to_string(); + db.write([("device_id", device_id.as_bytes())])?; + device_id + }; + + let device_id = Some(Arc::from(device_id)); + let mut state = this.state.lock(); + state.device_id = device_id.clone(); + for event in &mut state.queue { + event.device_id = device_id.clone(); + } + if !state.queue.is_empty() { + drop(state); + this.flush(); + } + + anyhow::Ok(()) + } + .log_err(), + ) + .detach(); + } + + pub fn set_user_id(&self, user_id: Option) { + self.state.lock().user_id = user_id.map(|id| id.to_string().into()); + } + + pub fn report_event(self: &Arc, kind: &str, properties: Value) { + if AMPLITUDE_API_KEY.is_none() { + return; + } + let mut state = self.state.lock(); let event = AmplitudeEvent { event_type: kind.to_string(), @@ -116,38 +174,39 @@ impl Telemetry { event_id: post_inc(&mut state.next_event_id), }; state.queue.push(event); - if state.queue.len() >= MAX_QUEUE_LEN { - drop(state); - self.flush(); - } else { - let this = self.clone(); - let executor = self.executor.clone(); - state.flush_task = Some(self.executor.spawn(async move { - executor.timer(DEBOUNCE_INTERVAL).await; - this.flush(); - })); + if state.device_id.is_some() { + if state.queue.len() >= MAX_QUEUE_LEN { + drop(state); + self.flush(); + } else { + let this = self.clone(); + let executor = self.executor.clone(); + state.flush_task = Some(self.executor.spawn(async move { + executor.timer(DEBOUNCE_INTERVAL).await; + this.flush(); + })); + } } } fn flush(&self) { let mut state = self.state.lock(); let events = mem::take(&mut state.queue); - let client = self.client.clone(); state.flush_task.take(); - self.executor - .spawn(async move { - let body = serde_json::to_vec(&AmplitudeEventBatch { - api_key: ZED_SECRET_CLIENT_TOKEN, - events, + + if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() { + let client = self.client.clone(); + self.executor + .spawn(async move { + let batch = AmplitudeEventBatch { api_key, events }; + let body = serde_json::to_vec(&batch).log_err()?; + let request = Request::post(AMPLITUDE_EVENTS_URL) + .body(body.into()) + .log_err()?; + client.send(request).await.log_err(); + Some(()) }) - .log_err()?; - let request = Request::post(AMPLITUDE_EVENTS_URL) - .header("Content-Type", "application/json") - .body(body.into()) - .log_err()?; - client.send(request).await.log_err(); - Some(()) - }) - .detach(); + .detach(); + } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c60abc187a..07a9fc011f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -29,6 +29,7 @@ use gpui::{ geometry::vector::{vec2f, Vector2F}, impl_actions, impl_internal_actions, platform::CursorStyle, + serde_json::json, text_layout, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -1053,6 +1054,7 @@ impl Editor { let editor_created_event = EditorCreated(cx.handle()); cx.emit_global(editor_created_event); + this.report_event("open editor", cx); this } @@ -5928,6 +5930,25 @@ impl Editor { }) .collect() } + + fn report_event(&self, name: &str, cx: &AppContext) { + if let Some((project, file)) = self.project.as_ref().zip( + self.buffer + .read(cx) + .as_singleton() + .and_then(|b| b.read(cx).file()), + ) { + project.read(cx).client().report_event( + name, + json!({ + "file_extension": file + .path() + .extension() + .and_then(|e| e.to_str()) + }), + ); + } + } } impl EditorSnapshot { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fb6f12a16f..6c004f2007 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -410,6 +410,7 @@ impl Item for Editor { let buffers = buffer.read(cx).all_buffers(); let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); + self.report_event("save editor", cx); cx.spawn(|_, mut cx| async move { let transaction = futures::select_biased! { _ = timeout => { diff --git a/crates/zed/build.rs b/crates/zed/build.rs index e39946876e..0ffa2397b0 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -3,6 +3,10 @@ use std::process::Command; fn main() { println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14"); + if let Ok(api_key) = std::env::var("AMPLITUDE_API_KEY") { + println!("cargo:rustc-env=AMPLITUDE_API_KEY={api_key}"); + } + let output = Command::new("npm") .current_dir("../../styles") .args(["install", "--no-save"]) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index bb913ab610..2dd90eb762 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -121,7 +121,6 @@ fn main() { vim::init(cx); terminal::init(cx); - let db = cx.background().block(db); cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) .detach(); @@ -139,6 +138,10 @@ fn main() { }) .detach(); + let db = cx.background().block(db); + client.start_telemetry(db.clone()); + client.report_event("start app", Default::default()); + let project_store = cx.add_model(|_| ProjectStore::new(db.clone())); let app_state = Arc::new(AppState { languages,