From 44d34e45c037ae5e5f366fdbfb97da259a6aa036 Mon Sep 17 00:00:00 2001 From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:27:01 +0800 Subject: [PATCH] Add telemetry for shortcut conflict detection feature. (#41271) ## Summary of the Pull Request Add telemetry for shortcut conflict detection. ## PR Checklist - [ ] Closes: #xxx - [ ] **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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Signed-off-by: Shawn Yuan Signed-off-by: Shuai Yuan --- .../ShortcutConflictControlClickedEvent.cs | 20 ++++++ .../Events/ShortcutConflictDetectedEvent.cs | 20 ++++++ .../Events/ShortcutConflictResolvedEvent.cs | 20 ++++++ .../Dashboard/ShortcutConflictControl.xaml.cs | 21 ++++++ .../ShortcutControl/ShortcutControl.xaml.cs | 59 +++++++++++++++++ .../MouseWithoutBordersViewModel.cs | 65 ++++++++++--------- 6 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs create mode 100644 src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs create mode 100644 src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs new file mode 100644 index 0000000000..3f5b8e9964 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictControlClickedEvent.cs @@ -0,0 +1,20 @@ +// 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.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictControlClickedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public int ConflictCount { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs new file mode 100644 index 0000000000..b8d7c13497 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictDetectedEvent.cs @@ -0,0 +1,20 @@ +// 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.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictDetectedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public int ConflictCount { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs new file mode 100644 index 0000000000..7f5bf56e82 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ShortcutConflictResolvedEvent.cs @@ -0,0 +1,20 @@ +// 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.PowerToys.Settings.UI.Library.Telemetry.Events +{ + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class ShortcutConflictResolvedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + + public string Source { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs index 25643e0c64..7195b159e1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs @@ -7,7 +7,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard; +using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Windows.ApplicationModel.Resources; @@ -18,6 +20,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { private static readonly ResourceLoader ResourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + private static bool _telemetryEventSent; + public static readonly DependencyProperty AllHotkeyConflictsDataProperty = DependencyProperty.Register( nameof(AllHotkeyConflictsData), @@ -92,6 +96,17 @@ namespace Microsoft.PowerToys.Settings.UI.Controls // Update visibility based on conflict count Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed; + + if (!_telemetryEventSent && HasConflicts) + { + // Log telemetry event when conflicts are detected + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictDetectedEvent() + { + ConflictCount = ConflictCount, + }); + + _telemetryEventSent = true; + } } private void OnPropertyChanged(string propertyName) @@ -115,6 +130,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls return; } + // Log telemetry event when user clicks the shortcut conflict button + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictControlClickedEvent() + { + ConflictCount = this.ConflictCount, + }); + // Create and show the new window instead of dialog var conflictWindow = new ShortcutConflictWindow(); diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index 3e3df56690..5b21743d5f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -10,17 +10,26 @@ using CommunityToolkit.WinUI; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using Microsoft.Windows.ApplicationModel.Resources; using Windows.System; namespace Microsoft.PowerToys.Settings.UI.Controls { + public enum ShortcutControlSource + { + SettingsPage, + ConflictWindow, + } + public sealed partial class ShortcutControl : UserControl, IDisposable { private readonly UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555; @@ -43,6 +52,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged)); public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged)); + // Dependency property to track the source/context of the ShortcutControl + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ShortcutControlSource), typeof(ShortcutControl), new PropertyMetadata(ShortcutControlSource.SettingsPage)); + private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -74,6 +86,47 @@ namespace Microsoft.PowerToys.Settings.UI.Controls } control.UpdateKeyVisualStyles(); + + // Check if conflict was resolved (had conflict before, no conflict now) + var oldValue = (bool)(e.OldValue ?? false); + var newValue = (bool)(e.NewValue ?? false); + + // General conflict resolution telemetry (for all sources) + if (oldValue && !newValue) + { + // Determine the actual source based on the control's context + var actualSource = DetermineControlSource(control); + + // Conflict was resolved - send general telemetry + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictResolvedEvent() + { + Source = actualSource.ToString(), + }); + } + } + + private static ShortcutControlSource DetermineControlSource(ShortcutControl control) + { + // Walk up the visual tree to find the parent window/container + DependencyObject parent = control; + while (parent != null) + { + parent = VisualTreeHelper.GetParent(parent); + + // Check if we're in a ShortcutConflictWindow + if (parent != null && parent.GetType().Name == "ShortcutConflictWindow") + { + return ShortcutControlSource.ConflictWindow; + } + + if (parent != null && (parent.GetType().Name == "MainWindow" || parent.GetType().Name == "ShellPage")) + { + return ShortcutControlSource.SettingsPage; + } + } + + // Fallback to the explicitly set value or default + return ShortcutControlSource.ConflictWindow; } private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -108,6 +161,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(TooltipProperty, value); } + public ShortcutControlSource Source + { + get => (ShortcutControlSource)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + public bool Enabled { get diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs index 496a8712a1..3d81acd81d 100644 --- a/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/MouseWithoutBordersViewModel.cs @@ -29,7 +29,7 @@ using Windows.ApplicationModel.DataTransfer; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class MouseWithoutBordersViewModel : PageViewModelBase, IDisposable + public partial class MouseWithoutBordersViewModel : PageViewModelBase { protected override string ModuleName => MouseWithoutBordersSettings.ModuleName; @@ -43,6 +43,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private readonly Lock _machineMatrixStringLock = new(); + private bool _disposed; + private static readonly Dictionary StatusColors = new Dictionary() { { SocketStatus.NA, new SolidColorBrush(ColorHelper.FromArgb(0, 0x71, 0x71, 0x71)) }, @@ -1262,38 +1264,43 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels protected override void Dispose(bool disposing) { - if (disposing) + if (!_disposed) { - // Cancel the cancellation token source - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); + if (disposing) + { + // Cancel the cancellation token source + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); - // Wait for the machine polling task to complete - try - { - _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); - } - catch (AggregateException) - { - // Task was cancelled, which is expected + // Wait for the machine polling task to complete + try + { + _machinePollingThreadTask?.Wait(TimeSpan.FromSeconds(1)); + } + catch (AggregateException) + { + // Task was cancelled, which is expected + } + + // Dispose the named pipe stream + try + { + syncHelperStream?.Dispose(); + } + catch (Exception ex) + { + Logger.LogError($"Error disposing sync helper stream: {ex}"); + } + finally + { + syncHelperStream = null; + } + + // Dispose the semaphore + _ipcSemaphore?.Dispose(); } - // Dispose the named pipe stream - try - { - syncHelperStream?.Dispose(); - } - catch (Exception ex) - { - Logger.LogError($"Error disposing sync helper stream: {ex}"); - } - finally - { - syncHelperStream = null; - } - - // Dispose the semaphore - _ipcSemaphore?.Dispose(); + _disposed = true; } base.Dispose(disposing);