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..7864c36b1f --- /dev/null +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Messages/ExtensionInvokedMessage.cs @@ -0,0 +1,7 @@ +// 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; + +public record ExtensionInvokedMessage(string ExtensionId, string CommandType, bool Success, ulong ExecutionTimeMs); 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 046d7ea336..928e01cc88 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -269,8 +269,17 @@ public partial class ShellViewModel : ObservableObject, var isMainPage = command == _rootPage; _isNested = !isMainPage; + // Track extension page navigation + if (host is not null) + { + string extensionId = host.GetExtensionDisplayName() ?? "builtin"; + string commandType = command?.Name ?? command?.Id ?? "unknown"; + WeakReferenceMessenger.Default.Send( + new(extensionId, commandType, 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}"); @@ -338,6 +347,12 @@ public partial class ShellViewModel : ObservableObject, private void SafeHandleInvokeCommandSynchronous(PerformCommandMessage message, IInvokableCommand invokable, AppExtensionHost? host) { + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var command = message.Command.Unsafe; + string extensionId = host?.GetExtensionDisplayName() ?? "builtin"; + string commandType = command?.Name ?? command?.Id ?? "unknown"; + bool success = false; + try { // Call out to extension process. @@ -348,16 +363,24 @@ 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; // TODO: It would be better to do this as a page exception, rather // than a silent log message. host?.Log(ex.Message); } + finally + { + stopwatch.Stop(); + WeakReferenceMessenger.Default.Send( + new(extensionId, commandType, success, (ulong)stopwatch.ElapsedMilliseconds)); + } } private void UnsafeHandleCommandResult(ICommandResult? result) 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/Events/CmdPalExtensionInvoked.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs new file mode 100644 index 0000000000..c10fe610a7 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Events/CmdPalExtensionInvoked.cs @@ -0,0 +1,50 @@ +// 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 display name of the command being invoked. + /// + public string CommandType { 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 commandType, bool success, ulong executionTimeMs) + { + EventName = "CmdPal_ExtensionInvoked"; + ExtensionId = extensionId; + CommandType = commandType; + Success = success; + ExecutionTimeMs = executionTimeMs; + } + + 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..b5358f95f7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TelemetryForwarder.cs @@ -22,12 +22,14 @@ namespace Microsoft.CmdPal.UI; internal sealed class TelemetryForwarder : ITelemetryService, IRecipient, - IRecipient + IRecipient, + IRecipient { public TelemetryForwarder() { WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } public void Receive(CmdPalInvokeResultMessage message) @@ -40,6 +42,15 @@ internal sealed class TelemetryForwarder : PowerToysTelemetry.Log.WriteEvent(new BeginInvoke()); } + public void Receive(ExtensionInvokedMessage message) + { + PowerToysTelemetry.Log.WriteEvent(new CmdPalExtensionInvoked( + message.ExtensionId, + message.CommandType, + message.Success, + message.ExecutionTimeMs)); + } + public void LogRunQuery(string query, int resultCount, ulong durationMs) { PowerToysTelemetry.Log.WriteEvent(new CmdPalRunQuery(query, resultCount, durationMs));