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
+
| Event Name |
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
Microsoft.PowerToys.CmdPalProcessStarted |
Triggered when the Command Palette process is started. |
+
+ | Microsoft.PowerToys.CmdPal_ExtensionInvoked |
+ Tracks extension usage including extension ID, command details, success status, and execution time. |
+
+
+ | Microsoft.PowerToys.CmdPal_SessionDuration |
+ Logs 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