diff --git a/Cargo.lock b/Cargo.lock index cfe18755dd..17ace3f47b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -959,6 +959,7 @@ dependencies = [ "serde", "smol", "sum_tree", + "tempfile", "thiserror", "time 0.3.11", "tiny_http", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index f61fa1c787..c9c783c659 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -35,6 +35,7 @@ tiny_http = "0.8" uuid = { version = "1.1.2", features = ["v4"] } url = "2.2" serde = { version = "*", features = ["derive"] } +tempfile = "3" [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 5d6bef5c23..0670add1af 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -33,6 +33,7 @@ use std::{ convert::TryFrom, fmt::Write as _, future::Future, + path::PathBuf, sync::{Arc, Weak}, time::{Duration, Instant}, }; @@ -332,10 +333,11 @@ impl Client { log::info!("set status on client {}: {:?}", self.id, status); let mut state = self.state.write(); *state.status.0.borrow_mut() = status; + let user_id = state.credentials.as_ref().map(|c| c.user_id); match status { Status::Connected { .. } => { - self.telemetry.set_user_id(self.user_id()); + self.telemetry.set_user_id(user_id); state._reconnect_task = None; } Status::ConnectionLost => { @@ -364,7 +366,7 @@ impl Client { })); } Status::SignedOut | Status::UpgradeRequired => { - self.telemetry.set_user_id(self.user_id()); + self.telemetry.set_user_id(user_id); state._reconnect_task.take(); } _ => {} @@ -1060,6 +1062,10 @@ impl Client { pub fn report_event(&self, kind: &str, properties: Value) { self.telemetry.report_event(kind, properties) } + + pub fn telemetry_log_file_path(&self) -> Option { + self.telemetry.log_file_path() + } } impl AnyWeakEntityHandle { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index f048dfdd49..77aa308f30 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -10,15 +10,18 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use std::{ + io::Write, mem, + path::PathBuf, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; +use tempfile::NamedTempFile; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; pub struct Telemetry { - client: Arc, + http_client: Arc, executor: Arc, session_id: u128, state: Mutex, @@ -34,6 +37,7 @@ struct TelemetryState { queue: Vec, next_event_id: usize, flush_task: Option>, + log_file: Option, } const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch"; @@ -52,10 +56,13 @@ struct AmplitudeEventBatch { #[derive(Serialize)] struct AmplitudeEvent { + #[serde(skip_serializing_if = "Option::is_none")] user_id: Option>, device_id: Option>, event_type: String, + #[serde(skip_serializing_if = "Option::is_none")] event_properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] user_properties: Option>, os_name: &'static str, os_version: Option>, @@ -80,8 +87,8 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); impl Telemetry { pub fn new(client: Arc, cx: &AppContext) -> Arc { let platform = cx.platform(); - Arc::new(Self { - client, + let this = Arc::new(Self { + http_client: client, executor: cx.background().clone(), session_id: SystemTime::now() .duration_since(UNIX_EPOCH) @@ -101,9 +108,29 @@ impl Telemetry { queue: Default::default(), flush_task: Default::default(), next_event_id: 0, + log_file: None, user_id: None, }), - }) + }); + + if AMPLITUDE_API_KEY.is_some() { + this.executor + .spawn({ + let this = this.clone(); + async move { + if let Some(tempfile) = NamedTempFile::new().log_err() { + this.state.lock().log_file = Some(tempfile); + } + } + }) + .detach(); + } + + this + } + + pub fn log_file_path(&self) -> Option { + Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } pub fn start(self: &Arc, db: Arc) { @@ -189,23 +216,39 @@ impl Telemetry { } } - fn flush(&self) { + fn flush(self: &Arc) { let mut state = self.state.lock(); let events = mem::take(&mut state.queue); state.flush_task.take(); + drop(state); if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() { - let client = self.client.clone(); + let this = self.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(()) - }) + .spawn( + async move { + let mut json_bytes = Vec::new(); + + if let Some(file) = &mut this.state.lock().log_file { + let file = file.as_file_mut(); + for event in &events { + json_bytes.clear(); + serde_json::to_writer(&mut json_bytes, event)?; + file.write_all(&json_bytes)?; + file.write(b"\n")?; + } + } + + let batch = AmplitudeEventBatch { api_key, events }; + json_bytes.clear(); + serde_json::to_writer(&mut json_bytes, &batch)?; + let request = + Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?; + this.http_client.send(request).await?; + Ok(()) + } + .log_err(), + ) .detach(); } } diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 3a34166ba6..f21845a589 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -332,6 +332,11 @@ pub fn menus() -> Vec> { action: Box::new(command_palette::Toggle), }, MenuItem::Separator, + MenuItem::Action { + name: "View Telemetry Log", + action: Box::new(crate::OpenTelemetryLog), + }, + MenuItem::Separator, MenuItem::Action { name: "Documentation", action: Box::new(crate::OpenBrowser { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cd906500ee..407f101421 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -56,6 +56,7 @@ actions!( DebugElements, OpenSettings, OpenLog, + OpenTelemetryLog, OpenKeymap, OpenDefaultSettings, OpenDefaultKeymap, @@ -146,6 +147,12 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { open_log_file(workspace, app_state.clone(), cx); } }); + cx.add_action({ + let app_state = app_state.clone(); + move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext| { + open_telemetry_log_file(workspace, app_state.clone(), cx); + } + }); cx.add_action({ let app_state = app_state.clone(); move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { @@ -504,6 +511,53 @@ fn open_log_file( }); } +fn open_telemetry_log_file( + workspace: &mut Workspace, + app_state: Arc, + cx: &mut ViewContext, +) { + workspace.with_local_workspace(cx, app_state.clone(), |_, cx| { + cx.spawn_weak(|workspace, mut cx| async move { + let workspace = workspace.upgrade(&cx)?; + let path = app_state.client.telemetry_log_file_path()?; + let log = app_state.fs.load(&path).await.log_err()?; + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| { + buffer.set_language(app_state.languages.get_language("JSON"), cx); + buffer.edit( + [( + 0..0, + concat!( + "// Zed collects anonymous usage data to help us understand how people are using the app.\n", + "// After the beta release, we'll provide the ability to opt out of this telemetry.\n", + "\n" + ), + )], + None, + cx, + ); + buffer.edit([(buffer.len()..buffer.len(), log)], None, cx); + }); + + let buffer = cx.add_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) + }); + workspace.add_item( + Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + cx, + ); + }); + + Some(()) + }) + .detach(); + }); +} + fn open_bundled_config_file( workspace: &mut Workspace, app_state: Arc,