From 37bd24db365d7bfc3e27fc77d0ac3c0948ffa189 Mon Sep 17 00:00:00 2001 From: Jessica Dene Earley-Cha <12740421+chatasweetie@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:23:16 -0800 Subject: [PATCH] Cmdpal: user research on extension invokes (#43905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Added two events that Niels asked for: - `CmdPal_ExtensionInvoked` - Track which extensions are being used (e.g., file search, app launching, calculator, etc.). - Properties logged - ExtensionId - Unique identifier of the extension provider - CommandType - Display name of the command being invoked - Success - Whether the command executed successfully - ExecutionTimeMs - Execution time in milliseconds - `CmdPal_SessionDuration` - Tracks how long Command Palette stays open (launch → close). - Properties logged - DurationMs - Session duration in milliseconds - CommandsExecuted - Number of commands executed - PagesVisited - Number of pages visited - DismissalReason - Why the session ended (Escape, LostFocus, Command, etc.) - SearchQueriesCount - Number of search queries executed - MaxNavigationDepth - Maximum navigation depth reached - ErrorCount - Number of errors encountered ## PR Checklist - [ ] Closes: #xxx - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx --- DATA_AND_PRIVACY.md | 9 +++ .../Messages/ErrorOccurredMessage.cs | 11 +++ .../Messages/ExtensionInvokedMessage.cs | 11 +++ .../Messages/NavigationDepthMessage.cs | 11 +++ .../Messages/SearchQueryMessage.cs | 11 +++ .../Messages/SessionDurationMessage.cs | 11 +++ .../Messages/TelemetryBeginInvokeMessage.cs | 10 +++ .../TelemetryExtensionInvokedMessage.cs | 11 +++ .../Messages/TelemetryInvokeResultMessage.cs | 10 +++ .../ShellViewModel.cs | 36 ++++++++- .../CommandPaletteHost.cs | 2 +- .../Controls/SearchBar.xaml.cs | 6 ++ .../Events/CmdPalExtensionInvoked.cs | 56 +++++++++++++ .../Events/CmdPalInvokeResult.cs | 1 + .../Events/CmdPalSessionDuration.cs | 68 ++++++++++++++++ .../Helpers/TelemetryForwarder.cs | 70 ++++++++++++---- .../Microsoft.CmdPal.UI/MainWindow.xaml.cs | 80 +++++++++++++++++++ .../Pages/ShellPage.xaml.cs | 3 + 18 files changed, 397 insertions(+), 20 deletions(-) create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ErrorOccurredMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ExtensionInvokedMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigationDepthMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SearchQueryMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SessionDurationMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryBeginInvokeMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryExtensionInvokedMessage.cs create mode 100644 src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryInvokeResultMessage.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalSessionDuration.cs diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index 8aba94f12f..c2699e7f9d 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links ### Command Palette + @@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links + + + + + + + +
Event NameMicrosoft.PowerToys.CmdPalProcessStarted Triggered when the Command Palette process is started.
Microsoft.PowerToys.CmdPal_ExtensionInvokedTracks extension usage including extension ID, command details, success status, and execution time.
Microsoft.PowerToys.CmdPal_SessionDurationLogs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.
### Crop And Lock diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ErrorOccurredMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ErrorOccurredMessage.cs new file mode 100644 index 0000000000..bfd60de675 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ErrorOccurredMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Message sent when an error occurs during command execution. +/// Used to track session error count for telemetry. +/// +public record ErrorOccurredMessage(); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ExtensionInvokedMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ExtensionInvokedMessage.cs new file mode 100644 index 0000000000..a8a2ee0055 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ExtensionInvokedMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Message sent when an extension command or page is invoked. +/// Captures extension usage metrics for telemetry tracking. +/// +public record ExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigationDepthMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigationDepthMessage.cs new file mode 100644 index 0000000000..b916f28244 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/NavigationDepthMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Message containing the current navigation depth (BackStack count) when navigating to a page. +/// Used to track maximum navigation depth reached during a session for telemetry. +/// +public record NavigationDepthMessage(int Depth); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SearchQueryMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SearchQueryMessage.cs new file mode 100644 index 0000000000..7516af0b34 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SearchQueryMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Message sent when a search query is executed in the Command Palette. +/// Used to track session search activity for telemetry. +/// +public record SearchQueryMessage(); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SessionDurationMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SessionDurationMessage.cs new file mode 100644 index 0000000000..4b77a1fd06 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/SessionDurationMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Message containing session telemetry data from Command Palette launch to dismissal. +/// Used to aggregate metrics like duration, commands executed, pages visited, and search activity. +/// +public record SessionDurationMessage(ulong DurationMs, int CommandsExecuted, int PagesVisited, string DismissalReason, int SearchQueriesCount, int MaxNavigationDepth, int ErrorCount); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryBeginInvokeMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryBeginInvokeMessage.cs new file mode 100644 index 0000000000..87a1ae8aef --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryBeginInvokeMessage.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Telemetry message sent when command invocation begins. +/// +public record TelemetryBeginInvokeMessage; diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryExtensionInvokedMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryExtensionInvokedMessage.cs new file mode 100644 index 0000000000..464d5ae696 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryExtensionInvokedMessage.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Telemetry message sent when an extension command or page is invoked. +/// Captures extension usage metrics for telemetry tracking. +/// +public record TelemetryExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryInvokeResultMessage.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryInvokeResultMessage.cs new file mode 100644 index 0000000000..06e0b4fd53 --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/TelemetryInvokeResultMessage.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +/// +/// Telemetry message sent when command invocation completes with a result. +/// +public record TelemetryInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index 16ca5b1fca..62c70076ad 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -270,8 +270,18 @@ public partial class ShellViewModel : ObservableObject, var isMainPage = command == _rootPage; _isNested = !isMainPage; + // Telemetry: Track extension page navigation for session metrics + if (host is not null) + { + string extensionId = host.GetExtensionDisplayName() ?? "builtin"; + string commandId = command?.Id ?? "unknown"; + string commandName = command?.Name ?? "unknown"; + WeakReferenceMessenger.Default.Send( + new(extensionId, commandId, commandName, true, 0)); + } + // Construct our ViewModel of the appropriate type and pass it the UI Thread context. - var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host); + var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!); if (pageViewModel is null) { CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); @@ -306,7 +316,7 @@ public partial class ShellViewModel : ObservableObject, { CoreLogger.LogDebug($"Invoking command"); - WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(); StartInvoke(message, invokable, host); } } @@ -339,6 +349,14 @@ public partial class ShellViewModel : ObservableObject, private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host) { + // Telemetry: Track command execution time and success + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var command = message.Command.Unsafe; + string extensionId = host?.GetExtensionDisplayName() ?? "builtin"; + string commandId = command?.Id ?? "unknown"; + string commandName = command?.Name ?? "unknown"; + bool success = false; + try { // Call out to extension process. @@ -349,16 +367,28 @@ public partial class ShellViewModel : ObservableObject, // But if it did succeed, we need to handle the result. UnsafeHandleCommandResult(result); + success = true; _handleInvokeTask = null; } catch (Exception ex) { + success = false; _handleInvokeTask = null; + // Telemetry: Track errors for session metrics + WeakReferenceMessenger.Default.Send(new()); + // TODO: It would be better to do this as a page exception, rather // than a silent log message. host?.Log(ex.Message); } + finally + { + // Telemetry: Send extension invocation metrics (always sent, even on failure) + stopwatch.Stop(); + WeakReferenceMessenger.Default.Send( + new(extensionId, commandId, commandName, success, (ulong)stopwatch.ElapsedMilliseconds)); + } } private void UnsafeHandleCommandResult(ICommandResult? result) @@ -372,7 +402,7 @@ public partial class ShellViewModel : ObservableObject, var kind = result.Kind; CoreLogger.LogDebug($"handling {kind.ToString()}"); - WeakReferenceMessenger.Default.Send(new(kind)); + WeakReferenceMessenger.Default.Send(new(kind)); switch (kind) { case CommandResultKind.Dismiss: diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs index da0972de8e..af089b3edc 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandPaletteHost.cs @@ -34,6 +34,6 @@ public sealed partial class CommandPaletteHost : AppExtensionHost, IExtensionHos public override string? GetExtensionDisplayName() { - return Extension?.ExtensionDisplayName; + return Extension?.ExtensionDisplayName ?? _builtInProvider?.DisplayName ?? _builtInProvider?.Id; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs index 0d6fd58afa..ca27af4719 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -379,6 +379,12 @@ public sealed partial class SearchBar : UserControl, if (CurrentPageViewModel is not null) { CurrentPageViewModel.SearchTextBox = FilterBox.Text; + + // Telemetry: Track search query count for session metrics (only non-empty queries) + if (!string.IsNullOrWhiteSpace(FilterBox.Text)) + { + WeakReferenceMessenger.Default.Send(new()); + } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs new file mode 100644 index 0000000000..0113a4ad27 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.CmdPal.UI.Events; + +/// +/// Tracks extension usage with extension name and invocation details. +/// Purpose: Identify popular vs. unused plugins and track extension performance. +/// +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public class CmdPalExtensionInvoked : EventBase, IEvent +{ + /// + /// Gets or sets the unique identifier of the extension provider. + /// + public string ExtensionId { get; set; } + + /// + /// Gets or sets the non-localized identifier of the command being invoked. + /// + public string CommandId { get; set; } + + /// + /// Gets or sets the localized display name of the command being invoked. + /// + public string CommandName { get; set; } + + /// + /// Gets or sets whether the command executed successfully. + /// + public bool Success { get; set; } + + /// + /// Gets or sets the execution time in milliseconds. + /// + public ulong ExecutionTimeMs { get; set; } + + public CmdPalExtensionInvoked(string extensionId, string commandId, string commandName, bool success, ulong executionTimeMs) + { + EventName = "CmdPal_ExtensionInvoked"; + ExtensionId = extensionId; + CommandId = commandId; + CommandName = commandName; + Success = success; + ExecutionTimeMs = executionTimeMs; + } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalInvokeResult.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalInvokeResult.cs index f4f8b4d0e8..287471f977 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalInvokeResult.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalInvokeResult.cs @@ -18,6 +18,7 @@ public class CmdPalInvokeResult : EventBase, IEvent public CmdPalInvokeResult(CommandResultKind resultKind) { + EventName = "CmdPal_InvokeResult"; ResultKind = resultKind.ToString(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalSessionDuration.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalSessionDuration.cs new file mode 100644 index 0000000000..357cb9db53 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalSessionDuration.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace Microsoft.CmdPal.UI.Events; + +/// +/// Tracks Command Palette session duration from launch to close. +/// Purpose: Understand user engagement patterns - quick actions vs. browsing behavior. +/// +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public class CmdPalSessionDuration : EventBase, IEvent +{ + /// + /// Gets or sets the session duration in milliseconds. + /// + public ulong DurationMs { get; set; } + + /// + /// Gets or sets the number of commands executed during the session. + /// + public int CommandsExecuted { get; set; } + + /// + /// Gets or sets the number of pages visited during the session. + /// + public int PagesVisited { get; set; } + + /// + /// Gets or sets the reason for dismissal (Escape, LostFocus, Command, etc.). + /// + public string DismissalReason { get; set; } + + /// + /// Gets or sets the number of search queries executed during the session. + /// + public int SearchQueriesCount { get; set; } + + /// + /// Gets or sets the maximum navigation depth reached during the session. + /// + public int MaxNavigationDepth { get; set; } + + /// + /// Gets or sets the number of errors encountered during the session. + /// + public int ErrorCount { get; set; } + + public CmdPalSessionDuration(ulong durationMs, int commandsExecuted, int pagesVisited, string dismissalReason, int searchQueriesCount, int maxNavigationDepth, int errorCount) + { + EventName = "CmdPal_SessionDuration"; + DurationMs = durationMs; + CommandsExecuted = commandsExecuted; + PagesVisited = pagesVisited; + DismissalReason = dismissalReason; + SearchQueriesCount = searchQueriesCount; + MaxNavigationDepth = maxNavigationDepth; + ErrorCount = errorCount; + } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs index e14d1abe3b..37139bb982 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs @@ -6,40 +6,78 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.UI.Events; +using Microsoft.CommandPalette.Extensions; using Microsoft.PowerToys.Telemetry; namespace Microsoft.CmdPal.UI; /// /// TelemetryForwarder is responsible for forwarding telemetry events from the -/// command palette core to PowerToys Telemetry. -/// This allows us to emit telemetry events as messages from the core, -/// and then handle them by logging to our PT telemetry provider. -/// -/// We may in the future want to replace this with a more generic "ITelemetryService" -/// or something similar, but this works for now. +/// command palette to PowerToys Telemetry. +/// Listens to telemetry-specific messages from the core layer and logs them to PowerToys telemetry. +/// Also implements ITelemetryService for dependency injection in extensions. /// internal sealed class TelemetryForwarder : ITelemetryService, - IRecipient, - IRecipient + IRecipient, + IRecipient, + IRecipient { public TelemetryForwarder() { - WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } - public void Receive(CmdPalInvokeResultMessage message) - { - PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(message.Kind)); - } - - public void Receive(BeginInvokeMessage message) + // Message handlers for telemetry events from core layer + public void Receive(TelemetryBeginInvokeMessage message) { PowerToysTelemetry.Log.WriteEvent(new BeginInvoke()); } + public void Receive(TelemetryInvokeResultMessage message) + { + PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(message.Kind)); + } + + public void Receive(TelemetryExtensionInvokedMessage message) + { + PowerToysTelemetry.Log.WriteEvent(new CmdPalExtensionInvoked( + message.ExtensionId, + message.CommandId, + message.CommandName, + message.Success, + message.ExecutionTimeMs)); + + // Increment session counter for commands executed + if (App.Current.AppWindow is MainWindow mainWindow) + { + mainWindow.IncrementCommandsExecuted(); + } + } + + // Static method for logging session duration from UI layer + public static void LogSessionDuration( + ulong durationMs, + int commandsExecuted, + int pagesVisited, + string dismissalReason, + int searchQueriesCount, + int maxNavigationDepth, + int errorCount) + { + PowerToysTelemetry.Log.WriteEvent(new CmdPalSessionDuration( + durationMs, + commandsExecuted, + pagesVisited, + dismissalReason, + searchQueriesCount, + maxNavigationDepth, + errorCount)); + } + + // ITelemetryService implementation for dependency injection in extensions public void LogRunQuery(string query, int resultCount, ulong durationMs) { PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs)); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 54e9b8a216..abaf102a9e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -52,6 +52,10 @@ public sealed partial class MainWindow : WindowEx, IRecipient, IRecipient, IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, IRecipient, IRecipient, IDisposable @@ -75,6 +79,14 @@ public sealed partial class MainWindow : WindowEx, private bool _ignoreHotKeyWhenFullScreen = true; private bool _themeServiceInitialized; + // Session tracking for telemetry + private Stopwatch? _sessionStopwatch; + private int _sessionCommandsExecuted; + private int _sessionPagesVisited; + private int _sessionSearchQueriesCount; + private int _sessionMaxNavigationDepth; + private int _sessionErrorCount; + private DesktopAcrylicController? _acrylicController; private SystemBackdropConfiguration? _configurationSource; private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan; @@ -123,6 +135,10 @@ public sealed partial class MainWindow : WindowEx, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -524,6 +540,11 @@ public sealed partial class MainWindow : WindowEx, { var settings = App.Current.Services.GetService()!; + // Start session tracking + _sessionStopwatch = Stopwatch.StartNew(); + _sessionCommandsExecuted = 0; + _sessionPagesVisited = 0; + ShowHwnd(message.Hwnd, settings.SummonOn); } @@ -532,6 +553,7 @@ public sealed partial class MainWindow : WindowEx, // This might come in off the UI thread. Make sure to hop back. DispatcherQueue.TryEnqueue(() => { + EndSession("Hide"); HideWindow(); }); } @@ -551,10 +573,67 @@ public sealed partial class MainWindow : WindowEx, // This might come in off the UI thread. Make sure to hop back. DispatcherQueue.TryEnqueue(() => { + EndSession("Dismiss"); HideWindow(); }); } + // Session telemetry: Track metrics during the Command Palette session + // These receivers increment counters that are sent when EndSession is called + public void Receive(NavigateToPageMessage message) + { + _sessionPagesVisited++; + } + + public void Receive(NavigationDepthMessage message) + { + if (message.Depth > _sessionMaxNavigationDepth) + { + _sessionMaxNavigationDepth = message.Depth; + } + } + + public void Receive(SearchQueryMessage message) + { + _sessionSearchQueriesCount++; + } + + public void Receive(ErrorOccurredMessage message) + { + _sessionErrorCount++; + } + + /// + /// Ends the current telemetry session and emits the CmdPal_SessionDuration event. + /// Aggregates all session metrics collected since ShowWindow and sends them to telemetry. + /// + /// The reason the session ended (e.g., Dismiss, Hide, LostFocus). + private void EndSession(string dismissalReason) + { + if (_sessionStopwatch is not null) + { + _sessionStopwatch.Stop(); + TelemetryForwarder.LogSessionDuration( + (ulong)_sessionStopwatch.ElapsedMilliseconds, + _sessionCommandsExecuted, + _sessionPagesVisited, + dismissalReason, + _sessionSearchQueriesCount, + _sessionMaxNavigationDepth, + _sessionErrorCount); + _sessionStopwatch = null; + } + } + + /// + /// Increments the session commands executed counter for telemetry. + /// Called by TelemetryForwarder when an extension command is invoked. + /// + internal void IncrementCommandsExecuted() + { + _sessionCommandsExecuted++; + } + private void HideWindow() { // Cloak our HWND to avoid all animations. @@ -764,6 +843,7 @@ public sealed partial class MainWindow : WindowEx, } // This will DWM cloak our window: + EndSession("LostFocus"); HideWindow(); PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus()); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs index 7e8dc9eebd..394325eb18 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -161,6 +161,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id)); + // Telemetry: Send navigation depth for session max depth tracking + WeakReferenceMessenger.Default.Send(new NavigationDepthMessage(RootFrame.BackStackDepth)); + if (!ViewModel.IsNested) { // todo BODGY