Add opt-out for metric reporting

Mikayla Maki and kay created

co-authored-by: kay <kay@zed.dev>

Change summary

crates/client/src/client.rs     | 31 ++++++++++++++++++++++++++-----
crates/client/src/telemetry.rs  | 19 ++++++++++++++++++-
crates/client/src/user.rs       | 14 ++++++--------
crates/editor/src/editor.rs     |  9 +++++----
crates/settings/src/settings.rs | 16 ++++++++++++++++
crates/zed/src/main.rs          |  6 +++++-
6 files changed, 76 insertions(+), 19 deletions(-)

Detailed changes

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/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 🔗

@@ -62,6 +62,15 @@ pub struct TelemetrySettings {
     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,
@@ -503,6 +512,13 @@ 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

crates/zed/src/main.rs 🔗

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