Additional Telemetry - Implementation of RFC0036 (#10336)

This commit is contained in:
James Truher [MSFT] 2019-08-15 16:13:53 -07:00 committed by Aditya Patwardhan
parent c8e72d1e66
commit fe2cc6aca8
17 changed files with 610 additions and 124 deletions

View file

@ -1012,7 +1012,7 @@ function Start-PSPester {
# All concatenated commands/arguments are suffixed with the delimiter (space)
# Disable telemetry for all startups of pwsh in tests
$command = "`$env:POWERSHELL_TELEMETRY_OPTOUT = 1;"
$command = "`$env:POWERSHELL_TELEMETRY_OPTOUT = 'yes';"
if ($Terse)
{
$command += "`$ProgressPreference = 'silentlyContinue'; "
@ -1153,7 +1153,7 @@ function Start-PSPester {
try {
$originalModulePath = $env:PSModulePath
$originalTelemetry = $env:POWERSHELL_TELEMETRY_OPTOUT
$env:POWERSHELL_TELEMETRY_OPTOUT = 1
$env:POWERSHELL_TELEMETRY_OPTOUT = 'yes'
if ($Unelevate)
{
Start-UnelevatedProcess -process $powershell -arguments ($PSFlags + "-c $Command")

View file

@ -8,8 +8,6 @@
<ItemGroup>
<ProjectReference Include="..\System.Management.Automation\System.Management.Automation.csproj" />
<!-- the following package(s) are from https://github.com/microsoft/applicationinsights-??? -->
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.10.0" />
</ItemGroup>
<PropertyGroup>

View file

@ -24,6 +24,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.PowerShell.Telemetry;
using ConsoleHandle = Microsoft.Win32.SafeHandles.SafeFileHandle;
using Dbg = System.Management.Automation.Diagnostics;
@ -123,12 +124,8 @@ namespace Microsoft.PowerShell
try
{
string profileDir;
#if UNIX
profileDir = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE);
#else
profileDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell";
string profileDir = Platform.CacheDirectory;
#if ! UNIX
if (!Directory.Exists(profileDir))
{
Directory.CreateDirectory(profileDir);
@ -200,12 +197,14 @@ namespace Microsoft.PowerShell
// First check for and handle PowerShell running in a server mode.
if (s_cpp.ServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("ServerMode");
ProfileOptimization.StartProfile("StartupProfileData-ServerMode");
System.Management.Automation.Remoting.Server.OutOfProcessMediator.Run(s_cpp.InitialCommand);
exitCode = 0;
}
else if (s_cpp.NamedPipeServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("NamedPipe");
ProfileOptimization.StartProfile("StartupProfileData-NamedPipeServerMode");
System.Management.Automation.Remoting.RemoteSessionNamedPipeServer.RunServerMode(
s_cpp.ConfigurationName);
@ -213,12 +212,14 @@ namespace Microsoft.PowerShell
}
else if (s_cpp.SSHServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SSHServer");
ProfileOptimization.StartProfile("StartupProfileData-SSHServerMode");
System.Management.Automation.Remoting.Server.SSHProcessMediator.Run(s_cpp.InitialCommand);
exitCode = 0;
}
else if (s_cpp.SocketServerMode)
{
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SocketServerMode");
ProfileOptimization.StartProfile("StartupProfileData-SocketServerMode");
System.Management.Automation.Remoting.Server.HyperVSocketMediator.Run(s_cpp.InitialCommand,
s_cpp.ConfigurationName);
@ -242,7 +243,7 @@ namespace Microsoft.PowerShell
PSHost.IsStdOutputRedirected = Console.IsOutputRedirected;
// Send startup telemetry for ConsoleHost startup
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry();
ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("Normal");
exitCode = s_theConsoleHost.Run(s_cpp, false);
}

View file

@ -1,102 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
namespace Microsoft.PowerShell
{
/// <summary>
/// Send up telemetry for startup.
/// </summary>
internal static class ApplicationInsightsTelemetry
{
// If this env var is true, yes, or 1, telemetry will NOT be sent.
private const string TelemetryOptoutEnvVar = "POWERSHELL_TELEMETRY_OPTOUT";
// Telemetry client to be reused when we start sending more telemetry
private static TelemetryClient _telemetryClient = null;
// Set this to true to reduce the latency of sending the telemetry
private static bool _developerMode = false;
// PSCoreInsight2 telemetry key
private const string _psCoreTelemetryKey = "ee4b2115-d347-47b0-adb6-b19c2c763808";
static ApplicationInsightsTelemetry()
{
TelemetryConfiguration.Active.InstrumentationKey = _psCoreTelemetryKey;
TelemetryConfiguration.Active.TelemetryChannel.DeveloperMode = _developerMode;
}
private static bool GetEnvironmentVariableAsBool(string name, bool defaultValue)
{
var str = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(str))
{
return defaultValue;
}
switch (str.ToLowerInvariant())
{
case "true":
case "1":
case "yes":
return true;
case "false":
case "0":
case "no":
return false;
default:
return defaultValue;
}
}
/// <summary>
/// Send the telemetry.
/// </summary>
private static void SendTelemetry(string eventName, Dictionary<string, string> payload)
{
try
{
var enabled = !GetEnvironmentVariableAsBool(name: TelemetryOptoutEnvVar, defaultValue: false);
if (!enabled)
{
return;
}
if (_telemetryClient == null)
{
_telemetryClient = new TelemetryClient();
}
_telemetryClient.TrackEvent(eventName, payload, null);
}
catch (Exception)
{
; // Do nothing, telemetry can't be sent
}
}
/// <summary>
/// Create the startup payload and send it up.
/// </summary>
internal static void SendPSCoreStartupTelemetry()
{
var properties = new Dictionary<string, string>();
properties.Add("GitCommitID", PSVersionInfo.GitCommitId);
properties.Add("OSDescription", RuntimeInformation.OSDescription);
SendTelemetry("ConsoleHostStartup", properties);
}
}
}

View file

@ -140,6 +140,21 @@ namespace System.Management.Automation
}
}
/// <summary>
/// Gets the location for the various caches.
/// </summary>
internal static string CacheDirectory
{
get
{
#if UNIX
return Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE);
#else
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell";
#endif
}
}
#if !UNIX
private static bool? _isNanoServer = null;
private static bool? _isIoT = null;

View file

@ -13,6 +13,8 @@
<ItemGroup>
<!-- the following package(s) are from https://github.com/JamesNK/Newtonsoft.Json -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<!-- the Application Insights package -->
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.10.0" />
<!-- the following package(s) are from https://github.com/dotnet/corefx -->
<PackageReference Include="Microsoft.Win32.Registry.AccessControl" Version="4.6.0-preview8.19405.3" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.6.0-preview8.19405.3" />

View file

@ -515,9 +515,12 @@ namespace System.Management.Automation
// This is the start of the real implementation of autocomplete/intellisense/tab completion
private static CommandCompletion CompleteInputImpl(Ast ast, Token[] tokens, IScriptPosition positionOfCursor, Hashtable options)
{
#if LEGACYTELEMETRY
// We could start collecting telemetry at a later date.
// We will leave the #if to remind us that we did this once.
var sw = new Stopwatch();
sw.Start();
#endif
using (var powershell = PowerShell.Create(RunspaceMode.CurrentRunspace))
{
var context = LocalPipeline.GetExecutionContextFromTLS();
@ -590,8 +593,10 @@ namespace System.Management.Automation
}
var completionResults = results ?? EmptyCompletionResult;
sw.Stop();
#if LEGACYTELEMETRY
// no telemetry here. We don't capture tab completion performance.
sw.Stop();
TelemetryAPI.ReportTabCompletionTelemetry(sw.ElapsedMilliseconds, completionResults.Count,
completionResults.Count > 0 ? completionResults[0].ResultType : CompletionResultType.Text);
#endif

View file

@ -9,6 +9,7 @@ using System.Management.Automation.Configuration;
using System.Management.Automation.Internal;
using System.Management.Automation.Tracing;
using System.Runtime.CompilerServices;
using Microsoft.PowerShell.Telemetry;
namespace System.Management.Automation
{
@ -150,6 +151,7 @@ namespace System.Management.Automation
if (IsModuleFeatureName(name))
{
list.Add(name);
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ExperimentalModuleFeatureActivation, name);
}
else if (IsEngineFeatureName(name))
{
@ -157,6 +159,7 @@ namespace System.Management.Automation
{
feature.Enabled = true;
list.Add(name);
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ExperimentalEngineFeatureActivation, name);
}
else
{

View file

@ -369,8 +369,9 @@ namespace Microsoft.PowerShell.Commands
/// </summary>
protected override void BeginProcessing()
{
#if LEGACYTELEMETRY
_timer.Start();
#endif
base.BeginProcessing();
if (ShowCommandInfo.IsPresent && Syntax.IsPresent)
@ -552,9 +553,11 @@ namespace Microsoft.PowerShell.Commands
}
}
#if LEGACYTELEMETRY
_timer.Stop();
#if LEGACYTELEMETRY
// No telemetry here - capturing the name of a command which we are not familiar with
// may be confidential customer information
// We want telementry on commands people look for but don't exist - this should give us an idea
// what sort of commands people expect but either don't exist, or maybe should be installed by default.
// The StartsWith is to avoid logging telemetry when suggestion mode checks the
@ -1443,8 +1446,9 @@ namespace Microsoft.PowerShell.Commands
private Collection<WildcardPattern> _nounPatterns;
private Collection<WildcardPattern> _modulePatterns;
#if LEGACYTELEMETRY
private Stopwatch _timer = new Stopwatch();
#endif
#endregion
#region ShowCommandInfo support

View file

@ -10,6 +10,7 @@ using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Language;
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Security;
@ -17,13 +18,13 @@ using System.Threading;
using Microsoft.Management.Infrastructure;
using Microsoft.PowerShell.Cmdletization;
using Microsoft.PowerShell.Telemetry;
using Dbg = System.Management.Automation.Diagnostics;
using System.Management.Automation.Language;
using Parser = System.Management.Automation.Language.Parser;
using ScriptBlock = System.Management.Automation.ScriptBlock;
using Token = System.Management.Automation.Language.Token;
#if LEGACYTELEMETRY
using Microsoft.PowerShell.Telemetry.Internal;
#endif
@ -825,6 +826,12 @@ namespace Microsoft.PowerShell.Commands
}
}
// Send telemetry on the imported modules
foreach (PSModuleInfo moduleInfo in remotelyImportedModules)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, moduleInfo.Name);
}
return remotelyImportedModules;
}
@ -1251,6 +1258,7 @@ namespace Microsoft.PowerShell.Commands
foreach (RemoteDiscoveryHelper.CimModule remoteCimModule in remotePsCimModules)
{
ImportModule_RemotelyViaCimModuleData(importModuleOptions, remoteCimModule, cimSession);
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, remoteCimModule.ModuleName);
}
}
@ -1743,6 +1751,7 @@ namespace Microsoft.PowerShell.Commands
// of doing Get-Module -list
foreach (PSModuleInfo module in ModuleInfo)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, module.Name);
RemoteDiscoveryHelper.DispatchModuleInfoProcessing(
module,
localAction: delegate ()
@ -1772,6 +1781,7 @@ namespace Microsoft.PowerShell.Commands
{
foreach (Assembly suppliedAssembly in Assembly)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name);
ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly);
}
}
@ -1785,6 +1795,8 @@ namespace Microsoft.PowerShell.Commands
{
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
// Telemetry here - report module load
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name);
#if LEGACYTELEMETRY
TelemetryAPI.ReportModuleLoad(foundModule);
#endif
@ -1809,6 +1821,7 @@ namespace Microsoft.PowerShell.Commands
BaseGuid = modulespec.Guid;
PSModuleInfo foundModule = ImportModule_LocallyViaName(importModuleOptions, modulespec.Name);
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name);
if (foundModule != null)
{
SetModuleBaseForEngineModules(foundModule.Name, this.Context);
@ -1818,6 +1831,10 @@ namespace Microsoft.PowerShell.Commands
else if (this.ParameterSetName.Equals(ParameterSet_FQName_ViaPsrpSession, StringComparison.OrdinalIgnoreCase))
{
ImportModule_RemotelyViaPsrpSession(importModuleOptions, null, FullyQualifiedName, this.PSSession);
foreach (ModuleSpecification modulespec in FullyQualifiedName)
{
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name);
}
}
else
{

View file

@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -16,6 +17,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Management.Infrastructure;
using Microsoft.PowerShell.Telemetry;
using Dbg = System.Management.Automation.Diagnostics;
@ -638,6 +640,7 @@ namespace System.Management.Automation
Streams = new PSDataStreams(this);
_endInvokeMethod = EndInvoke;
_endStopMethod = EndStop;
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.PowerShellCreate, "create");
}
/// <summary>

View file

@ -7,6 +7,7 @@ using System.Management.Automation.Runspaces;
using System.Management.Automation.Tracing;
using System.Reflection;
using System.Runtime.ExceptionServices;
using Microsoft.PowerShell.Telemetry;
using Dbg = System.Management.Automation.Diagnostics;
@ -1025,6 +1026,10 @@ namespace System.Management.Automation.Internal
CommandState.Started,
commandProcessor.Command.MyInvocation);
// Telemetry here
// the type of command should be sent along
// commandProcessor.CommandInfo.CommandType
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ApplicationType, commandProcessor.Command.CommandInfo.CommandType.ToString());
#if LEGACYTELEMETRY
Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.TraceExecutedCommand(commandProcessor.Command.CommandInfo, commandProcessor.Command.CommandOrigin);
#endif

View file

@ -10,6 +10,7 @@ using System.Management.Automation.Remoting;
using System.Management.Automation.Remoting.Client;
using System.Management.Automation.Tracing;
using System.Threading;
using Microsoft.PowerShell.Telemetry;
using Dbg = System.Management.Automation.Diagnostics;
#if LEGACYTELEMETRY
@ -854,6 +855,8 @@ namespace System.Management.Automation.Runspaces.Internal
PSEtwLog.LogOperationalVerbose(PSEventId.RunspacePoolOpen, PSOpcode.Open,
PSTask.CreateRunspace, PSKeyword.UseAlwaysOperational);
// Telemetry here - remote session
ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.RemoteSessionOpen, isAsync.ToString());
#if LEGACYTELEMETRY
TelemetryAPI.ReportRemoteSessionCreated(_connectionInfo);
#endif

View file

@ -0,0 +1,397 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
namespace Microsoft.PowerShell.Telemetry
{
/// <summary>
/// The category of telemetry.
/// </summary>
internal enum TelemetryType
{
/// <summary>
/// Telemetry of the application type (cmdlet, script, etc).
/// </summary>
ApplicationType,
/// <summary>
/// Send telemetry when we load a module, only module names in the s_knownModules list
/// will be reported, otherwise it will be "anonymous".
/// </summary>
ModuleLoad,
/// <summary>
/// Send telemetry for experimental module feature activation.
/// All experimental engine features will be have telemetry.
/// </summary>
ExperimentalEngineFeatureActivation,
/// <summary>
/// Send telemetry for experimental module feature activation.
/// Experimental module features will send telemetry based on the module it is in.
/// If we send telemetry for the module, we will also do so for any experimental feature
/// in that module.
/// </summary>
ExperimentalModuleFeatureActivation,
/// <summary>
/// Send telemetry for each PowerShell.Create API.
/// </summary>
PowerShellCreate,
/// <summary>
/// Remote session creation.
/// </summary>
RemoteSessionOpen,
}
/// <summary>
/// Send up telemetry for startup.
/// </summary>
public static class ApplicationInsightsTelemetry
{
// If this env var is true, yes, or 1, telemetry will NOT be sent.
private const string _telemetryOptoutEnvVar = "POWERSHELL_TELEMETRY_OPTOUT";
// PSCoreInsight2 telemetry key
// private const string _psCoreTelemetryKey = "ee4b2115-d347-47b0-adb6-b19c2c763808"; // Production
private const string _psCoreTelemetryKey = "d26a5ef4-d608-452c-a6b8-a4a55935f70d"; // V7 Preview 3
// Use "anonymous" as the string to return when you can't report a name
private const string _anonymous = "anonymous";
// the telemetry failure string
private const string _telemetryFailure = "TELEMETRY_FAILURE";
// Telemetry client to be reused when we start sending more telemetry
private static TelemetryClient s_telemetryClient { get; set; }
// the unique identifier for the user, when we start we
private static string s_uniqueUserIdentifier { get; set; }
// the session identifier
private static string s_sessionId { get; set; }
/// Use a hashset for quick lookups.
/// We send telemetry only a known set of modules.
/// If it's not in the list (initialized in the static constructor), then we report anonymous.
private static HashSet<string> s_knownModules;
/// <summary>Gets a value indicating whether telemetry can be sent.</summary>
public static bool CanSendTelemetry { get; private set; }
/// <summary>
/// Initializes static members of the <see cref="ApplicationInsightsTelemetry"/> class.
/// Static constructor determines whether telemetry is to be sent, and then
/// sets the telemetry key and set the telemetry delivery mode.
/// Creates the session ID and initializes the HashSet of known module names.
/// Gets or constructs the unique identifier.
/// </summary>
static ApplicationInsightsTelemetry()
{
// If we can't send telemetry, there's no reason to do any of this
CanSendTelemetry = !GetEnvironmentVariableAsBool(name: _telemetryOptoutEnvVar, defaultValue: false);
if (CanSendTelemetry)
{
s_telemetryClient = new TelemetryClient();
TelemetryConfiguration.Active.InstrumentationKey = _psCoreTelemetryKey;
// Set this to true to reduce latency during development
TelemetryConfiguration.Active.TelemetryChannel.DeveloperMode = false;
s_sessionId = Guid.NewGuid().ToString();
// use a hashset when looking for module names, it should be quicker than a string comparison
s_knownModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
"Microsoft.PowerShell.Archive",
"Microsoft.PowerShell.Host",
"Microsoft.PowerShell.Management",
"Microsoft.PowerShell.Security",
"Microsoft.PowerShell.Utility",
"PackageManagement",
"Pester",
"PowerShellGet",
"PSDesiredStateConfiguration",
"PSReadLine",
"ThreadJob",
};
s_uniqueUserIdentifier = GetUniqueIdentifier().ToString();
}
}
/// <summary>
/// Determine whether the environment variable is set and how.
/// </summary>
/// <param name="name">The name of the environment variable.</param>
/// <param name="defaultValue">If the environment variable is not set, use this as the default value.</param>
/// <returns>A boolean representing the value of the environment variable.</returns>
private static bool GetEnvironmentVariableAsBool(string name, bool defaultValue)
{
var str = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(str))
{
return defaultValue;
}
switch (str.ToLowerInvariant())
{
case "true":
case "1":
case "yes":
return true;
case "false":
case "0":
case "no":
return false;
default:
return defaultValue;
}
}
/// <summary>
/// Send telemetry as a metric.
/// </summary>
/// <param name="metricId">The type of telemetry that we'll be sending.</param>
/// <param name="data">The specific details about the telemetry.</param>
internal static void SendTelemetryMetric(TelemetryType metricId, string data)
{
if (!CanSendTelemetry)
{
return;
}
string metricName = metricId.ToString();
try
{
switch (metricId)
{
case TelemetryType.ApplicationType:
case TelemetryType.PowerShellCreate:
case TelemetryType.RemoteSessionOpen:
case TelemetryType.ExperimentalEngineFeatureActivation:
s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, data);
break;
case TelemetryType.ExperimentalModuleFeatureActivation:
string experimentalFeatureName = GetExperimentalFeatureName(data);
s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, experimentalFeatureName);
break;
case TelemetryType.ModuleLoad:
string moduleName = GetModuleName(data); // This will return anonymous if the modulename is not on the report list
s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, moduleName);
break;
}
}
catch
{
// do nothing, telemetry can't be sent
// don't send the panic telemetry as if we have failed above, it will likely fail here.
}
}
// Get the experimental feature name. If we can report it, we'll return the name of the feature, otherwise, we'll return "anonymous"
private static string GetExperimentalFeatureName(string featureNameToValidate)
{
// An experimental feature in a module is guaranteed to start with the module name
// we can strip out the text past the last '.' as the text before that will be the ModuleName
int lastDotIndex = featureNameToValidate.LastIndexOf('.');
string moduleName = featureNameToValidate.Substring(0, lastDotIndex);
if (s_knownModules.Contains(moduleName))
{
return featureNameToValidate;
}
return _anonymous;
}
// Get the module name. If we can report it, we'll return the name, otherwise, we'll return "anonymous"
private static string GetModuleName(string moduleNameToValidate)
{
if (s_knownModules.Contains(moduleNameToValidate))
{
return moduleNameToValidate;
}
return _anonymous;
}
/// <summary>
/// Create the startup payload and send it up.
/// This is done only once during for the console host.
/// </summary>
/// <param name="mode">The "mode" of the startup.</param>
internal static void SendPSCoreStartupTelemetry(string mode)
{
if (!CanSendTelemetry)
{
return;
}
var properties = new Dictionary<string, string>();
// The variable POWERSHELL_DISTRIBUTION_CHANNEL is set in our docker images.
// This allows us to track the actual docker OS as OSDescription provides only "linuxkit"
// which has limited usefulness
var channel = Environment.GetEnvironmentVariable("POWERSHELL_DISTRIBUTION_CHANNEL");
properties.Add("SessionId", s_sessionId);
properties.Add("UUID", s_uniqueUserIdentifier);
properties.Add("GitCommitID", PSVersionInfo.GitCommitId);
properties.Add("OSDescription", RuntimeInformation.OSDescription);
properties.Add("OSChannel", string.IsNullOrEmpty(channel) ? "unknown" : channel);
properties.Add("StartMode", string.IsNullOrEmpty(mode) ? "unknown" : mode);
try
{
s_telemetryClient.TrackEvent("ConsoleHostStartup", properties, null);
}
catch
{
// do nothing, telemetry cannot be sent
}
}
/// <summary>
/// Try to read the file and collect the guid.
/// </summary>
/// <param name="telemetryFilePath">The path to the telemetry file.</param>
/// <param name="id">The newly created id.</param>
/// <returns>
/// The method returns a bool indicating success or failure of creating the id.
/// </returns>
private static bool TryGetIdentifier(string telemetryFilePath, out Guid id)
{
if (File.Exists(telemetryFilePath))
{
// attempt to read the persisted identifier
const int GuidSize = 16;
byte[] buffer = new byte[GuidSize];
try
{
using (FileStream fs = new FileStream(telemetryFilePath, FileMode.Open, FileAccess.Read))
{
// if the read is invalid, or wrong size, we return it
int n = fs.Read(buffer, 0, GuidSize);
if (n == GuidSize)
{
// it's possible this could through
id = new Guid(buffer);
if (id != Guid.Empty)
{
return true;
}
}
}
}
catch
{
// something went wrong, the file may not exist or not have enough bytes, so return false
}
}
id = Guid.Empty;
return false;
}
/// <summary>
/// Try to create a unique identifier and persist it to the telemetry.uuid file.
/// </summary>
/// <param name="telemetryFilePath">The path to the persisted telemetry.uuid file.</param>
/// <param name="id">The created identifier.</param>
/// <returns>
/// The method returns a bool indicating success or failure of creating the id.
/// </returns>
private static bool TryCreateUniqueIdentifierAndFile(string telemetryFilePath, out Guid id)
{
// one last attempt to retrieve before creating incase we have a lot of simultaneous entry into the mutex.
id = Guid.Empty;
if (TryGetIdentifier(telemetryFilePath, out id))
{
return true;
}
// The directory may not exist, so attempt to create it
// CreateDirectory will simply return the directory if exists
try
{
Directory.CreateDirectory(Path.GetDirectoryName(telemetryFilePath));
}
catch
{
// send a telemetry indicating a problem with the cache dir
// it's likely something is seriously wrong so we should at least report it.
// We don't want to provide reasons here, that's not the point, but we
// would like to know if we're having a generalized problem which we can trace statistically
CanSendTelemetry = false;
s_telemetryClient.GetMetric(_telemetryFailure, "Detail").TrackValue(1, "cachedir");
return false;
}
// Create and save the new identifier, and if there's a problem, disable telemetry
try
{
id = Guid.NewGuid();
File.WriteAllBytes(telemetryFilePath, id.ToByteArray());
return true;
}
catch
{
// another bit of telemetry to notify us about a problem with saving the unique id.
s_telemetryClient.GetMetric(_telemetryFailure, "Detail").TrackValue(1, "saveuuid");
}
return false;
}
/// <summary>
/// Retrieve the unique identifier from the persisted file, if it doesn't exist create it.
/// Generate a guid which will be used as the UUID.
/// </summary>
/// <returns>A guid which represents the unique identifier.</returns>
private static Guid GetUniqueIdentifier()
{
// Try to get the unique id. If this returns false, we'll
// create/recreate the telemetry.uuid file to persist for next startup.
Guid id = Guid.Empty;
string uuidPath = Path.Join(Platform.CacheDirectory, "telemetry.uuid");
if (TryGetIdentifier(uuidPath, out id))
{
return id;
}
// Multiple processes may start simultaneously so we need a system wide
// way to control access to the file in the case (although remote) when we have
// simulataneous shell starts without the persisted file which attempt to create the file.
using (var m = new Mutex(true, "CreateUniqueUserId"))
{
// TryCreateUniqueIdentifierAndFile shouldn't throw, but the mutex might
try
{
m.WaitOne();
if (TryCreateUniqueIdentifierAndFile(uuidPath, out id))
{
return id;
}
}
catch (Exception)
{
// Any problem in generating a uuid will result in no telemetry being sent.
// Try to send the failure in telemetry, but it will have no unique id.
s_telemetryClient.GetMetric(_telemetryFailure, "Detail").TrackValue(1, "mutex");
}
finally
{
m.ReleaseMutex();
}
}
// something bad happened, turn off telemetry since the unique id wasn't set.
CanSendTelemetry = false;
return id;
}
}
}

View file

@ -95,6 +95,11 @@ Describe "Verify Markdown Links" {
it "<url> should work" -TestCases $trueFailures {
param($url)
# there could be multiple reasons why a failure is ok
# check against the allowed failures
# 503 = service temporarily unavailable
$allowedFailures = @( 503 )
$prefix = $url.Substring(0,7)
# Logging for diagnosability. Azure DevOps sometimes redacts the full url.
@ -108,7 +113,9 @@ Describe "Verify Markdown Links" {
}
catch
{
throw "retry of URL failed with error: $($_.Message)"
if ( $allowedFailures -notcontains $_.Exception.Response.StatusCode ) {
throw "retry of URL failed with error: $($_.Exception.Message)"
}
}
}
else {

View file

@ -43,7 +43,6 @@ Describe "Validate start of console host" -Tag CI {
'System.Private.CoreLib.dll'
'System.Private.Uri.dll'
'System.Private.Xml.dll'
'System.Private.Xml.Linq.dll'
'System.Reflection.Emit.ILGeneration.dll'
'System.Reflection.Emit.Lightweight.dll'
'System.Reflection.Primitives.dll'
@ -67,9 +66,7 @@ Describe "Validate start of console host" -Tag CI {
'System.Threading.Tasks.Parallel.dll'
'System.Threading.Thread.dll'
'System.Threading.ThreadPool.dll'
'System.Threading.Timer.dll'
'System.Xml.ReaderWriter.dll'
'System.Xml.XDocument.dll'
)
if ($IsWindows) {

View file

@ -0,0 +1,131 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
# unit tests for telemetry
# these tests aren't going to check that telemetry is being sent
# only that we're not treating the telemetry.uuid file correctly
Describe "Telemetry for shell startup" -Tag CI {
BeforeAll {
# 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].GetMember("CacheDirectory","NonPublic,Static").GetMethod.Invoke($null, $null)
$uuidPath = Join-Path -Path $cacheDir -ChildPath telemetry.uuid
$uuidFileExists = Test-Path -Path $uuidPath
if ( $uuidFileExists ) {
$originalBytes = Get-Content -AsByteStream -Path $uuidPath
Rename-Item -Path $uuidPath -NewName "${uuidPath}.original"
}
$PWSH = (Get-Process -Id $PID).MainModule.FileName
$telemetrySet = Test-Path -Path env:POWERSHELL_TELEMETRY_OPTOUT
$SendingTelemetry = $env:POWERSHELL_TELEMETRY_OPTOUT
}
AfterAll {
# check and reset the telemetry.uuid file
if ( $uuidFileExists ) {
if ( Test-Path -Path "${uuidPath}.original" ) {
Rename-Item -NewName $uuidPath -Path "${uuidPath}.original" -Force
}
else {
[System.IO.File]::WriteAllBytes($uuidPath, $originalBytes)
}
}
if ( $telemetrySet ) {
$env:POWERSHELL_TELEMETRY_OPTOUT = $SendingTelemetry
}
}
AfterEach {
if ( Test-Path -Path $uuidPath ) {
Remove-Item -Path $uuidPath
}
if ( Test-Path -Path env:POWERSHELL_TELEMETRY_OPTOUT ) {
Remove-Item env:POWERSHELL_TELEMETRY_OPTOUT
}
}
It "Should not create a uuid file if telemetry is opted out" {
$env:POWERSHELL_TELEMETRY_OPTOUT = 1
& $PWSH -NoProfile -Command "exit"
$uuidPath | Should -Not -Exist
}
It "Should create a uuid file if telemetry is opted in" {
$env:POWERSHELL_TELEMETRY_OPTOUT = "no"
& $PWSH -NoProfile -Command "exit"
$uuidPath | Should -Exist
}
It "Should create a uuid file by default" {
if ( Test-Path env:POWERSHELL_TELEMETRY_OPTOUT ) { Remove-Item -Path env:POWERSHELL_TELEMETRY_OPTOUT }
& $PWSH -NoProfile -Command "exit"
$uuidPath | Should -Exist
}
It "Should create a property uuid file when telemetry is sent" {
$env:POWERSHELL_TELEMETRY_OPTOUT = "no"
& $PWSH -NoProfile -Command "exit"
$uuidPath | Should -Exist
(Get-ChildItem -Path $uuidPath).Length | Should -Be 16
[byte[]]$newBytes = Get-Content -AsByteStream -Path $uuidPath
[System.Guid]::New($newBytes) | Should -BeOfType [System.Guid]
}
It "Should not create a telemetry file if one already exists and telemetry is opted in" {
[byte[]]$bytes = [System.Guid]::NewGuid().ToByteArray()
[System.IO.File]::WriteAllBytes($uuidPath, $bytes)
& $PWSH -NoProfile -Command "exit"
[byte[]]$newBytes = Get-Content -AsByteStream -Path $uuidPath
Compare-Object -ReferenceObject $bytes -DifferenceObject $newBytes | Should -BeNullOrEmpty
}
It "Should create a new telemetry file if the current one is 00000000-0000-0000-0000-000000000000" {
[byte[]]$zeroGuid = [System.Guid]::Empty.ToByteArray()
[System.IO.File]::WriteAllBytes($uuidPath, $zeroGuid)
& $PWSH -NoProfile -Command "exit"
[byte[]]$newBytes = Get-Content -AsByteStream -Path $uuidPath
# we could legitimately have zeros in the new guid, so we can't check for that
# we're just making sure that there *is* a difference
Compare-Object -ReferenceObject $zeroGuid -DifferenceObject $newBytes | Should -Not -BeNullOrEmpty
}
It "Should create a new telemetry file if the current one is smaller than 16 bytes" {
$badBytes = [byte[]]::new(8);
[System.IO.File]::WriteAllBytes($uuidPath, $badBytes)
& $PWSH -NoProfile -Command "exit"
[byte[]]$nb = Get-Content -AsByteStream -Path $uuidPath
[System.Guid]::New($nb) | Should -BeOfType [System.Guid]
}
It "Should not create a new telemetry file if the current one has a valid guid and is larger than 16 bytes" {
$g = [Guid]::newGuid()
$tooManyBytes = $g.ToByteArray() * 2
[System.IO.File]::WriteAllBytes($uuidPath, $tooManyBytes)
[byte[]]$nb = Get-Content -Path $uuidPath -AsByteStream | Select-Object -First 16
$ng = [System.Guid]::new($nb)
$g | Should -Be $ng
}
It "Should properly set whether telemetry is sent based on when environment variable is not set" {
$result = & $PWSH -NoProfile -Command '[Microsoft.PowerShell.Telemetry.ApplicationInsightsTelemetry]::CanSendTelemetry'
$result | Should -Be "True"
}
$telemetryIsSetData = @(
@{ name = "set to no"; Value = "no" ; expectedValue = "True" }
@{ name = "set to 0"; Value = "0"; expectedValue = "True" }
@{ name = "set to false"; Value = "false"; expectedValue = "True" }
@{ name = "set to yes"; Value = "yes"; expectedValue = "False" }
@{ name = "set to 1"; Value = "1"; expectedValue = "False" }
@{ name = "set to true"; Value = "true"; expectedValue = "False" }
)
It "Should properly set whether telemetry is sent based on environment variable when <name>" -TestCases $telemetryIsSetData {
param ( [string]$name, [string]$value, [string]$expectedValue )
$env:POWERSHELL_TELEMETRY_OPTOUT = $value
$result = & $PWSH -NoProfile -Command '[Microsoft.PowerShell.Telemetry.ApplicationInsightsTelemetry]::CanSendTelemetry'
$result | Should -Be $expectedValue
}
}