add CmdPal_SessionDuration telemetry event

This commit is contained in:
chatasweetie
2025-11-25 15:52:02 -08:00
parent 8bb900eee2
commit 26103fa0c7
11 changed files with 225 additions and 2 deletions

View File

@@ -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;
/// <summary>
/// Message sent when an error occurs during command execution.
/// Used to track session error count for telemetry.
/// </summary>
public record ErrorOccurredMessage();

View File

@@ -4,4 +4,8 @@
namespace Microsoft.CmdPal.Core.ViewModels.Messages; namespace Microsoft.CmdPal.Core.ViewModels.Messages;
/// <summary>
/// Message sent when an extension command or page is invoked.
/// Captures extension usage metrics for telemetry tracking.
/// </summary>
public record ExtensionInvokedMessage(string ExtensionId, string CommandType, bool Success, ulong ExecutionTimeMs); public record ExtensionInvokedMessage(string ExtensionId, string CommandType, bool Success, ulong ExecutionTimeMs);

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public record NavigationDepthMessage(int Depth);

View File

@@ -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;
/// <summary>
/// Message sent when a search query is executed in the Command Palette.
/// Used to track session search activity for telemetry.
/// </summary>
public record SearchQueryMessage();

View File

@@ -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;
/// <summary>
/// Message containing session telemetry data from Command Palette launch to dismissal.
/// Used to aggregate metrics like duration, commands executed, pages visited, and search activity.
/// </summary>
public record SessionDurationMessage(ulong DurationMs, int CommandsExecuted, int PagesVisited, string DismissalReason, int SearchQueriesCount, int MaxNavigationDepth, int ErrorCount);

View File

@@ -269,7 +269,7 @@ public partial class ShellViewModel : ObservableObject,
var isMainPage = command == _rootPage; var isMainPage = command == _rootPage;
_isNested = !isMainPage; _isNested = !isMainPage;
// Track extension page navigation // Telemetry: Track extension page navigation for session metrics
if (host is not null) if (host is not null)
{ {
string extensionId = host.GetExtensionDisplayName() ?? "builtin"; string extensionId = host.GetExtensionDisplayName() ?? "builtin";
@@ -347,6 +347,7 @@ public partial class ShellViewModel : ObservableObject,
private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host) private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host)
{ {
// Telemetry: Track command execution time and success
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var command = message.Command.Unsafe; var command = message.Command.Unsafe;
string extensionId = host?.GetExtensionDisplayName() ?? "builtin"; string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
@@ -371,12 +372,16 @@ public partial class ShellViewModel : ObservableObject,
success = false; success = false;
_handleInvokeTask = null; _handleInvokeTask = null;
// Telemetry: Track errors for session metrics
WeakReferenceMessenger.Default.Send<ErrorOccurredMessage>(new());
// TODO: It would be better to do this as a page exception, rather // TODO: It would be better to do this as a page exception, rather
// than a silent log message. // than a silent log message.
host?.Log(ex.Message); host?.Log(ex.Message);
} }
finally finally
{ {
// Telemetry: Send extension invocation metrics (always sent, even on failure)
stopwatch.Stop(); stopwatch.Stop();
WeakReferenceMessenger.Default.Send<ExtensionInvokedMessage>( WeakReferenceMessenger.Default.Send<ExtensionInvokedMessage>(
new(extensionId, commandType, success, (ulong)stopwatch.ElapsedMilliseconds)); new(extensionId, commandType, success, (ulong)stopwatch.ElapsedMilliseconds));

View File

@@ -329,6 +329,12 @@ public sealed partial class SearchBar : UserControl,
if (CurrentPageViewModel is not null) if (CurrentPageViewModel is not null)
{ {
CurrentPageViewModel.SearchTextBox = FilterBox.Text; CurrentPageViewModel.SearchTextBox = FilterBox.Text;
// Telemetry: Track search query count for session metrics (only non-empty queries)
if (!string.IsNullOrWhiteSpace(FilterBox.Text))
{
WeakReferenceMessenger.Default.Send<SearchQueryMessage>(new());
}
} }
} }

View File

@@ -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;
/// <summary>
/// Tracks Command Palette session duration from launch to close.
/// Purpose: Understand user engagement patterns - quick actions vs. browsing behavior.
/// </summary>
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class CmdPalSessionDuration : EventBase, IEvent
{
/// <summary>
/// Gets or sets the session duration in milliseconds.
/// </summary>
public ulong DurationMs { get; set; }
/// <summary>
/// Gets or sets the number of commands executed during the session.
/// </summary>
public int CommandsExecuted { get; set; }
/// <summary>
/// Gets or sets the number of pages visited during the session.
/// </summary>
public int PagesVisited { get; set; }
/// <summary>
/// Gets or sets the reason for dismissal (Escape, LostFocus, Command, etc.).
/// </summary>
public string DismissalReason { get; set; }
/// <summary>
/// Gets or sets the number of search queries executed during the session.
/// </summary>
public int SearchQueriesCount { get; set; }
/// <summary>
/// Gets or sets the maximum navigation depth reached during the session.
/// </summary>
public int MaxNavigationDepth { get; set; }
/// <summary>
/// Gets or sets the number of errors encountered during the session.
/// </summary>
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;
}

View File

@@ -23,13 +23,15 @@ internal sealed class TelemetryForwarder :
ITelemetryService, ITelemetryService,
IRecipient<BeginInvokeMessage>, IRecipient<BeginInvokeMessage>,
IRecipient<CmdPalInvokeResultMessage>, IRecipient<CmdPalInvokeResultMessage>,
IRecipient<ExtensionInvokedMessage> IRecipient<ExtensionInvokedMessage>,
IRecipient<SessionDurationMessage>
{ {
public TelemetryForwarder() public TelemetryForwarder()
{ {
WeakReferenceMessenger.Default.Register<BeginInvokeMessage>(this); WeakReferenceMessenger.Default.Register<BeginInvokeMessage>(this);
WeakReferenceMessenger.Default.Register<CmdPalInvokeResultMessage>(this); WeakReferenceMessenger.Default.Register<CmdPalInvokeResultMessage>(this);
WeakReferenceMessenger.Default.Register<ExtensionInvokedMessage>(this); WeakReferenceMessenger.Default.Register<ExtensionInvokedMessage>(this);
WeakReferenceMessenger.Default.Register<SessionDurationMessage>(this);
} }
public void Receive(CmdPalInvokeResultMessage message) public void Receive(CmdPalInvokeResultMessage message)
@@ -51,6 +53,18 @@ internal sealed class TelemetryForwarder :
message.ExecutionTimeMs)); message.ExecutionTimeMs));
} }
public void Receive(SessionDurationMessage message)
{
PowerToysTelemetry.Log.WriteEvent(new CmdPalSessionDuration(
message.DurationMs,
message.CommandsExecuted,
message.PagesVisited,
message.DismissalReason,
message.SearchQueriesCount,
message.MaxNavigationDepth,
message.ErrorCount));
}
public void LogRunQuery(string query, int resultCount, ulong durationMs) public void LogRunQuery(string query, int resultCount, ulong durationMs)
{ {
PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs)); PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs));

View File

@@ -49,6 +49,11 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<ShowWindowMessage>, IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>, IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>, IRecipient<QuitMessage>,
IRecipient<ExtensionInvokedMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<NavigationDepthMessage>,
IRecipient<SearchQueryMessage>,
IRecipient<ErrorOccurredMessage>,
IDisposable IDisposable
{ {
private const int DefaultWidth = 800; private const int DefaultWidth = 800;
@@ -66,6 +71,14 @@ public sealed partial class MainWindow : WindowEx,
private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new(); private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new();
private bool _ignoreHotKeyWhenFullScreen = true; private bool _ignoreHotKeyWhenFullScreen = true;
// 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 DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource; private SystemBackdropConfiguration? _configurationSource;
@@ -100,6 +113,11 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<QuitMessage>(this); WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this); WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this); WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<ExtensionInvokedMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
WeakReferenceMessenger.Default.Register<SearchQueryMessage>(this);
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
// Hide our titlebar. // Hide our titlebar.
// We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed // We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed
@@ -484,6 +502,11 @@ public sealed partial class MainWindow : WindowEx,
{ {
var settings = App.Current.Services.GetService<SettingsModel>()!; var settings = App.Current.Services.GetService<SettingsModel>()!;
// Start session tracking
_sessionStopwatch = Stopwatch.StartNew();
_sessionCommandsExecuted = 0;
_sessionPagesVisited = 0;
ShowHwnd(message.Hwnd, settings.SummonOn); ShowHwnd(message.Hwnd, settings.SummonOn);
} }
@@ -492,6 +515,7 @@ public sealed partial class MainWindow : WindowEx,
// This might come in off the UI thread. Make sure to hop back. // This might come in off the UI thread. Make sure to hop back.
DispatcherQueue.TryEnqueue(() => DispatcherQueue.TryEnqueue(() =>
{ {
EndSession("Hide");
HideWindow(); HideWindow();
}); });
} }
@@ -506,10 +530,64 @@ public sealed partial class MainWindow : WindowEx,
// This might come in off the UI thread. Make sure to hop back. // This might come in off the UI thread. Make sure to hop back.
DispatcherQueue.TryEnqueue(() => DispatcherQueue.TryEnqueue(() =>
{ {
EndSession("Dismiss");
HideWindow(); HideWindow();
}); });
} }
// Session telemetry: Track metrics during the Command Palette session
// These receivers increment counters that are sent when EndSession is called
public void Receive(ExtensionInvokedMessage message)
{
_sessionCommandsExecuted++;
}
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++;
}
/// <summary>
/// Ends the current telemetry session and emits the CmdPal_SessionDuration event.
/// Aggregates all session metrics collected since ShowWindow and sends them to telemetry.
/// </summary>
/// <param name="dismissalReason">The reason the session ended (e.g., Dismiss, Hide, LostFocus).</param>
private void EndSession(string dismissalReason)
{
if (_sessionStopwatch is not null)
{
_sessionStopwatch.Stop();
WeakReferenceMessenger.Default.Send<SessionDurationMessage>(new(
(ulong)_sessionStopwatch.ElapsedMilliseconds,
_sessionCommandsExecuted,
_sessionPagesVisited,
dismissalReason,
_sessionSearchQueriesCount,
_sessionMaxNavigationDepth,
_sessionErrorCount));
_sessionStopwatch = null;
}
}
private void HideWindow() private void HideWindow()
{ {
// Cloak our HWND to avoid all animations. // Cloak our HWND to avoid all animations.
@@ -681,6 +759,7 @@ public sealed partial class MainWindow : WindowEx,
} }
// This will DWM cloak our window: // This will DWM cloak our window:
EndSession("LostFocus");
HideWindow(); HideWindow();
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus()); PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());

View File

@@ -161,6 +161,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id)); 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) if (!ViewModel.IsNested)
{ {
// todo BODGY // todo BODGY