settings_ui: Add telemetry (#40973)

Anthony Eid created

1. Settings Viewed: Whenever someone opens or refocus the settings ui
via an action
2. Settings Closed: When the settings ui window is closed 
3. Settings Navigation Clicked: The category and subcategory that a user
clicked on
4. Settings Error Shown: Whenever an error banner shows up
5. Settings Changed: The setting a user changed through the UI


cc: @katie-z-geer 

Release Notes:

- N/A

Change summary

Cargo.lock                            |   1 
crates/settings_ui/Cargo.toml         |  11 +-
crates/settings_ui/src/settings_ui.rs | 111 ++++++++++++++++++++++------
3 files changed, 95 insertions(+), 28 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15338,6 +15338,7 @@ dependencies = [
  "session",
  "settings",
  "strum 0.27.2",
+ "telemetry",
  "theme",
  "title_bar",
  "ui",

crates/settings_ui/Cargo.toml 🔗

@@ -18,12 +18,13 @@ test-support = []
 [dependencies]
 anyhow.workspace = true
 bm25 = "2.3.2"
-heck.workspace = true
 editor.workspace = true
 feature_flags.workspace = true
 fs.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
+heck.workspace = true
+log.workspace = true
 menu.workspace = true
 paths.workspace = true
 picker.workspace = true
@@ -34,14 +35,14 @@ search.workspace = true
 serde.workspace = true
 settings.workspace = true
 strum.workspace = true
+telemetry.workspace = true
 theme.workspace = true
-ui_input.workspace = true
+title_bar.workspace = true
 ui.workspace = true
+ui_input.workspace = true
 util.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
-log.workspace = true
-title_bar.workspace = true
 
 [dev-dependencies]
 assets.workspace = true
@@ -51,7 +52,7 @@ gpui = { workspace = true, features = ["test-support"] }
 language.workspace = true
 node_runtime.workspace = true
 paths.workspace = true
+pretty_assertions.workspace = true
 session.workspace = true
 settings.workspace = true
 zlog.workspace = true
-pretty_assertions.workspace = true

crates/settings_ui/src/settings_ui.rs 🔗

@@ -20,11 +20,12 @@ use settings::{Settings, SettingsContent, SettingsStore};
 use std::{
     any::{Any, TypeId, type_name},
     cell::RefCell,
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     num::{NonZero, NonZeroU32},
     ops::Range,
     rc::Rc,
     sync::{Arc, LazyLock, RwLock},
+    time::Duration,
 };
 use title_bar::platform_title_bar::PlatformTitleBar;
 use ui::{
@@ -213,7 +214,7 @@ impl<T: PartialEq + Clone + Send + Sync + 'static> AnySettingField for SettingFi
             } else {
                 None
             };
-            update_settings_file(current_file.clone(), cx, move |settings, _| {
+            update_settings_file(current_file.clone(), None, cx, move |settings, _| {
                 (this.write)(settings, value_to_set);
             })
             // todo(settings_ui): Don't log err
@@ -500,6 +501,8 @@ pub fn open_settings_editor(
     workspace_handle: WindowHandle<Workspace>,
     cx: &mut App,
 ) {
+    telemetry::event!("Settings Viewed");
+
     /// Assumes a settings GUI window is already open
     fn open_path(
         path: &str,
@@ -668,6 +671,7 @@ pub struct SettingsWindow {
     files_focus_handle: FocusHandle,
     search_index: Option<Arc<SearchIndex>>,
     list_state: ListState,
+    shown_errors: HashSet<String>,
 }
 
 struct SearchIndex {
@@ -1109,6 +1113,14 @@ enum SettingsUiFile {
 }
 
 impl SettingsUiFile {
+    fn setting_type(&self) -> &'static str {
+        match self {
+            SettingsUiFile::User => "User",
+            SettingsUiFile::Project(_) => "Project",
+            SettingsUiFile::Server(_) => "Server",
+        }
+    }
+
     fn is_server(&self) -> bool {
         matches!(self, SettingsUiFile::Server(_))
     }
@@ -1196,6 +1208,8 @@ impl SettingsWindow {
                     window.remove_window();
                 })
                 .ok();
+
+                telemetry::event!("Settings Closed")
             }
         })
         .detach();
@@ -1286,6 +1300,7 @@ impl SettingsWindow {
                 .tab_index(HEADER_CONTAINER_TAB_INDEX)
                 .tab_stop(false),
             search_index: None,
+            shown_errors: HashSet::default(),
             list_state,
         };
 
@@ -1579,6 +1594,9 @@ impl SettingsWindow {
                     );
                 })
                 .ok();
+
+            cx.background_executor().timer(Duration::from_secs(1)).await;
+            telemetry::event!("Settings Searched", query = query)
         }));
     }
 
@@ -1833,6 +1851,10 @@ impl SettingsWindow {
         }
         self.current_file = self.files[ix].0.clone();
 
+        if let SettingsUiFile::Project((_, _)) = &self.current_file {
+            telemetry::event!("Setting Project Clicked");
+        }
+
         self.build_ui(window, cx);
 
         if self
@@ -2219,8 +2241,18 @@ impl SettingsWindow {
                                                     },
                                                 ))
                                         })
-                                        .on_click(
+                                        .on_click({
+                                            let category = this.pages[entry.page_index].title;
+                                            let subcategory =
+                                                (!entry.is_root).then_some(entry.title);
+
                                             cx.listener(move |this, _, window, cx| {
+                                                telemetry::event!(
+                                                    "Settings Navigation Clicked",
+                                                    category = category,
+                                                    subcategory = subcategory
+                                                );
+
                                                 this.open_and_scroll_to_navbar_entry(
                                                     entry_index,
                                                     None,
@@ -2228,8 +2260,8 @@ impl SettingsWindow {
                                                     window,
                                                     cx,
                                                 );
-                                            }),
-                                        )
+                                            })
+                                        })
                                     })
                                     .collect()
                             }),
@@ -2646,6 +2678,10 @@ impl SettingsWindow {
         if let Some(error) =
             SettingsStore::global(cx).error_for_file(self.current_file.to_settings())
         {
+            if self.shown_errors.insert(error.clone()) {
+                telemetry::event!("Settings Error Shown", error = &error);
+            }
+
             warning_banner = v_flex()
                 .pb_4()
                 .child(
@@ -3097,9 +3133,12 @@ fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
 
 fn update_settings_file(
     file: SettingsUiFile,
+    file_name: Option<&'static str>,
     cx: &mut App,
     update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
 ) -> Result<()> {
+    telemetry::event!("Settings Change", setting = file_name, type = file.setting_type());
+
     match file {
         SettingsUiFile::Project((worktree_id, rel_path)) => {
             let rel_path = rel_path.join(paths::local_settings_file_relative_path());
@@ -3170,7 +3209,7 @@ fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
         )
         .on_confirm({
             move |new_text, cx| {
-                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
                     (field.write)(settings, new_text.map(Into::into));
                 })
                 .log_err(); // todo(settings_ui) don't log err
@@ -3199,8 +3238,10 @@ fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
         .color(SwitchColor::Accent)
         .on_click({
             move |state, _window, cx| {
+                telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type());
+
                 let state = *state == ui::ToggleState::Selected;
-                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
                     (field.write)(settings, Some(state.into()));
                 })
                 .log_err(); // todo(settings_ui) don't log err
@@ -3222,7 +3263,7 @@ fn render_number_field<T: NumberFieldType + Send + Sync>(
         .on_change({
             move |value, _window, cx| {
                 let value = *value;
-                update_settings_file(file.clone(), cx, move |settings, _cx| {
+                update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| {
                     (field.write)(settings, Some(value));
                 })
                 .log_err(); // todo(settings_ui) don't log err
@@ -3278,9 +3319,14 @@ where
                             if value == current_value {
                                 return;
                             }
-                            update_settings_file(file.clone(), cx, move |settings, _cx| {
-                                (field.write)(settings, Some(value));
-                            })
+                            update_settings_file(
+                                file.clone(),
+                                field.json_path,
+                                cx,
+                                move |settings, _cx| {
+                                    (field.write)(settings, Some(value));
+                                },
+                            )
                             .log_err(); // todo(settings_ui) don't log err
                         },
                     );
@@ -3336,9 +3382,14 @@ fn render_font_picker(
                 font_picker(
                     current_value.clone().into(),
                     move |font_name, cx| {
-                        update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            (field.write)(settings, Some(font_name.into()));
-                        })
+                        update_settings_file(
+                            file.clone(),
+                            field.json_path,
+                            cx,
+                            move |settings, _cx| {
+                                (field.write)(settings, Some(font_name.into()));
+                            },
+                        )
                         .log_err(); // todo(settings_ui) don't log err
                     },
                     window,
@@ -3380,9 +3431,17 @@ fn render_theme_picker(
                 theme_picker(
                     current_value,
                     move |theme_name, cx| {
-                        update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            (field.write)(settings, Some(settings::ThemeName(theme_name.into())));
-                        })
+                        update_settings_file(
+                            file.clone(),
+                            field.json_path,
+                            cx,
+                            move |settings, _cx| {
+                                (field.write)(
+                                    settings,
+                                    Some(settings::ThemeName(theme_name.into())),
+                                );
+                            },
+                        )
                         .log_err(); // todo(settings_ui) don't log err
                     },
                     window,
@@ -3424,12 +3483,17 @@ fn render_icon_theme_picker(
                 icon_theme_picker(
                     current_value,
                     move |theme_name, cx| {
-                        update_settings_file(file.clone(), cx, move |settings, _cx| {
-                            (field.write)(
-                                settings,
-                                Some(settings::IconThemeName(theme_name.into())),
-                            );
-                        })
+                        update_settings_file(
+                            file.clone(),
+                            field.json_path,
+                            cx,
+                            move |settings, _cx| {
+                                (field.write)(
+                                    settings,
+                                    Some(settings::IconThemeName(theme_name.into())),
+                                );
+                            },
+                        )
                         .log_err(); // todo(settings_ui) don't log err
                     },
                     window,
@@ -3565,6 +3629,7 @@ pub mod test {
             files_focus_handle: cx.focus_handle(),
             search_index: None,
             list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)),
+            shown_errors: HashSet::default(),
         };
 
         settings_window.build_filter_table();