CmdPal: GEH per partes; part 1: error report builder, sanitizer and internals tools setting page (#44140)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR adds three parts of the original big bad global error handler
(error report builder, sanitization and internal tools UI).

### Error Report Generation

- `ErrorReportBuilder`: Produces a detailed, technical report with
system context.
- Comprehensive data: OS version, architecture, culture, app version,
elevation status, etc.
- Exception analysis: Coalesces nested exception messages and HRESULT
details for clearer diagnostics.

<details><summary>Example</summary>
<pre>

This is an error report generated by Windows Command Palette.
If you are seeing this, it means something went a little sideways in the
app.
You can help us fix it by filing a report at
https://aka.ms/powerToysReportBug.

(While you’re at it, give the details below a quick skim — just to make
sure there’s nothing personal you’d prefer not to share. It’s rare, but
sometimes little surprises sneak in.)
============================================================
Summary:
  Message:               Test exception; thrown from the UI thread
  Type:                  System.NotImplementedException
  Source:                Microsoft.CmdPal.UI
  Time:                  2025-08-25 18:54:44.3854569
  HRESULT:               0x80004001 (-2147467263)
  Context:               MainThreadException

Application:
  App version:           0.0.1.0
  Is elevated:           no

Environment:
  OS version:            Microsoft Windows 10.0.26120
  OS architecture:       X64
  Runtime identifier:    win-x64
  Framework:             .NET 9.0.8
  Process architecture:  X64
  Culture:               cs-CZ
  UI culture:            en-US

Stack Trace:
at
Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object
sender, RoutedEventArgs e)
at
WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object
sender, RoutedEventArgs e)
at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr e)

------------------ Full Exception Details ------------------
System.NotImplementedException: Test exception; thrown from the UI
thread
at
Microsoft.CmdPal.UI.Settings.InternalPage.ThrowPlainMainThreadException_Click(Object
sender, RoutedEventArgs e)
at
WinRT._EventSource_global__Microsoft_UI_Xaml_RoutedEventHandler.EventState.<GetEventInvoke>b__1_0(Object
sender, RoutedEventArgs e)
at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr e)

============================================================

</pre>
</details> 

Real-world example: #41362

### PII Sanitization Framework

- `ErrorReportSanitizer`: Multi-layer sanitization pipeline for
sensitive data.
- Nine specialized rule providers:
- `PiiRuleProvider`: Personally identifiable information (emails, phone
numbers, SSNs).
- `ProfilePathAndUsernameRuleProvider`: Windows user profiles and
usernames.
- `NetworkRuleProvider`: IP addresses, MAC addresses, network
identifiers.
- `SecretKeyValueRulesProvider`: API keys, tokens, passwords in
key/value formats.
  - `FilenameMaskRuleProvider`: Sensitive file paths and extensions.
  - `UrlRuleProvider`: URLs and web addresses.
  - `TokenRuleProvider`: JWT and other auth tokens.
  - `ConnectionStringRuleProvider`: Database connection strings.
- `EnvironmentPropertiesRuleProvider`: Environment variables and system
properties.

### Internals Tools Page

A page in settings available in non-CI-builds:

<img width="1305" height="745" alt="image"
src="https://github.com/user-attachments/assets/3145ecfd-997f-491d-8c8a-6096634b6045"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2026-01-29 04:09:37 +01:00
committed by GitHub
parent f82afdf384
commit 8ec530c65e
50 changed files with 2529 additions and 35 deletions

View File

@@ -0,0 +1,45 @@
// 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.UI.Settings;
public partial class InternalPage
{
internal static class SampleData
{
internal static string ExceptionMessageWithPii { get; } =
$"""
Test exception with personal information; thrown from the UI thread
Here is e-mail address <jane.doe@contoso.com>
IPv4 address: 192.168.100.1
IPv4 loopback address: 127.0.0.1
MAC address: 00-14-22-01-23-45
IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
IPv6 loopback address: ::1
Password: P@ssw0rd123!
Password=secret
Api key: 1234567890abcdef
PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb
InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com;
X-API-key: 1234567890abcdef
Pet-Shop-Subscription-Key: 1234567890abcdef
Here is a user name {Environment.UserName}
And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\Pictures
Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal
Here is machine name {Environment.MachineName}
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
User email john.doe@company.com failed validation
File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt
Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test
Phone number 555-123-4567 is invalid
API key abc123def456ghi789jkl012mno345pqr678 expired
Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123
Error accessing file://C:/Users/john.doe/Documents/confidential.pdf
JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret
FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv
Email service error: mailto:admin@internal-company.com?subject=Alert
""";
}
}

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Microsoft.CmdPal.UI.Settings.InternalPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1">
<Grid Padding="16">
<StackPanel
MaxWidth="1000"
HorizontalAlignment="Stretch"
Spacing="{StaticResource SettingsCardSpacing}">
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="Tools on this page are for internal use only. This page is not visible in CI builds." />
<!-- Exception Handling Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Exception Handling" />
<controls:SettingsExpander
Description="Actions for testing global exception handling from the application"
Header="Throw exceptions"
HeaderIcon="{ui:FontIcon Glyph=&#xE783;}"
IsExpanded="True">
<controls:SettingsExpander.Items>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread">
<Button Click="ThrowPlainMainThreadException_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Header="Throw an unhandled exception from the UI thread (with PII)">
<Button Click="ThrowPlainMainThreadExceptionPii_Click" Content="Throw" />
</controls:SettingsCard>
<controls:SettingsCard Description="Throw with delay, when the task is collected by the GC" Header="Throw unobserved exception from a task">
<Button Click="ThrowExceptionInUnobservedTask_Click" Content="Throw" />
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<!-- Diagnostics Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Diagnostics" />
<controls:SettingsCard
x:Name="LogsSettingsCard"
Header="Logs folder"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}">
<Button Click="OpenLogsCardClicked" Content="Open folder" />
</controls:SettingsCard>
<controls:SettingsCard
x:Name="CurrentLogFileSettingsCard"
Header="Current log file"
HeaderIcon="{ui:FontIcon Glyph=&#xF7BB;}">
<Button Click="OpenCurrentLogCardClicked" Content="Open log" />
</controls:SettingsCard>
<!-- Data Section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Data and Files" />
<controls:SettingsCard
x:Name="ConfigurationFolderSettingsCard"
Header="Configuration folder"
HeaderIcon="{ui:FontIcon Glyph=&#xF73D;}">
<Button Click="OpenConfigFolderCardClick" Content="Open folder" />
</controls:SettingsCard>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,92 @@
// 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 ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Xaml;
using Windows.System;
using Page = Microsoft.UI.Xaml.Controls.Page;
namespace Microsoft.CmdPal.UI.Settings;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class InternalPage : Page
{
public InternalPage()
{
InitializeComponent();
}
private void ThrowPlainMainThreadException_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Throwing test exception from the UI thread");
throw new NotImplementedException("Test exception; thrown from the UI thread");
}
private void ThrowExceptionInUnobservedTask_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Starting a task that will throw test exception");
Task.Run(() =>
{
Logger.LogDebug("Throwing test exception from a task");
throw new InvalidOperationException("Test exception; thrown from a task");
});
}
private void ThrowPlainMainThreadExceptionPii_Click(object sender, RoutedEventArgs e)
{
Logger.LogDebug("Throwing test exception from the UI thread (PII)");
throw new InvalidOperationException(SampleData.ExceptionMessageWithPii);
}
private async void OpenLogsCardClicked(object sender, RoutedEventArgs e)
{
try
{
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
if (Directory.Exists(logFolderPath))
{
await Launcher.LaunchFolderPathAsync(logFolderPath);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open directory in Explorer", ex);
}
}
private async void OpenCurrentLogCardClicked(object sender, RoutedEventArgs e)
{
try
{
var logPath = Logger.CurrentLogFile;
if (File.Exists(logPath))
{
await Launcher.LaunchUriAsync(new Uri(logPath));
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open log file", ex);
}
}
private async void OpenConfigFolderCardClick(object sender, RoutedEventArgs e)
{
try
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
if (Directory.Exists(directory))
{
await Launcher.LaunchFolderPathAsync(directory);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to open directory in Explorer", ex);
}
}
}

View File

@@ -72,6 +72,7 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
<!-- "Internal Tools" page item is added dynamically from code -->
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>

View File

@@ -30,6 +30,8 @@ public sealed partial class SettingsWindow : WindowEx,
{
private readonly LocalKeyboardListener _localKeyboardListener;
private readonly NavigationViewItem? _internalNavItem;
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded.
@@ -54,6 +56,23 @@ public sealed partial class SettingsWindow : WindowEx,
_localKeyboardListener.Start();
Closed += SettingsWindow_Closed;
RootElement.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler(RootElement_OnPointerPressed), true);
if (!BuildInfo.IsCiBuild)
{
_internalNavItem = new NavigationViewItem
{
Content = "Internal Tools",
Icon = new FontIcon { Glyph = "\uEC7A" },
Tag = "Internal",
};
NavView.MenuItems.Add(_internalNavItem);
}
else
{
_internalNavItem = null;
}
Navigate("General");
}
private void SettingsWindow_Closed(object sender, WindowEventArgs args)
@@ -68,9 +87,6 @@ public sealed partial class SettingsWindow : WindowEx,
// Delay necessary to ensure NavigationView visual state can match navigation
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
NavView.SelectedItem = NavView.MenuItems[0];
Navigate("General");
if (sender is NavigationView navigationView)
{
// Register for pane open/close changes to announce to screen readers
@@ -96,15 +112,33 @@ public sealed partial class SettingsWindow : WindowEx,
Navigate((selectedItem.Tag as string)!);
}
private void Navigate(string page)
internal void Navigate(string page)
{
var pageType = page switch
Type? pageType;
switch (page)
{
"General" => typeof(GeneralPage),
"Appearance" => typeof(AppearancePage),
"Extensions" => typeof(ExtensionsPage),
_ => null,
};
case "General":
pageType = typeof(GeneralPage);
break;
case "Appearance":
pageType = typeof(AppearancePage);
break;
case "Extensions":
pageType = typeof(ExtensionsPage);
break;
case "Internal":
pageType = typeof(InternalPage);
break;
case "":
// intentional no-op: empty tag means no navigation
pageType = null;
break;
default:
// unknown page, no-op and log
pageType = null;
Logger.LogError($"Unknown settings page tag '{page}'");
break;
}
if (pageType is not null)
{
@@ -268,6 +302,12 @@ public sealed partial class SettingsWindow : WindowEx,
BreadCrumbs.Add(new(extensionsPageType, extensionsPageType));
BreadCrumbs.Add(new(vm.DisplayName, vm));
}
else if (e.SourcePageType == typeof(InternalPage) && _internalNavItem is not null)
{
NavView.SelectedItem = _internalNavItem;
var pageType = "Internal";
BreadCrumbs.Add(new(pageType, pageType));
}
else
{
BreadCrumbs.Add(new($"[{e.SourcePageType?.Name}]", string.Empty));