Merge pull request #2020 from zed-industries/telemtry-opt-out

Mikayla Maki created

Telemetry opt out

Change summary

assets/settings/default.json                            |   7 
crates/client/src/client.rs                             |  31 +
crates/client/src/telemetry.rs                          |  19 +
crates/client/src/user.rs                               |  14 
crates/collab/src/tests/randomized_integration_tests.rs |   5 
crates/editor/src/editor.rs                             |   9 
crates/settings/src/settings.rs                         |  51 +++
crates/workspace/src/dock.rs                            |   6 
crates/workspace/src/item.rs                            |   2 
crates/zed/src/main.rs                                  | 158 +++++-----
10 files changed, 207 insertions(+), 95 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -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:

crates/client/src/client.rs 🔗

@@ -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> {

crates/client/src/telemetry.rs 🔗

@@ -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(),

crates/client/src/user.rs 🔗

@@ -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();
                             }

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -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);
         });
     }

crates/editor/src/editor.rs 🔗

@@ -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(),
+            );
         }
     }
 }

crates/settings/src/settings.rs 🔗

@@ -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,
         }
     }

crates/workspace/src/dock.rs 🔗

@@ -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,

crates/workspace/src/item.rs 🔗

@@ -919,7 +919,7 @@ pub(crate) mod test {
         }
 
         fn serialized_item_kind() -> Option<&'static str> {
-            None
+            Some("TestItem")
         }
 
         fn deserialize(

crates/zed/src/main.rs 🔗

@@ -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(