Detailed changes
@@ -79,6 +79,13 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
+ // Control what info Zed sends to our servers
+ "telemetry": {
+ // Send debug info like crash reports.
+ "diagnostics": true,
+ // Send anonymized usage data like what languages you're using Zed with.
+ "metrics": true
+ },
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -25,6 +25,7 @@ use postage::watch;
use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize;
+use settings::{Settings, TelemetrySettings};
use std::{
any::TypeId,
collections::HashMap,
@@ -423,7 +424,9 @@ impl Client {
}));
}
Status::SignedOut | Status::UpgradeRequired => {
- self.telemetry.set_authenticated_user_info(None, false);
+ let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
+ self.telemetry
+ .set_authenticated_user_info(None, false, telemetry_settings);
state._reconnect_task.take();
}
_ => {}
@@ -706,7 +709,13 @@ impl Client {
credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some();
if read_from_keychain {
- self.report_event("read credentials from keychain", Default::default());
+ cx.read(|cx| {
+ self.report_event(
+ "read credentials from keychain",
+ Default::default(),
+ cx.global::<Settings>().telemetry(),
+ );
+ });
}
}
if credentials.is_none() {
@@ -997,6 +1006,8 @@ impl Client {
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
+ let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
+
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
@@ -1079,7 +1090,11 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
- telemetry.report_event("authenticate with browser", Default::default());
+ telemetry.report_event(
+ "authenticate with browser",
+ Default::default(),
+ metrics_enabled,
+ );
Ok(Credentials {
user_id: user_id.parse()?,
@@ -1287,8 +1302,14 @@ impl Client {
self.telemetry.start();
}
- pub fn report_event(&self, kind: &str, properties: Value) {
- self.telemetry.report_event(kind, properties.clone());
+ pub fn report_event(
+ &self,
+ kind: &str,
+ properties: Value,
+ telemetry_settings: TelemetrySettings,
+ ) {
+ self.telemetry
+ .report_event(kind, properties.clone(), telemetry_settings);
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
@@ -10,6 +10,7 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
+use settings::TelemetrySettings;
use std::{
io::Write,
mem,
@@ -184,11 +185,18 @@ impl Telemetry {
.detach();
}
+ /// This method takes the entire TelemetrySettings struct in order to force client code
+ /// to pull the struct out of the settings global. Do not remove!
pub fn set_authenticated_user_info(
self: &Arc<Self>,
metrics_id: Option<String>,
is_staff: bool,
+ telemetry_settings: TelemetrySettings,
) {
+ if !telemetry_settings.metrics() {
+ return;
+ }
+
let this = self.clone();
let mut state = self.state.lock();
let device_id = state.device_id.clone();
@@ -221,7 +229,16 @@ impl Telemetry {
}
}
- pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
+ pub fn report_event(
+ self: &Arc<Self>,
+ kind: &str,
+ properties: Value,
+ telemetry_settings: TelemetrySettings,
+ ) {
+ if !telemetry_settings.metrics() {
+ return;
+ }
+
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.to_string(),
@@ -5,6 +5,7 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
+use settings::Settings;
use std::sync::{Arc, Weak};
use util::TryFutureExt as _;
@@ -141,14 +142,11 @@ impl UserStore {
let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
- if let Some(info) = info {
- client.telemetry.set_authenticated_user_info(
- Some(info.metrics_id.clone()),
- info.staff,
- );
- } else {
- client.telemetry.set_authenticated_user_info(None, false);
- }
+ client.telemetry.set_authenticated_user_info(
+ info.as_ref().map(|info| info.metrics_id.clone()),
+ info.as_ref().map(|info| info.staff).unwrap_or(false),
+ cx.read(|cx| cx.global::<Settings>().telemetry()),
+ );
current_user_tx.send(user).await.ok();
}
@@ -18,6 +18,7 @@ use rand::{
distributions::{Alphanumeric, DistString},
prelude::*,
};
+use settings::Settings;
use std::{env, ffi::OsStr, path::PathBuf, sync::Arc};
#[gpui::test(iterations = 100)]
@@ -104,6 +105,8 @@ async fn test_random_collaboration(
cx.function_name.clone(),
);
+ client_cx.update(|cx| cx.set_global(Settings::test(cx)));
+
let op_start_signal = futures::channel::mpsc::unbounded();
let client = server.create_client(&mut client_cx, &username).await;
user_ids.push(client.current_user_id(&client_cx));
@@ -173,6 +176,7 @@ async fn test_random_collaboration(
available_users.push((removed_user_id, client.username.clone()));
client_cx.update(|cx| {
cx.clear_globals();
+ cx.set_global(Settings::test(cx));
drop(client);
});
@@ -401,6 +405,7 @@ async fn test_random_collaboration(
for (client, mut cx) in clients {
cx.update(|cx| {
cx.clear_globals();
+ cx.set_global(Settings::test(cx));
drop(client);
});
}
@@ -6087,10 +6087,11 @@ impl Editor {
let extension = Path::new(file.file_name(cx))
.extension()
.and_then(|e| e.to_str());
- project
- .read(cx)
- .client()
- .report_event(name, json!({ "File Extension": extension }));
+ project.read(cx).client().report_event(
+ name,
+ json!({ "File Extension": extension }),
+ cx.global::<Settings>().telemetry(),
+ );
}
}
}
@@ -51,9 +51,26 @@ pub struct Settings {
pub language_overrides: HashMap<Arc<str>, EditorSettings>,
pub lsp: HashMap<Arc<str>, LspSettings>,
pub theme: Arc<Theme>,
+ pub telemetry_defaults: TelemetrySettings,
+ pub telemetry_overrides: TelemetrySettings,
pub staff_mode: bool,
}
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TelemetrySettings {
+ diagnostics: Option<bool>,
+ metrics: Option<bool>,
+}
+
+impl TelemetrySettings {
+ pub fn metrics(&self) -> bool {
+ self.metrics.unwrap()
+ }
+ pub fn diagnostics(&self) -> bool {
+ self.diagnostics.unwrap()
+ }
+}
+
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct FeatureFlags {
pub experimental_themes: bool,
@@ -302,6 +319,8 @@ pub struct SettingsFileContent {
#[serde(default)]
pub theme: Option<String>,
#[serde(default)]
+ pub telemetry: TelemetrySettings,
+ #[serde(default)]
pub staff_mode: Option<bool>,
}
@@ -312,6 +331,7 @@ pub struct LspSettings {
}
impl Settings {
+ /// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults(
assets: impl AssetSource,
font_cache: &FontCache,
@@ -363,11 +383,13 @@ impl Settings {
language_overrides: Default::default(),
lsp: defaults.lsp.clone(),
theme: themes.get(&defaults.theme.unwrap()).unwrap(),
-
+ telemetry_defaults: defaults.telemetry,
+ telemetry_overrides: Default::default(),
staff_mode: false,
}
}
+ // Fill out the overrride and etc. settings from the user's settings.json
pub fn set_user_settings(
&mut self,
data: SettingsFileContent,
@@ -419,6 +441,7 @@ impl Settings {
self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
self.terminal_overrides = data.terminal;
self.language_overrides = data.languages;
+ self.telemetry_overrides = data.telemetry;
self.lsp = data.lsp;
}
@@ -489,6 +512,27 @@ impl Settings {
.unwrap_or_else(|| R::default())
}
+ pub fn telemetry(&self) -> TelemetrySettings {
+ TelemetrySettings {
+ diagnostics: Some(self.telemetry_diagnostics()),
+ metrics: Some(self.telemetry_metrics()),
+ }
+ }
+
+ pub fn telemetry_diagnostics(&self) -> bool {
+ self.telemetry_overrides
+ .diagnostics
+ .or(self.telemetry_defaults.diagnostics)
+ .expect("missing default")
+ }
+
+ pub fn telemetry_metrics(&self) -> bool {
+ self.telemetry_overrides
+ .metrics
+ .or(self.telemetry_defaults.metrics)
+ .expect("missing default")
+ }
+
pub fn terminal_scroll(&self) -> AlternateScroll {
self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.as_ref())
}
@@ -540,6 +584,11 @@ impl Settings {
lsp: Default::default(),
projects_online_by_default: true,
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
+ telemetry_defaults: TelemetrySettings {
+ diagnostics: Some(true),
+ metrics: Some(true),
+ },
+ telemetry_overrides: Default::default(),
staff_mode: false,
}
}
@@ -470,7 +470,7 @@ mod tests {
use super::*;
use crate::{
dock,
- item::test::TestItem,
+ item::{self, test::TestItem},
persistence::model::{
SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
},
@@ -492,7 +492,7 @@ mod tests {
Settings::test_async(cx);
cx.update(|cx| {
- register_deserializable_item::<TestItem>(cx);
+ register_deserializable_item::<item::test::TestItem>(cx);
});
let serialized_workspace = SerializedWorkspace {
@@ -508,7 +508,7 @@ mod tests {
children: vec![SerializedItem {
active: true,
item_id: 0,
- kind: "test".into(),
+ kind: "TestItem".into(),
}],
},
left_sidebar_open: false,
@@ -919,7 +919,7 @@ pub(crate) mod test {
}
fn serialized_item_kind() -> Option<&'static str> {
- None
+ Some("TestItem")
}
fn deserialize(
@@ -18,7 +18,7 @@ use futures::{
channel::{mpsc, oneshot},
FutureExt, SinkExt, StreamExt,
};
-use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
+use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
use isahc::{config::Configurable, Request};
use language::LanguageRegistry;
use log::LevelFilter;
@@ -50,10 +50,13 @@ fn main() {
log::info!("========== starting zed ==========");
let mut app = gpui::App::new(Assets).unwrap();
+
let app_version = ZED_APP_VERSION
.or_else(|| app.platform().app_version().ok())
.map_or("dev".to_string(), |v| v.to_string());
- init_panic_hook(app_version, http.clone(), app.background());
+ init_panic_hook(app_version);
+
+ app.background();
load_embedded_fonts(&app);
@@ -61,7 +64,6 @@ fn main() {
let themes = ThemeRegistry::new(Assets, app.font_cache());
let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
-
let config_files = load_config_files(&app, fs.clone());
let login_shell_env_loaded = if stdout_is_a_pty() {
@@ -88,15 +90,6 @@ fn main() {
cx.set_global(*RELEASE_CHANNEL);
cx.set_global(HomeDir(paths::HOME.to_path_buf()));
- let client = client::Client::new(http.clone(), cx);
- let mut languages = LanguageRegistry::new(login_shell_env_loaded);
- languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
- let languages = Arc::new(languages);
- let init_languages = cx
- .background()
- .spawn(languages::init(languages.clone(), cx.background().clone()));
- let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
-
let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
//Setup settings global before binding actions
@@ -105,7 +98,19 @@ fn main() {
settings_file_content.clone(),
fs.clone(),
));
+
watch_settings_file(default_settings, settings_file_content, themes.clone(), cx);
+ upload_previous_panics(http.clone(), cx);
+
+ let client = client::Client::new(http.clone(), cx);
+ let mut languages = LanguageRegistry::new(login_shell_env_loaded);
+ languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
+ let languages = Arc::new(languages);
+ let init_languages = cx
+ .background()
+ .spawn(languages::init(languages.clone(), cx.background().clone()));
+ let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
+
watch_keymap_file(keymap_file, cx);
context_menu::init(cx);
@@ -143,7 +148,11 @@ fn main() {
.detach();
client.start_telemetry();
- client.report_event("start app", Default::default());
+ client.report_event(
+ "start app",
+ Default::default(),
+ cx.global::<Settings>().telemetry(),
+ );
let app_state = Arc::new(AppState {
languages,
@@ -251,65 +260,7 @@ fn init_logger() {
}
}
-fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: Arc<Background>) {
- background
- .spawn({
- async move {
- let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
- let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
- while let Some(child) = children.next().await {
- let child = child?;
- let child_path = child.path();
- if child_path.extension() != Some(OsStr::new("panic")) {
- continue;
- }
- let filename = if let Some(filename) = child_path.file_name() {
- filename.to_string_lossy()
- } else {
- continue;
- };
-
- let mut components = filename.split('-');
- if components.next() != Some("zed") {
- continue;
- }
- let version = if let Some(version) = components.next() {
- version
- } else {
- continue;
- };
-
- let text = smol::fs::read_to_string(&child_path)
- .await
- .context("error reading panic file")?;
- let body = serde_json::to_string(&json!({
- "text": text,
- "version": version,
- "token": ZED_SECRET_CLIENT_TOKEN,
- }))
- .unwrap();
- let request = Request::post(&panic_report_url)
- .redirect_policy(isahc::config::RedirectPolicy::Follow)
- .header("Content-Type", "application/json")
- .body(body.into())?;
- let response = http.send(request).await.context("error sending panic")?;
- if response.status().is_success() {
- std::fs::remove_file(child_path)
- .context("error removing panic after sending it successfully")
- .log_err();
- } else {
- return Err(anyhow!(
- "error uploading panic to server: {}",
- response.status()
- ));
- }
- }
- Ok::<_, anyhow::Error>(())
- }
- .log_err()
- })
- .detach();
-
+fn init_panic_hook(app_version: String) {
let is_pty = stdout_is_a_pty();
panic::set_hook(Box::new(move |info| {
let backtrace = Backtrace::new();
@@ -358,6 +309,69 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
}));
}
+fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
+ let diagnostics_telemetry = cx.global::<Settings>().telemetry_diagnostics();
+
+ cx.background()
+ .spawn({
+ async move {
+ let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
+ let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
+ while let Some(child) = children.next().await {
+ let child = child?;
+ let child_path = child.path();
+
+ if child_path.extension() != Some(OsStr::new("panic")) {
+ continue;
+ }
+ let filename = if let Some(filename) = child_path.file_name() {
+ filename.to_string_lossy()
+ } else {
+ continue;
+ };
+
+ let mut components = filename.split('-');
+ if components.next() != Some("zed") {
+ continue;
+ }
+ let version = if let Some(version) = components.next() {
+ version
+ } else {
+ continue;
+ };
+
+ if diagnostics_telemetry {
+ let text = smol::fs::read_to_string(&child_path)
+ .await
+ .context("error reading panic file")?;
+ let body = serde_json::to_string(&json!({
+ "text": text,
+ "version": version,
+ "token": ZED_SECRET_CLIENT_TOKEN,
+ }))
+ .unwrap();
+ let request = Request::post(&panic_report_url)
+ .redirect_policy(isahc::config::RedirectPolicy::Follow)
+ .header("Content-Type", "application/json")
+ .body(body.into())?;
+ let response = http.send(request).await.context("error sending panic")?;
+ if !response.status().is_success() {
+ log::error!("Error uploading panic to server: {}", response.status());
+ }
+ }
+
+ // We've done what we can, delete the file
+ std::fs::remove_file(child_path)
+ .context("error removing panic")
+ .log_err();
+ }
+ Ok::<_, anyhow::Error>(())
+ }
+ .log_err()
+ })
+ .detach();
+}
+
async fn load_login_shell_environment() -> Result<()> {
let marker = "ZED_LOGIN_SHELL_START";
let shell = env::var("SHELL").context(