mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-07 03:36:44 +02:00
Cmdpal: user research on extension invokes (#43905)
## 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
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [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
This commit is contained in:
committed by
GitHub
parent
557a07589d
commit
37bd24db36
@@ -262,6 +262,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
### Command Palette
|
### Command Palette
|
||||||
|
|
||||||
<table style="width:100%">
|
<table style="width:100%">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Event Name</th>
|
<th>Event Name</th>
|
||||||
@@ -315,6 +316,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
|||||||
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
|
||||||
<td>Triggered when the Command Palette process is started.</td>
|
<td>Triggered when the Command Palette process is started.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Microsoft.PowerToys.CmdPal_ExtensionInvoked</td>
|
||||||
|
<td>Tracks extension usage including extension ID, command details, success status, and execution time.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Microsoft.PowerToys.CmdPal_SessionDuration</td>
|
||||||
|
<td>Logs session metrics from launch to dismissal including duration, commands executed, pages visited, search queries, navigation depth, and errors.</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
### Crop And Lock
|
### Crop And Lock
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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 extension command or page is invoked.
|
||||||
|
/// Captures extension usage metrics for telemetry tracking.
|
||||||
|
/// </summary>
|
||||||
|
public record ExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);
|
||||||
@@ -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);
|
||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Telemetry message sent when command invocation begins.
|
||||||
|
/// </summary>
|
||||||
|
public record TelemetryBeginInvokeMessage;
|
||||||
@@ -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>
|
||||||
|
/// Telemetry message sent when an extension command or page is invoked.
|
||||||
|
/// Captures extension usage metrics for telemetry tracking.
|
||||||
|
/// </summary>
|
||||||
|
public record TelemetryExtensionInvokedMessage(string ExtensionId, string CommandId, string CommandName, bool Success, ulong ExecutionTimeMs);
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Telemetry message sent when command invocation completes with a result.
|
||||||
|
/// </summary>
|
||||||
|
public record TelemetryInvokeResultMessage(Microsoft.CommandPalette.Extensions.CommandResultKind Kind);
|
||||||
@@ -270,8 +270,18 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
var isMainPage = command == _rootPage;
|
var isMainPage = command == _rootPage;
|
||||||
_isNested = !isMainPage;
|
_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<TelemetryExtensionInvokedMessage>(
|
||||||
|
new(extensionId, commandId, commandName, true, 0));
|
||||||
|
}
|
||||||
|
|
||||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
// 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)
|
if (pageViewModel is null)
|
||||||
{
|
{
|
||||||
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||||
@@ -306,7 +316,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
{
|
{
|
||||||
CoreLogger.LogDebug($"Invoking command");
|
CoreLogger.LogDebug($"Invoking command");
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send<BeginInvokeMessage>();
|
WeakReferenceMessenger.Default.Send<TelemetryBeginInvokeMessage>();
|
||||||
StartInvoke(message, invokable, host);
|
StartInvoke(message, invokable, host);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,6 +349,14 @@ 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 command = message.Command.Unsafe;
|
||||||
|
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
|
||||||
|
string commandId = command?.Id ?? "unknown";
|
||||||
|
string commandName = command?.Name ?? "unknown";
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Call out to extension process.
|
// 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.
|
// But if it did succeed, we need to handle the result.
|
||||||
UnsafeHandleCommandResult(result);
|
UnsafeHandleCommandResult(result);
|
||||||
|
|
||||||
|
success = true;
|
||||||
_handleInvokeTask = null;
|
_handleInvokeTask = null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// Telemetry: Send extension invocation metrics (always sent, even on failure)
|
||||||
|
stopwatch.Stop();
|
||||||
|
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
|
||||||
|
new(extensionId, commandId, commandName, success, (ulong)stopwatch.ElapsedMilliseconds));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnsafeHandleCommandResult(ICommandResult? result)
|
private void UnsafeHandleCommandResult(ICommandResult? result)
|
||||||
@@ -372,7 +402,7 @@ public partial class ShellViewModel : ObservableObject,
|
|||||||
var kind = result.Kind;
|
var kind = result.Kind;
|
||||||
CoreLogger.LogDebug($"handling {kind.ToString()}");
|
CoreLogger.LogDebug($"handling {kind.ToString()}");
|
||||||
|
|
||||||
WeakReferenceMessenger.Default.Send<CmdPalInvokeResultMessage>(new(kind));
|
WeakReferenceMessenger.Default.Send<TelemetryInvokeResultMessage>(new(kind));
|
||||||
switch (kind)
|
switch (kind)
|
||||||
{
|
{
|
||||||
case CommandResultKind.Dismiss:
|
case CommandResultKind.Dismiss:
|
||||||
|
|||||||
@@ -34,6 +34,6 @@ public sealed partial class CommandPaletteHost : AppExtensionHost, IExtensionHos
|
|||||||
|
|
||||||
public override string? GetExtensionDisplayName()
|
public override string? GetExtensionDisplayName()
|
||||||
{
|
{
|
||||||
return Extension?.ExtensionDisplayName;
|
return Extension?.ExtensionDisplayName ?? _builtInProvider?.DisplayName ?? _builtInProvider?.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,6 +379,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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks extension usage with extension name and invocation details.
|
||||||
|
/// Purpose: Identify popular vs. unused plugins and track extension performance.
|
||||||
|
/// </summary>
|
||||||
|
[EventData]
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||||
|
public class CmdPalExtensionInvoked : EventBase, IEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the extension provider.
|
||||||
|
/// </summary>
|
||||||
|
public string ExtensionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the non-localized identifier of the command being invoked.
|
||||||
|
/// </summary>
|
||||||
|
public string CommandId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the localized display name of the command being invoked.
|
||||||
|
/// </summary>
|
||||||
|
public string CommandName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether the command executed successfully.
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the execution time in milliseconds.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ public class CmdPalInvokeResult : EventBase, IEvent
|
|||||||
|
|
||||||
public CmdPalInvokeResult(CommandResultKind resultKind)
|
public CmdPalInvokeResult(CommandResultKind resultKind)
|
||||||
{
|
{
|
||||||
|
EventName = "CmdPal_InvokeResult";
|
||||||
ResultKind = resultKind.ToString();
|
ResultKind = resultKind.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -6,40 +6,78 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Microsoft.CmdPal.Core.Common.Services;
|
using Microsoft.CmdPal.Core.Common.Services;
|
||||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||||
using Microsoft.CmdPal.UI.Events;
|
using Microsoft.CmdPal.UI.Events;
|
||||||
|
using Microsoft.CommandPalette.Extensions;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
|
||||||
namespace Microsoft.CmdPal.UI;
|
namespace Microsoft.CmdPal.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TelemetryForwarder is responsible for forwarding telemetry events from the
|
/// TelemetryForwarder is responsible for forwarding telemetry events from the
|
||||||
/// command palette core to PowerToys Telemetry.
|
/// command palette to PowerToys Telemetry.
|
||||||
/// This allows us to emit telemetry events as messages from the core,
|
/// Listens to telemetry-specific messages from the core layer and logs them to PowerToys telemetry.
|
||||||
/// and then handle them by logging to our PT telemetry provider.
|
/// Also implements ITelemetryService for dependency injection in extensions.
|
||||||
///
|
|
||||||
/// We may in the future want to replace this with a more generic "ITelemetryService"
|
|
||||||
/// or something similar, but this works for now.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class TelemetryForwarder :
|
internal sealed class TelemetryForwarder :
|
||||||
ITelemetryService,
|
ITelemetryService,
|
||||||
IRecipient<BeginInvokeMessage>,
|
IRecipient<TelemetryBeginInvokeMessage>,
|
||||||
IRecipient<CmdPalInvokeResultMessage>
|
IRecipient<TelemetryInvokeResultMessage>,
|
||||||
|
IRecipient<TelemetryExtensionInvokedMessage>
|
||||||
{
|
{
|
||||||
public TelemetryForwarder()
|
public TelemetryForwarder()
|
||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Register<BeginInvokeMessage>(this);
|
WeakReferenceMessenger.Default.Register<TelemetryBeginInvokeMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<CmdPalInvokeResultMessage>(this);
|
WeakReferenceMessenger.Default.Register<TelemetryInvokeResultMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<TelemetryExtensionInvokedMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Receive(CmdPalInvokeResultMessage message)
|
// Message handlers for telemetry events from core layer
|
||||||
{
|
public void Receive(TelemetryBeginInvokeMessage message)
|
||||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalInvokeResult(message.Kind));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(BeginInvokeMessage message)
|
|
||||||
{
|
{
|
||||||
PowerToysTelemetry.Log.WriteEvent(new BeginInvoke());
|
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)
|
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));
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
IRecipient<ShowWindowMessage>,
|
IRecipient<ShowWindowMessage>,
|
||||||
IRecipient<HideWindowMessage>,
|
IRecipient<HideWindowMessage>,
|
||||||
IRecipient<QuitMessage>,
|
IRecipient<QuitMessage>,
|
||||||
|
IRecipient<NavigateToPageMessage>,
|
||||||
|
IRecipient<NavigationDepthMessage>,
|
||||||
|
IRecipient<SearchQueryMessage>,
|
||||||
|
IRecipient<ErrorOccurredMessage>,
|
||||||
IRecipient<DragStartedMessage>,
|
IRecipient<DragStartedMessage>,
|
||||||
IRecipient<DragCompletedMessage>,
|
IRecipient<DragCompletedMessage>,
|
||||||
IDisposable
|
IDisposable
|
||||||
@@ -75,6 +79,14 @@ public sealed partial class MainWindow : WindowEx,
|
|||||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||||
private bool _themeServiceInitialized;
|
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 DesktopAcrylicController? _acrylicController;
|
||||||
private SystemBackdropConfiguration? _configurationSource;
|
private SystemBackdropConfiguration? _configurationSource;
|
||||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||||
@@ -123,6 +135,10 @@ 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<NavigateToPageMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<SearchQueryMessage>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<ErrorOccurredMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
WeakReferenceMessenger.Default.Register<DragStartedMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
WeakReferenceMessenger.Default.Register<DragCompletedMessage>(this);
|
||||||
|
|
||||||
@@ -524,6 +540,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +553,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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -551,10 +573,67 @@ 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(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();
|
||||||
|
TelemetryForwarder.LogSessionDuration(
|
||||||
|
(ulong)_sessionStopwatch.ElapsedMilliseconds,
|
||||||
|
_sessionCommandsExecuted,
|
||||||
|
_sessionPagesVisited,
|
||||||
|
dismissalReason,
|
||||||
|
_sessionSearchQueriesCount,
|
||||||
|
_sessionMaxNavigationDepth,
|
||||||
|
_sessionErrorCount);
|
||||||
|
_sessionStopwatch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increments the session commands executed counter for telemetry.
|
||||||
|
/// Called by TelemetryForwarder when an extension command is invoked.
|
||||||
|
/// </summary>
|
||||||
|
internal void IncrementCommandsExecuted()
|
||||||
|
{
|
||||||
|
_sessionCommandsExecuted++;
|
||||||
|
}
|
||||||
|
|
||||||
private void HideWindow()
|
private void HideWindow()
|
||||||
{
|
{
|
||||||
// Cloak our HWND to avoid all animations.
|
// Cloak our HWND to avoid all animations.
|
||||||
@@ -764,6 +843,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());
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user