From 0bde929d548dc2f0733d8a33449067c642737d44 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:50:53 -0400 Subject: [PATCH] Add keymap editor UI telemetry events (#34571) - Search queries - Keybinding update or removed - Copy action name - Copy context name cc @katie-z-geer Release Notes: - N/A Co-authored-by: Ben Kunkle --- Cargo.lock | 1 + crates/settings/src/keymap_file.rs | 52 +++++++++++++++++ crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 81 ++++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1b3ec32c8..091b944169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14720,6 +14720,7 @@ dependencies = [ "serde", "serde_json", "settings", + "telemetry", "theme", "tree-sitter-json", "tree-sitter-rust", diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index d738e30c4f..e6a32f731b 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -847,6 +847,7 @@ impl KeymapFile { } } +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -865,6 +866,47 @@ pub enum KeybindUpdateOperation<'a> { }, } +impl KeybindUpdateOperation<'_> { + pub fn generate_telemetry( + &self, + ) -> ( + // The keybind that is created + String, + // The keybinding that was removed + String, + // The source of the keybinding + String, + ) { + let (new_binding, removed_binding, source) = match &self { + KeybindUpdateOperation::Replace { + source, + target, + target_keybind_source, + } => (Some(source), Some(target), Some(*target_keybind_source)), + KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None), + KeybindUpdateOperation::Remove { + target, + target_keybind_source, + } => (None, Some(target), Some(*target_keybind_source)), + }; + + let new_binding = new_binding + .map(KeybindUpdateTarget::telemetry_string) + .unwrap_or("null".to_owned()); + let removed_binding = removed_binding + .map(KeybindUpdateTarget::telemetry_string) + .unwrap_or("null".to_owned()); + + let source = source + .as_ref() + .map(KeybindSource::name) + .map(ToOwned::to_owned) + .unwrap_or("null".to_owned()); + + (new_binding, removed_binding, source) + } +} + impl<'a> KeybindUpdateOperation<'a> { pub fn add(source: KeybindUpdateTarget<'a>) -> Self { Self::Add { source, from: None } @@ -905,6 +947,16 @@ impl<'a> KeybindUpdateTarget<'a> { keystrokes.pop(); keystrokes } + + fn telemetry_string(&self) -> String { + format!( + "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}", + self.action_name, + self.context.unwrap_or("global"), + self.action_arguments.unwrap_or("none"), + self.keystrokes_unparsed() + ) + } } #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 4502d994e7..e512c4e4d4 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -34,6 +34,7 @@ search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +telemetry.workspace = true theme.workspace = true tree-sitter-json.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 78636f7023..e0105fa875 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,6 +1,7 @@ use std::{ ops::{Not as _, Range}, sync::Arc, + time::Duration, }; use anyhow::{Context as _, anyhow}; @@ -12,7 +13,7 @@ use gpui::{ Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, - ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, + ScrollWheelEvent, StyledText, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -151,6 +152,13 @@ impl SearchMode { SearchMode::KeyStroke { .. } => SearchMode::Normal, } } + + fn exact_match(&self) -> bool { + match self { + SearchMode::Normal => false, + SearchMode::KeyStroke { exact_match } => *exact_match, + } + } } #[derive(Default, PartialEq, Copy, Clone)] @@ -249,6 +257,7 @@ struct KeymapEditor { keybinding_conflict_state: ConflictState, filter_state: FilterState, search_mode: SearchMode, + search_query_debounce: Option>, // corresponds 1 to 1 with keybindings string_match_candidates: Arc>, matches: Vec, @@ -347,6 +356,7 @@ impl KeymapEditor { context_menu: None, previous_edit: None, humanized_action_names, + search_query_debounce: None, }; this.on_keymap_changed(cx); @@ -371,10 +381,32 @@ impl KeymapEditor { } } - fn on_query_changed(&self, cx: &mut Context) { + fn on_query_changed(&mut self, cx: &mut Context) { let action_query = self.current_action_query(cx); let keystroke_query = self.current_keystroke_query(cx); + let exact_match = self.search_mode.exact_match(); + let timer = cx.background_executor().timer(Duration::from_secs(1)); + self.search_query_debounce = Some(cx.background_spawn({ + let action_query = action_query.clone(); + let keystroke_query = keystroke_query.clone(); + async move { + timer.await; + + let keystroke_query = keystroke_query + .into_iter() + .map(|keystroke| keystroke.unparse()) + .collect::>() + .join(" "); + + telemetry::event!( + "Keystroke Search Completed", + action_query = action_query, + keystroke_query = keystroke_query, + keystroke_exact_match = exact_match + ) + } + })); cx.spawn(async move |this, cx| { Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?; this.update(cx, |this, cx| { @@ -474,6 +506,7 @@ impl KeymapEditor { } this.selected_index.take(); this.matches = matches; + cx.notify(); }) } @@ -864,6 +897,26 @@ impl KeymapEditor { return; }; let keymap_editor = cx.entity(); + + let arguments = keybind + .action_arguments + .as_ref() + .map(|arguments| arguments.text.clone()); + let context = keybind + .context + .as_ref() + .map(|context| context.local_str().unwrap_or("global")); + let source = keybind.source.as_ref().map(|source| source.1.clone()); + + telemetry::event!( + "Edit Keybinding Modal Opened", + keystroke = keybind.keystroke_text, + action = keybind.action_name, + source = source, + context = context, + arguments = arguments, + ); + self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); @@ -899,7 +952,7 @@ impl KeymapEditor { return; }; - let Ok(fs) = self + let std::result::Result::Ok(fs) = self .workspace .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) else { @@ -929,6 +982,8 @@ impl KeymapEditor { let Some(context) = context else { return; }; + + telemetry::event!("Keybinding Context Copied", context = context.clone()); cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone())); } @@ -944,6 +999,8 @@ impl KeymapEditor { let Some(action) = action else { return; }; + + telemetry::event!("Keybinding Action Copied", action = action.clone()); cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); } @@ -2222,6 +2279,9 @@ async fn save_keybinding_update( from: Some(target), } }; + + let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); + let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; @@ -2231,6 +2291,13 @@ async fn save_keybinding_update( ) .await .context("Failed to write keymap file")?; + + telemetry::event!( + "Keybinding Updated", + new_keybinding = new_keybinding, + removed_keybinding = removed_keybinding, + source = source + ); Ok(()) } @@ -2266,6 +2333,7 @@ async fn remove_keybinding( .unwrap_or(KeybindSource::User), }; + let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; @@ -2275,6 +2343,13 @@ async fn remove_keybinding( ) .await .context("Failed to write keymap file")?; + + telemetry::event!( + "Keybinding Removed", + new_keybinding = new_keybinding, + removed_keybinding = removed_keybinding, + source = source + ); Ok(()) }