diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index 2caee8edfda1..62446dbd5c0a 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -108,6 +108,7 @@ Thank you for using PowerToys! | Microsoft.PowerToys.AwakeIndefinitelyKeepAwakeEvent | Triggered when the system is set to stay awake indefinitely. | | Microsoft.PowerToys.AwakeNoKeepAwakeEvent | Occurs when Awake is turned off, allowing the computer to enter sleep mode. | | Microsoft.PowerToys.AwakeTimedKeepAwakeEvent | Triggered when the system is kept awake for a specified time duration. | +| Microsoft.PowerToys.Awake_CLICommand | Triggered when an Awake CLI command is executed, logging the command name and success status. | ### Color Picker @@ -204,6 +205,7 @@ Thank you for using PowerToys! | Microsoft.PowerToys.FileLocksmith_Invoked | Occurs when File Locksmith is invoked. | | Microsoft.PowerToys.FileLocksmith_InvokedRet | Triggered when File Locksmith invocation returns a result. | | Microsoft.PowerToys.FileLocksmith_QueryContextMenuError | Occurs when there is an error querying the context menu for File Locksmith. | +| Microsoft.PowerToys.FileLocksmith_CLICommand | Triggered when a File Locksmith CLI command is executed, logging the operation mode (query, kill, query-wait, query-json, or help) and success status. | ### FileExplorerAddOns @@ -258,6 +260,7 @@ Thank you for using PowerToys! | Microsoft.PowerToys.ImageResizer_Invoked | Occurs when Image Resizer is invoked by the user. | | Microsoft.PowerToys.ImageResizer_InvokedRet | Fires when the Image Resizer operation is completed and returns a result. | | Microsoft.PowerToys.ImageResizer_QueryContextMenuError | Triggered when there is an error querying the context menu for Image Resizer. | +| Microsoft.PowerToys.ImageResizer_CLICommand | Triggered when an Image Resizer CLI command is executed, logging the command name and success status. | ### Keyboard Manager diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp index 17015e1ea336..39d9fec0d2d7 100644 --- a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp +++ b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.cpp @@ -121,7 +121,7 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr if (argc < 2) { Logger::warn("No arguments provided"); - return { 1, get_usage(strings) }; + return { 1, get_usage(strings), L"help" }; } bool json_output = false; @@ -156,18 +156,18 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr catch (...) { Logger::error("Invalid timeout value"); - return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT) }; + return { 1, strings.GetString(IDS_ERROR_INVALID_TIMEOUT), L"query-wait" }; } } else { Logger::error("Timeout argument missing"); - return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG) }; + return { 1, strings.GetString(IDS_ERROR_TIMEOUT_ARG), L"query-wait" }; } } else if (arg == L"--help") { - return { 0, get_usage(strings) }; + return { 0, get_usage(strings), L"help" }; } else { @@ -178,7 +178,7 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr if (paths.empty()) { Logger::error("No paths specified"); - return { 1, strings.GetString(IDS_ERROR_NO_PATHS) }; + return { 1, strings.GetString(IDS_ERROR_NO_PATHS), L"query" }; } Logger::info("Processing {} paths", paths.size()); @@ -213,13 +213,13 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr { Logger::warn("Timeout waiting for files to be unlocked"); ss << strings.GetString(IDS_TIMEOUT); - return { 1, ss.str() }; + return { 1, ss.str(), L"query-wait" }; } } Sleep(200); } - return { 0, ss.str() }; + return { 0, ss.str(), L"query-wait" }; } auto results = finder.find(paths); @@ -244,5 +244,6 @@ CommandResult run_command(int argc, wchar_t* argv[], IProcessFinder& finder, IPr output_ss << get_text(results, strings); } - return { 0, output_ss.str() }; + std::wstring cmd_name = kill ? L"kill" : (json_output ? L"query-json" : L"query"); + return { 0, output_ss.str(), cmd_name }; } diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h index c8f519592f33..eba9003c364f 100644 --- a/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h +++ b/src/modules/FileLocksmith/FileLocksmithCLI/CLILogic.h @@ -8,6 +8,7 @@ struct CommandResult { int exit_code; std::wstring output; + std::wstring command_name; }; struct IProcessFinder diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp index 67a4304b4e0a..15a8c4b4a138 100644 --- a/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp +++ b/src/modules/FileLocksmith/FileLocksmithCLI/main.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "CLILogic.h" #include "FileLocksmithLib/FileLocksmith.h" +#include "FileLocksmithLib/Trace.h" #include #include "resource.h" #include @@ -47,6 +48,7 @@ struct RealStringProvider : IStringProvider int wmain(int argc, wchar_t* argv[]) { winrt::init_apartment(); + Trace::RegisterProvider(); LoggerHelpers::init_logger(L"FileLocksmithCLI", L"", LogSettings::fileLocksmithLoggerName); Logger::info("FileLocksmithCLI started"); @@ -65,7 +67,10 @@ int wmain(int argc, wchar_t* argv[]) Logger::info("Command succeeded"); } + Trace::CLICommand(result.command_name.c_str(), result.exit_code == 0); + std::wcout << result.output; + Trace::UnregisterProvider(); return result.exit_code; } #endif diff --git a/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp index a67e42badf64..da58127e4df6 100644 --- a/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp +++ b/src/modules/FileLocksmith/FileLocksmithCLI/tests/FileLocksmithCLITests.cpp @@ -52,6 +52,7 @@ namespace FileLocksmithCLIUnitTests auto result = run_command(1, argv, finder, terminator, strings); Assert::AreEqual(1, result.exit_code); + Assert::AreEqual(std::wstring(L"help"), result.command_name); } TEST_METHOD(TestHelp) @@ -64,6 +65,7 @@ namespace FileLocksmithCLIUnitTests auto result = run_command(2, argv, finder, terminator, strings); Assert::AreEqual(0, result.exit_code); + Assert::AreEqual(std::wstring(L"help"), result.command_name); } TEST_METHOD(TestFindProcesses) @@ -77,6 +79,7 @@ namespace FileLocksmithCLIUnitTests auto result = run_command(2, argv, finder, terminator, strings); Assert::AreEqual(0, result.exit_code); + Assert::AreEqual(std::wstring(L"query"), result.command_name); Assert::IsTrue(result.output.find(L"123") != std::wstring::npos); Assert::IsTrue(result.output.find(L"process") != std::wstring::npos); } @@ -94,6 +97,7 @@ namespace FileLocksmithCLIUnitTests Microsoft::VisualStudio::CppUnitTestFramework::Logger::WriteMessage(result.output.c_str()); Assert::AreEqual(0, result.exit_code); + Assert::AreEqual(std::wstring(L"query-json"), result.command_name); Assert::IsTrue(result.output.find(L"\"pid\"") != std::wstring::npos); Assert::IsTrue(result.output.find(L"123") != std::wstring::npos); } @@ -109,6 +113,7 @@ namespace FileLocksmithCLIUnitTests auto result = run_command(3, argv, finder, terminator, strings); Assert::AreEqual(0, result.exit_code); + Assert::AreEqual(std::wstring(L"kill"), result.command_name); Assert::AreEqual((size_t)1, terminator.terminatedPids.size()); Assert::AreEqual((DWORD)123, terminator.terminatedPids[0]); } @@ -125,6 +130,7 @@ namespace FileLocksmithCLIUnitTests auto result = run_command(5, argv, finder, terminator, strings); Assert::AreEqual(1, result.exit_code); + Assert::AreEqual(std::wstring(L"query-wait"), result.command_name); } }; } diff --git a/src/modules/FileLocksmith/FileLocksmithLib/Trace.cpp b/src/modules/FileLocksmith/FileLocksmithLib/Trace.cpp index a3d8e9038e0e..bd2c751e074b 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/Trace.cpp +++ b/src/modules/FileLocksmith/FileLocksmithLib/Trace.cpp @@ -49,3 +49,14 @@ void Trace::QueryContextMenuError(_In_ HRESULT hr) noexcept TraceLoggingHResult(hr), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); } + +void Trace::CLICommand(_In_ PCWSTR commandName, _In_ bool successful) noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + "FileLocksmith_CLICommand", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingWideString(commandName, "CommandName"), + TraceLoggingBoolean(successful, "Successful")); +} diff --git a/src/modules/FileLocksmith/FileLocksmithLib/Trace.h b/src/modules/FileLocksmith/FileLocksmithLib/Trace.h index 98642de854d1..9f8f090b52d2 100644 --- a/src/modules/FileLocksmith/FileLocksmithLib/Trace.h +++ b/src/modules/FileLocksmith/FileLocksmithLib/Trace.h @@ -11,4 +11,5 @@ class Trace : public telemetry::TraceBase static void Invoked() noexcept; static void InvokedRet(_In_ HRESULT hr) noexcept; static void QueryContextMenuError(_In_ HRESULT hr) noexcept; + static void CLICommand(_In_ PCWSTR commandName, _In_ bool successful) noexcept; }; diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs index 6b65e9eea31b..5d3b591c84a3 100644 --- a/src/modules/awake/Awake/Program.cs +++ b/src/modules/awake/Awake/Program.cs @@ -19,6 +19,7 @@ using Awake.Core.Models; using Awake.Core.Native; using Awake.Properties; +using Awake.Telemetry; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Telemetry; @@ -68,6 +69,7 @@ private static async Task Main(string[] args) if (parseResult.Errors.Count > 0) { // Shows errors and returns non-zero. + LogCLITelemetry(successful: false); return rootCommand.Invoke(args); } @@ -96,6 +98,7 @@ private static async Task Main(string[] args) { // Awake is already running - there is no need for us to process // anything further + LogCLITelemetry(successful: false); Exit(Core.Constants.AppName + " is already running! Exiting the application.", 1); return 1; } @@ -103,6 +106,7 @@ private static async Task Main(string[] args) { if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) { + LogCLITelemetry(successful: false); Exit("PowerToys.Awake tried to start with a group policy setting that disables the tool. Please contact your system administrator.", 1); return 1; } @@ -125,7 +129,9 @@ private static async Task Main(string[] args) Bridge.GetPwrCapabilities(out _powerCapabilities); Logger.LogInfo(JsonSerializer.Serialize(_powerCapabilities, _serializerOptions)); - return await rootCommand.InvokeAsync(args); + var result = await rootCommand.InvokeAsync(args); + LogCLITelemetry(successful: result == 0); + return result; } } } @@ -216,6 +222,22 @@ private static RootCommand BuildRootCommand() return rootCommand; } + private static void LogCLITelemetry(bool successful) + { + try + { + PowerToysTelemetry.Log.WriteEvent(new AwakeCLICommandEvent + { + CommandName = "awake", + Successful = successful, + }); + } + catch (Exception ex) + { + Logger.LogError($"Failed to log CLI telemetry: {ex.Message}"); + } + } + private static void AwakeUnhandledExceptionCatcher(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is Exception exception) diff --git a/src/modules/awake/Awake/Telemetry/AwakeCLICommandEvent.cs b/src/modules/awake/Awake/Telemetry/AwakeCLICommandEvent.cs new file mode 100644 index 000000000000..098d3d911450 --- /dev/null +++ b/src/modules/awake/Awake/Telemetry/AwakeCLICommandEvent.cs @@ -0,0 +1,37 @@ +// 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 Awake.Telemetry +{ + /// + /// Telemetry event for Awake CLI command execution. + /// + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class AwakeCLICommandEvent : EventBase, IEvent + { + public AwakeCLICommandEvent() + { + EventName = "Awake_CLICommand"; + CommandName = string.Empty; + } + + /// + /// Gets or sets the name of the CLI command that was executed. + /// + public string CommandName { get; set; } + + /// + /// Gets or sets a value indicating whether the command executed successfully. + /// + public bool Successful { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/imageresizer/ImageResizerCLI/Program.cs b/src/modules/imageresizer/ImageResizerCLI/Program.cs index d24ab93bde01..2876caa32aa7 100644 --- a/src/modules/imageresizer/ImageResizerCLI/Program.cs +++ b/src/modules/imageresizer/ImageResizerCLI/Program.cs @@ -7,7 +7,9 @@ using System.Text; using ImageResizer.Cli; +using ImageResizer.Cli.Telemetry; using ManagedCommon; +using Microsoft.PowerToys.Telemetry; namespace ImageResizerCLI; @@ -37,14 +39,33 @@ private static int Main(string[] args) try { var executor = new ImageResizerCliExecutor(); - return executor.Run(args); + int result = executor.Run(args); + LogCLITelemetry(executor.CommandName, result == 0); + return result; } catch (Exception ex) { CliLogger.Error($"Unhandled exception: {ex.Message}"); CliLogger.Error($"Stack trace: {ex.StackTrace}"); Console.Error.WriteLine($"Fatal error: {ex.Message}"); + LogCLITelemetry("resize", successful: false); return 1; } } + + private static void LogCLITelemetry(string commandName, bool successful) + { + try + { + PowerToysTelemetry.Log.WriteEvent(new ImageResizerCLICommandEvent + { + CommandName = commandName, + Successful = successful, + }); + } + catch (Exception ex) + { + CliLogger.Error($"Failed to log CLI telemetry: {ex.Message}"); + } + } } diff --git a/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs b/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs index 851d3e798314..00d593344adf 100644 --- a/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs +++ b/src/modules/imageresizer/ui/Cli/ImageResizerCliExecutor.cs @@ -19,6 +19,11 @@ namespace ImageResizer.Cli /// public class ImageResizerCliExecutor { + /// + /// Gets the name of the last CLI operation that was executed. + /// + public string CommandName { get; private set; } = "resize"; + /// /// Runs the CLI executor with the provided command-line arguments. /// @@ -37,18 +42,21 @@ public int Run(string[] args) } CliOptions.PrintUsage(); + CommandName = "error"; return 1; } if (cliOptions.ShowHelp) { CliOptions.PrintUsage(); + CommandName = "help"; return 0; } if (cliOptions.ShowConfig) { CliOptions.PrintConfig(Settings.Default); + CommandName = "show-config"; return 0; } @@ -56,6 +64,7 @@ public int Run(string[] args) { Console.WriteLine(Resources.CLI_NoInputFiles); CliOptions.PrintUsage(); + CommandName = "error"; return 1; } diff --git a/src/modules/imageresizer/ui/Cli/Telemetry/ImageResizerCLICommandEvent.cs b/src/modules/imageresizer/ui/Cli/Telemetry/ImageResizerCLICommandEvent.cs new file mode 100644 index 000000000000..fc8450b710f9 --- /dev/null +++ b/src/modules/imageresizer/ui/Cli/Telemetry/ImageResizerCLICommandEvent.cs @@ -0,0 +1,37 @@ +// 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 ImageResizer.Cli.Telemetry +{ + /// + /// Telemetry event for Image Resizer CLI command execution. + /// + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ImageResizerCLICommandEvent : EventBase, IEvent + { + public ImageResizerCLICommandEvent() + { + EventName = "ImageResizer_CLICommand"; + CommandName = string.Empty; + } + + /// + /// Gets or sets the name of the CLI command that was executed. + /// + public string CommandName { get; set; } + + /// + /// Gets or sets a value indicating whether the command executed successfully. + /// + public bool Successful { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +}