Update PowerShell telemetry to respect the diagnostics and feedback setting on Windows (#27328)

This commit is contained in:
Dongbo Wang
2026-04-24 09:52:06 -07:00
committed by GitHub
parent 0458f16cc5
commit 65e6a8055c
6 changed files with 427 additions and 33 deletions
@@ -121,6 +121,18 @@
value="0x3002"
version="1"
/>
<!--Telemetry events-->
<event
channel="C_OPERATIONAL"
level="win:Error"
message="$(string.PS_PROVIDER.event.E_O_TelemetrySettingError.message)"
opcode="Exception"
symbol="TelemetrySettingError"
task="Telemetry"
template="T_TelemetrySettingError"
value="0x3011"
version="1"
/>
<!--M3P events-->
<event
channel="C_ANALYTIC"
@@ -2208,17 +2220,41 @@
value="0x6017"
version="1"
/>
<event
channel="C_ANALYTIC"
keywords="AmsiState"
level="win:Verbose"
message="$(string.PS_PROVIDER.event.E_A_AmsiState.message)"
opcode="Method"
symbol="AmsiState"
task="Amsi"
template="T_AmsiState"
value="0x4001"
version="1"
<event
channel="C_ANALYTIC"
keywords="AmsiState"
level="win:Verbose"
message="$(string.PS_PROVIDER.event.E_A_AmsiState.message)"
opcode="Method"
symbol="AmsiState"
task="Amsi"
template="T_AmsiState"
value="0x4001"
version="1"
/>
<event
channel="C_ANALYTIC"
keywords="WDACQuery"
level="win:Verbose"
message="$(string.PS_PROVIDER.event.E_A_WDACQuery.message)"
opcode="Method"
symbol="WDACQuery"
task="WDAC"
template="T_WDACQuery"
value="0x4002"
version="1"
/>
<event
channel="C_ANALYTIC"
keywords="WDACAudit"
level="win:Verbose"
message = "$(string.PS_PROVIDER.event.E_A_WDACAudit.message)"
opcode="Method"
symbol="WDACAudit"
task="WDACAudit"
template="T_WDACAudit"
value="0x4003"
version="1"
/>
</events>
<channels>
@@ -2409,6 +2445,12 @@
symbol="T_EXPERIMENTALFEATURE"
value="107"
/>
<task
message="$(string.PS_PROVIDER.task.T_Telemetry.message)"
name="Telemetry"
symbol="T_TELEMETRY"
value="108"
/>
<task
message="$(string.PS_PROVIDER.task.T_ScheduledJob.message)"
name="ScheduledJob"
@@ -2427,11 +2469,23 @@
symbol="T_ISEOperation"
value="120"
/>
<task
message="$(string.PS_PROVIDER.task.T_AmsiState.message)"
name="Amsi"
symbol="T_Amsi"
value="130"
<task
message="$(string.PS_PROVIDER.task.T_AmsiState.message)"
name="Amsi"
symbol="T_Amsi"
value="130"
/>
<task
message="$(string.PS_PROVIDER.task.T_WDACQuery.message)"
name="WDAC"
symbol="T_WDAC"
value="131"
/>
<task
message="$(string.PS_PROVIDER.task.T_WDACAudit.message)"
name="WDACAudit"
symbol="T_WDACAudit"
value="132"
/>
</tasks>
<opcodes>
@@ -2593,11 +2647,23 @@
name="PSWorkflow"
symbol="K_PSWORKFLOW"
/>
<keyword
mask="0x400"
message="$(string.PS_PROVIDER.keyword.K_AmsiState.message)"
name="AmsiState"
symbol="K_AmsiState"
<keyword
mask="0x400"
message="$(string.PS_PROVIDER.keyword.K_AmsiState.message)"
name="AmsiState"
symbol="K_AmsiState"
/>
<keyword
mask="0x800"
message="$(string.PS_PROVIDER.keyword.K_WDACQuery.message)"
name="WDACQuery"
symbol="K_WDACQuery"
/>
<keyword
mask="0x1000"
message="$(string.PS_PROVIDER.keyword.K_WDACAudit.message)"
name="WDACAudit"
symbol="K_WDACAudit"
/>
</keywords>
<maps>
@@ -4004,6 +4070,20 @@
name="StackTrace"
/>
</template>
<template tid="T_TelemetrySettingError">
<data
inType="win:UnicodeString"
name="Name"
/>
<data
inType="win:UnicodeString"
name="Message"
/>
<data
inType="win:UnicodeString"
name="StackTrace"
/>
</template>
<template tid="T_TrackingGuid">
<data
inType="win:GUID"
@@ -4080,16 +4160,48 @@
name="FileName"
/>
</template>
<template tid="T_AmsiState">
<data
inType="win:UnicodeString"
name="Action"
<template tid="T_AmsiState">
<data
inType="win:UnicodeString"
name="Action"
/>
<data
inType="win:UnicodeString"
name="AmsiContext"
<data
inType="win:UnicodeString"
name="AmsiContext"
/>
</template>
</template>
<template tid="T_WDACQuery">
<data
inType="win:UnicodeString"
name="QueryName"
/>
<data
inType="win:UnicodeString"
name="FileName"
/>
<data
inType="win:Int32"
name="QuerySuccess"
/>
<data
inType="win:Int32"
name="QuerySResult"
/>
</template>
<template tid="T_WDACAudit">
<data
inType="win:UnicodeString"
name="Title"
/>
<data
inType="win:UnicodeString"
name="Message"
/>
<data
inType="win:UnicodeString"
name="FullyQualifiedId"
/>
</template>
</templates>
</provider>
</events>
@@ -5535,6 +5647,14 @@
id="PS_PROVIDER.task.T_ExperimentalFeature.message"
value="PowerShell Experimental Features"
/>
<string
id="PS_PROVIDER.event.E_O_TelemetrySettingError.message"
value="Failed to retrieve diagnostics and feedback setting from Windows.%n Exception: %1 %n Message: %2 %n StackTrace: %3 %n"
/>
<string
id="PS_PROVIDER.task.T_Telemetry.message"
value="PowerShell Telemetry"
/>
<string
id="PS_PROVIDER.task.T_NamedPipe.message"
value="PowerShell Named Pipe IPC"
@@ -5719,6 +5839,30 @@
id="PS_PROVIDER.event.E_O_REMOTE_NAMEDPIPE_DISCONNECT.message"
value="PowerShell IPC disconnect on process: %1 in AppDomain: %2 for User: %3."
/>
<string
id="PS_PROVIDER.event.E_A_WDACQuery.message"
value="WDAC Query. %n %t Query: %1 %n %t File: %2 %n %t SuccessCode: %3 %n %t ResultCode: %4"
/>
<string
id="PS_PROVIDER.keyword.K_WDACQuery.message"
value="WDAC Query"
/>
<string
id="PS_PROVIDER.task.T_WDACQuery.message"
value="WDAC Query"
/>
<string
id="PS_PROVIDER.event.E_A_WDACAudit.message"
value="WDAC Audit. %n %t Title: %1 %n %t Message: %2 %n %t FullyQualifiedId: %3"
/>
<string
id="PS_PROVIDER.keyword.K_WDACAudit.message"
value="WDAC Audit"
/>
<string
id="PS_PROVIDER.task.T_WDACAudit.message"
value="WDAC Audit"
/>
</stringTable>
</resources>
</localization>
@@ -179,9 +179,12 @@ namespace System.Management.Automation
{
int result = Interop.Windows.CoInitializeEx(IntPtr.Zero, Interop.Windows.COINIT_APARTMENTTHREADED);
// If 0 is returned the thread has been initialized for the first time
// as an STA and thus supported and needs to be uninitialized.
if (result > 0)
// Per COM documentation: Each successful call to CoInitializeEx (including S_FALSE)
// must be balanced by a corresponding call to CoUninitialize.
// - S_OK (0) means we initialized for the first time.
// - S_FALSE (1) means already initialized, but still increments the reference count.
// Both require CoUninitialize to decrement the reference count.
if (result >= 0)
{
Interop.Windows.CoUninitialize();
}
@@ -166,6 +166,9 @@ namespace System.Management.Automation.Internal
ExperimentalFeature_InvalidName = 0x3001,
ExperimentalFeature_ReadConfig_Error = 0x3002,
// Windows Diagnostics And Usage Data Settings
Telemetry_Setting_Error = 0x3011,
// Scheduled Jobs
ScheduledJob_Start = 0xD001,
ScheduledJob_Complete = 0xD002,
@@ -240,6 +243,7 @@ namespace System.Management.Automation.Internal
ProviderStop = 0x69,
ExecutePipeline = 0x6A,
ExperimentalFeature = 0x6B,
Telemetry = 0x6C,
ScheduledJob = 0x6E,
NamedPipe = 0x6F,
ISEOperation = 0x78,
@@ -177,12 +177,20 @@ namespace Microsoft.PowerShell.Telemetry
/// </summary>
static ApplicationInsightsTelemetry()
{
// If we can't send telemetry, there's no reason to do any of this
CanSendTelemetry = !Utils.GetEnvironmentVariableAsBool(name: _telemetryOptoutEnvVar, defaultValue: false)
&& Platform.TryDeriveFromCache("telemetry.uuid", out s_uuidPath);
#if !UNIX
if (CanSendTelemetry)
{
// Respect the diagnostics and feedback setting in Windows.
CanSendTelemetry = WindowsDataCollectionSetting.CanCollectDiagnostics(PlatformDataCollectionLevel.Enhanced);
}
#endif
if (!CanSendTelemetry)
{
// Avoid the initialization work if we can't send telemetry.
return;
}
@@ -0,0 +1,185 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#if !UNIX
using System;
using System.Management.Automation.Internal;
using System.Management.Automation.Tracing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;
namespace Microsoft.PowerShell.Telemetry;
internal enum PlatformDataCollectionLevel : int
{
/// <summary>
/// Minimum — only security-related data. Enterprise/education editions only.
/// </summary>
Security = 0,
/// <summary>
/// Device info, capabilities, and basic reliability data.
/// </summary>
Basic = 1,
/// <summary>
/// More detailed usage and reliability data, including app/feature usage patterns.
/// Removed as a user-facing option in Windows 11 (collapsed into Full).
/// </summary>
Enhanced = 2,
/// <summary>
/// All of the above plus advanced diagnostics data that can help Microsoft fix problems.
/// </summary>
Full = 3,
}
/// <summary>
/// Minimal projection of <c>IInspectable</c>, the base interface for all WinRT objects.
/// Slots 3–5 in every WinRT interface vtable (after <c>IUnknown</c>'s QueryInterface/AddRef/Release).
/// </summary>
[GeneratedComInterface]
[Guid("AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90")]
internal partial interface IInspectable
{
void GetIids(out uint iidCount, out nint iids);
nint GetRuntimeClassName();
int GetTrustLevel();
}
/// <summary>
/// Projection of the WinRT interface <c>Windows.System.Profile.IPlatformDiagnosticsAndUsageDataSettingsStatics</c>
/// (IID B6E24C1B-7B1C-4B32-8C62-A66597CE723A).
/// Vtable slots 6–9, following the three <c>IInspectable</c> slots.
/// </summary>
[GeneratedComInterface]
[Guid("B6E24C1B-7B1C-4B32-8C62-A66597CE723A")]
internal partial interface IPlatformDiagnosticsAndUsageDataSettingsStatics : IInspectable
{
PlatformDataCollectionLevel GetCollectionLevel();
long AddCollectionLevelChanged(nint handler);
void RemoveCollectionLevelChanged(long token);
// WinRT marshals bool as a byte; use byte to avoid any MarshalAs ambiguity with the source generator.
byte CanCollectDiagnostics(PlatformDataCollectionLevel level);
}
/// <summary>
/// Wraps <c>Windows.System.Profile.PlatformDiagnosticsAndUsageDataSettings</c> using compile-time COM interop
/// and source-generated P/Invoke. No extra runtime DLLs are required.
/// </summary>
internal static partial class WindowsDataCollectionSetting
{
/// <summary>
/// Returns <see langword="true"/> if the device's diagnostic data collection policy permits collecting at or above <paramref name="level"/>.
/// </summary>
/// <param name="level">The minimum <see cref="PlatformDataCollectionLevel"/> to test against.</param>
internal static bool CanCollectDiagnostics(PlatformDataCollectionLevel level)
{
const string ClassName = "Windows.System.Profile.PlatformDiagnosticsAndUsageDataSettings";
// When initializing WinRT on the calling thread, use the multi-threaded apartment (MTA).
// This is to cover the case where PowerShell gets used in a thread-pool thread.
// See the doc at https://learn.microsoft.com/windows/win32/api/roapi/ne-roapi-ro_init_type
const int RO_INIT_MULTITHREADED = 1;
// Return values for 'RoInitialize':
// - S_OK (0) - we successfully initialized; must call 'RoUninitialize'.
// - S_FALSE (1) - already initialized with the same apartment type; must still call 'RoUninitialize'.
// - RPC_E_CHANGED_MODE - already initialized with a different apartment type; WinRT still works, do NOT call 'RoUninitialize'.
const int RPC_E_CHANGED_MODE = unchecked((int)0x80010106);
int initHr = -1;
nint hstring = default;
nint factoryPtr = default;
try
{
// Initialize WinRT on the calling thread. 'RoGetActivationFactory' requires it.
initHr = RoInitialize(RO_INIT_MULTITHREADED);
if (initHr < 0 && initHr != RPC_E_CHANGED_MODE)
{
// The call to initialize the Windows Runtime failed.
// Throw an exception with the HRESULT error code to provide more context on the failure.
Marshal.ThrowExceptionForHR(initHr);
}
Marshal.ThrowExceptionForHR(
WindowsCreateString(ClassName, (uint)ClassName.Length, out hstring));
Guid iid = new("B6E24C1B-7B1C-4B32-8C62-A66597CE723A");
Marshal.ThrowExceptionForHR(
RoGetActivationFactory(hstring, ref iid, out factoryPtr));
var comWrappers = new StrategyBasedComWrappers();
var comObject = comWrappers.GetOrCreateObjectForComInstance(factoryPtr, CreateObjectFlags.None);
var platformSetting = (IPlatformDiagnosticsAndUsageDataSettingsStatics)comObject;
return platformSetting.CanCollectDiagnostics(level) != 0;
}
catch (Exception ex)
{
// Log any exceptions that occur during this process, but swallow them and return false to disable telemetry rather than crashing the product.
// This API is only used to gate telemetry collection, so failure should be non-fatal.
PSEtwLog.LogOperationalError(
PSEventId.Telemetry_Setting_Error,
PSOpcode.Exception,
PSTask.Telemetry,
PSKeyword.UseAlwaysOperational,
ex.GetType().FullName,
ex.Message,
ex.StackTrace);
return false;
}
finally
{
if (factoryPtr != default)
{
Marshal.Release(factoryPtr);
}
if (hstring != default)
{
_ = WindowsDeleteString(hstring);
}
// Per COM documentation: Each successful call to 'RoInitialize' (including S_FALSE)
// must be balanced by a corresponding call to 'RoUninitialize'.
if (initHr >= 0)
{
RoUninitialize();
}
}
}
[LibraryImport("api-ms-win-core-winrt-string-l1-1-0.dll", StringMarshalling = StringMarshalling.Utf16)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static partial int WindowsCreateString(
string sourceString,
uint length,
out nint hstring);
[LibraryImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static partial int WindowsDeleteString(nint hstring);
[LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static partial int RoGetActivationFactory(nint activatableClassId, ref Guid iid, out nint factory);
[LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static partial int RoInitialize(int initType);
[LibraryImport("api-ms-win-core-winrt-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static partial void RoUninitialize();
}
#endif
@@ -5,8 +5,53 @@
# these tests aren't going to check that telemetry is being sent
# only that we're not treating the telemetry.uuid file correctly
function Get-OSTelemetryLevel {
<#
.SYNOPSIS
Returns the effective Windows Telemetry level (0-3).
Logic: Checks GPO overrides, then System preferences, then defaults to 1.
#>
$gpoPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection"
$sysPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection"
$valueName = "AllowTelemetry"
# 1. Check the "Managed" Policy (Group Policy)
if (Test-Path $gpoPath) {
$gpoValue = Get-ItemProperty -Path $gpoPath -Name $valueName -ErrorAction SilentlyContinue
if ($gpoValue -and $gpoValue.$valueName) {
return [int]$gpoValue.$valueName
}
}
# 2. Check the "User/System" Preference (Settings App)
if (Test-Path $sysPath) {
$sysValue = Get-ItemProperty -Path $sysPath -Name $valueName -ErrorAction SilentlyContinue
if ($sysValue -and $sysValue.$valueName) {
return [int]$sysValue.$valueName
}
}
# 3. Fallback to OS Default (Basic/Required)
return 1
}
Describe "Telemetry for shell startup" -Tag CI {
BeforeAll {
$skipTelemetryTests = $false
if ($IsWindows) {
## Skip telemetry tests if the OS telemetry level is less than 2 (Enhanced) -- PS telemetry is disabled in this case.
$osTelemetryLevel = Get-OSTelemetryLevel
$skipTelemetryTests = $osTelemetryLevel -lt 2
}
if ($skipTelemetryTests) {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
return
}
# if the telemetry file exists, move it out of the way
# the member is internal, but we can retrieve it via reflection
$cacheDir = [System.Management.Automation.Platform].GetField("CacheDirectory","NonPublic,Static").GetValue($null)
@@ -23,6 +68,11 @@ Describe "Telemetry for shell startup" -Tag CI {
}
AfterAll {
if ($skipTelemetryTests) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
return
}
# check and reset the telemetry.uuid file
if ( $uuidFileExists ) {
if ( Test-Path -Path "${uuidPath}.original" ) {