Cargo.lock 🔗
@@ -959,6 +959,7 @@ dependencies = [
"serde",
"smol",
"sum_tree",
+ "tempfile",
"thiserror",
"time 0.3.11",
"tiny_http",
Max Brunsfeld and Joseph Lyons created
Co-authored-by: Joseph Lyons <joseph@zed.dev>
Cargo.lock | 1
crates/client/Cargo.toml | 1
crates/client/src/client.rs | 10 +++
crates/client/src/telemetry.rs | 73 ++++++++++++++++++++++++++++-------
crates/zed/src/menus.rs | 5 ++
crates/zed/src/zed.rs | 54 ++++++++++++++++++++++++++
6 files changed, 127 insertions(+), 17 deletions(-)
@@ -959,6 +959,7 @@ dependencies = [
"serde",
"smol",
"sum_tree",
+ "tempfile",
"thiserror",
"time 0.3.11",
"tiny_http",
@@ -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"] }
@@ -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<PathBuf> {
+ self.telemetry.log_file_path()
+ }
}
impl AnyWeakEntityHandle {
@@ -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<dyn HttpClient>,
+ http_client: Arc<dyn HttpClient>,
executor: Arc<Background>,
session_id: u128,
state: Mutex<TelemetryState>,
@@ -34,6 +37,7 @@ struct TelemetryState {
queue: Vec<AmplitudeEvent>,
next_event_id: usize,
flush_task: Option<Task<()>>,
+ log_file: Option<NamedTempFile>,
}
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<Arc<str>>,
device_id: Option<Arc<str>>,
event_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
event_properties: Option<Map<String, Value>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
user_properties: Option<Map<String, Value>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
@@ -80,8 +87,8 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
impl Telemetry {
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
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<PathBuf> {
+ Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
}
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
@@ -189,23 +216,39 @@ impl Telemetry {
}
}
- fn flush(&self) {
+ fn flush(self: &Arc<Self>) {
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();
}
}
@@ -332,6 +332,11 @@ pub fn menus() -> Vec<Menu<'static>> {
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 {
@@ -56,6 +56,7 @@ actions!(
DebugElements,
OpenSettings,
OpenLog,
+ OpenTelemetryLog,
OpenKeymap,
OpenDefaultSettings,
OpenDefaultKeymap,
@@ -146,6 +147,12 @@ pub fn init(app_state: &Arc<AppState>, 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<Workspace>| {
+ 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<Workspace>| {
@@ -504,6 +511,53 @@ fn open_log_file(
});
}
+fn open_telemetry_log_file(
+ workspace: &mut Workspace,
+ app_state: Arc<AppState>,
+ cx: &mut ViewContext<Workspace>,
+) {
+ 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<AppState>,