diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 795921a0c5..aefcb58cfd 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #nullable enable using System; @@ -195,6 +194,57 @@ namespace Microsoft.PowerShell "workingdirectory" }; + /// + /// These represent the parameters that are used when starting pwsh. + /// We can query in our telemetry to determine how pwsh was invoked. + /// + [Flags] + internal enum ParameterBitmap : long + { + Command = 0x00000001, // -Command | -c + ConfigurationName = 0x00000002, // -ConfigurationName | -config + CustomPipeName = 0x00000004, // -CustomPipeName + EncodedCommand = 0x00000008, // -EncodedCommand | -e | -ec + EncodedArgument = 0x00000010, // -EncodedArgument + ExecutionPolicy = 0x00000020, // -ExecutionPolicy | -ex | -ep + File = 0x00000040, // -File | -f + Help = 0x00000080, // -Help, -?, /? + InputFormat = 0x00000100, // -InputFormat | -inp | -if + Interactive = 0x00000200, // -Interactive | -i + Login = 0x00000400, // -Login | -l + MTA = 0x00000800, // -MTA + NoExit = 0x00001000, // -NoExit | -noe + NoLogo = 0x00002000, // -NoLogo | -nol + NonInteractive = 0x00004000, // -NonInteractive | -noni + NoProfile = 0x00008000, // -NoProfile | -nop + OutputFormat = 0x00010000, // -OutputFormat | -o | -of + SettingsFile = 0x00020000, // -SettingsFile | -settings + SSHServerMode = 0x00040000, // -SSHServerMode | -sshs + SocketServerMode = 0x00080000, // -SocketServerMode | -sockets + ServerMode = 0x00100000, // -ServerMode | -server + NamedPipeServerMode = 0x00200000, // -NamedPipeServerMode | -namedpipes + STA = 0x00400000, // -STA + Version = 0x00800000, // -Version | -v + WindowStyle = 0x01000000, // -WindowStyle | -w + WorkingDirectory = 0x02000000, // -WorkingDirectory | -wd + // Enum values for specified ExecutionPolicy + EPUnrestricted = 0x0000000100000000, // ExecutionPolicy unrestricted + EPRemoteSigned = 0x0000000200000000, // ExecutionPolicy remote signed + EPAllSigned = 0x0000000400000000, // ExecutionPolicy all signed + EPRestricted = 0x0000000800000000, // ExecutionPolicy restricted + EPDefault = 0x0000001000000000, // ExecutionPolicy default + EPBypass = 0x0000002000000000, // ExecutionPolicy bypass + EPUndefined = 0x0000004000000000, // ExecutionPolicy undefined + EPIncorrect = 0x0000008000000000, // ExecutionPolicy incorrect + } + + internal ParameterBitmap ParametersUsed = 0; + + internal double ParametersUsedAsDouble + { + get { return (double)ParametersUsed; } + } + [Conditional("DEBUG")] private void AssertArgumentsParsed() { @@ -641,6 +691,53 @@ namespace Microsoft.PowerShell return Path.GetFullPath(path); } + /// + /// Determine the execution policy based on the supplied string. + /// If the string doesn't match to any known execution policy, set it to incorrect. + /// + /// The value provided on the command line. + /// The execution policy. + private static ParameterBitmap GetExecutionPolicy(string? _executionPolicy) + { + if (_executionPolicy is null) + { + return ParameterBitmap.EPUndefined; + } + + ParameterBitmap executionPolicySetting = ParameterBitmap.EPIncorrect; + + if (string.Equals(_executionPolicy, "default", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPDefault; + } + else if (string.Equals(_executionPolicy, "remotesigned", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPRemoteSigned; + } + else if (string.Equals(_executionPolicy, "bypass", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPBypass; + } + else if (string.Equals(_executionPolicy, "allsigned", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPAllSigned; + } + else if (string.Equals(_executionPolicy, "restricted", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPRestricted; + } + else if (string.Equals(_executionPolicy, "unrestricted", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPUnrestricted; + } + else if (string.Equals(_executionPolicy, "undefined", StringComparison.OrdinalIgnoreCase)) + { + executionPolicySetting = ParameterBitmap.EPUndefined; + } + + return executionPolicySetting; + } + private static bool MatchSwitch(string switchKey, string match, string smallestUnambiguousMatch) { Dbg.Assert(!string.IsNullOrEmpty(match), "need a value"); @@ -755,6 +852,7 @@ namespace Microsoft.PowerShell _noInteractive = true; _skipUserInit = true; _noExit = false; + ParametersUsed |= ParameterBitmap.Version; break; } @@ -763,48 +861,59 @@ namespace Microsoft.PowerShell _showHelp = true; _showExtendedHelp = true; _abortStartup = true; + ParametersUsed |= ParameterBitmap.Help; } else if (MatchSwitch(switchKey, "login", "l")) { // On Windows, '-Login' does nothing. // On *nix, '-Login' is already handled much earlier to improve startup performance, so we do nothing here. + ParametersUsed |= ParameterBitmap.Login; } else if (MatchSwitch(switchKey, "noexit", "noe")) { _noExit = true; noexitSeen = true; + ParametersUsed |= ParameterBitmap.NoExit; } else if (MatchSwitch(switchKey, "noprofile", "nop")) { _skipUserInit = true; + ParametersUsed |= ParameterBitmap.NoProfile; } else if (MatchSwitch(switchKey, "nologo", "nol")) { _showBanner = false; + ParametersUsed |= ParameterBitmap.NoLogo; } else if (MatchSwitch(switchKey, "noninteractive", "noni")) { _noInteractive = true; + ParametersUsed |= ParameterBitmap.NonInteractive; } else if (MatchSwitch(switchKey, "socketservermode", "so")) { _socketServerMode = true; + ParametersUsed |= ParameterBitmap.SocketServerMode; } else if (MatchSwitch(switchKey, "servermode", "s")) { _serverMode = true; + ParametersUsed |= ParameterBitmap.ServerMode; } else if (MatchSwitch(switchKey, "namedpipeservermode", "nam")) { _namedPipeServerMode = true; + ParametersUsed |= ParameterBitmap.NamedPipeServerMode; } else if (MatchSwitch(switchKey, "sshservermode", "sshs")) { _sshServerMode = true; + ParametersUsed |= ParameterBitmap.SSHServerMode; } else if (MatchSwitch(switchKey, "interactive", "i")) { _noInteractive = false; + ParametersUsed |= ParameterBitmap.Interactive; } else if (MatchSwitch(switchKey, "configurationname", "config")) { @@ -817,6 +926,7 @@ namespace Microsoft.PowerShell } _configurationName = args[i]; + ParametersUsed |= ParameterBitmap.ConfigurationName; } else if (MatchSwitch(switchKey, "custompipename", "cus")) { @@ -841,7 +951,9 @@ namespace Microsoft.PowerShell break; } #endif + _customPipeName = args[i]; + ParametersUsed |= ParameterBitmap.CustomPipeName; } else if (MatchSwitch(switchKey, "command", "c")) { @@ -849,6 +961,8 @@ namespace Microsoft.PowerShell { break; } + + ParametersUsed |= ParameterBitmap.Command; } else if (MatchSwitch(switchKey, "windowstyle", "w")) { @@ -875,6 +989,8 @@ namespace Microsoft.PowerShell string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.InvalidWindowStyleArgument, args[i], e.Message)); break; } + + ParametersUsed |= ParameterBitmap.WindowStyle; #endif } else if (MatchSwitch(switchKey, "file", "f")) @@ -883,6 +999,8 @@ namespace Microsoft.PowerShell { break; } + + ParametersUsed |= ParameterBitmap.File; } #if DEBUG else if (MatchSwitch(switchKey, "isswait", "isswait")) @@ -894,14 +1012,18 @@ namespace Microsoft.PowerShell { ParseFormat(args, ref i, ref _outFormat, CommandLineParameterParserStrings.MissingOutputFormatParameter); _outputFormatSpecified = true; + ParametersUsed |= ParameterBitmap.OutputFormat; } else if (MatchSwitch(switchKey, "inputformat", "inp") || MatchSwitch(switchKey, "if", "if")) { ParseFormat(args, ref i, ref _inFormat, CommandLineParameterParserStrings.MissingInputFormatParameter); + ParametersUsed |= ParameterBitmap.InputFormat; } else if (MatchSwitch(switchKey, "executionpolicy", "ex") || MatchSwitch(switchKey, "ep", "ep")) { ParseExecutionPolicy(args, ref i, ref _executionPolicy, CommandLineParameterParserStrings.MissingExecutionPolicyParameter); + ParametersUsed |= ParameterBitmap.ExecutionPolicy; + ParametersUsed |= GetExecutionPolicy(_executionPolicy); } else if (MatchSwitch(switchKey, "encodedcommand", "e") || MatchSwitch(switchKey, "ec", "e")) { @@ -910,6 +1032,8 @@ namespace Microsoft.PowerShell { break; } + + ParametersUsed |= ParameterBitmap.EncodedCommand; } else if (MatchSwitch(switchKey, "encodedarguments", "encodeda") || MatchSwitch(switchKey, "ea", "ea")) { @@ -917,6 +1041,8 @@ namespace Microsoft.PowerShell { break; } + + ParametersUsed |= ParameterBitmap.EncodedArgument; } else if (MatchSwitch(switchKey, "settingsfile", "settings")) { @@ -925,6 +1051,8 @@ namespace Microsoft.PowerShell { break; } + + ParametersUsed |= ParameterBitmap.SettingsFile; } else if (MatchSwitch(switchKey, "sta", "sta")) { @@ -944,6 +1072,7 @@ namespace Microsoft.PowerShell } _staMode = true; + ParametersUsed |= ParameterBitmap.STA; } else if (MatchSwitch(switchKey, "mta", "mta")) { @@ -963,6 +1092,7 @@ namespace Microsoft.PowerShell } _staMode = false; + ParametersUsed |= ParameterBitmap.MTA; } else if (MatchSwitch(switchKey, "workingdirectory", "wo") || MatchSwitch(switchKey, "wd", "wd")) { @@ -975,6 +1105,7 @@ namespace Microsoft.PowerShell } _workingDirectory = args[i]; + ParametersUsed |= ParameterBitmap.WorkingDirectory; } #if !UNIX else if (MatchSwitch(switchKey, "removeworkingdirectorytrailingcharacter", "removeworkingdirectorytrailingcharacter")) @@ -990,6 +1121,9 @@ namespace Microsoft.PowerShell { break; } + + // default to filename being the next argument. + ParametersUsed |= ParameterBitmap.File; } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 4703d54d88..6a78b4c864 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -192,7 +192,7 @@ namespace Microsoft.PowerShell // First check for and handle PowerShell running in a server mode. if (s_cpp.ServerMode) { - ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("ServerMode"); + ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("ServerMode", s_cpp.ParametersUsedAsDouble); ProfileOptimization.StartProfile("StartupProfileData-ServerMode"); StdIOProcessMediator.Run( initialCommand: s_cpp.InitialCommand, @@ -202,7 +202,7 @@ namespace Microsoft.PowerShell } else if (s_cpp.SSHServerMode) { - ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SSHServer"); + ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SSHServer", s_cpp.ParametersUsedAsDouble); ProfileOptimization.StartProfile("StartupProfileData-SSHServerMode"); StdIOProcessMediator.Run( initialCommand: s_cpp.InitialCommand, @@ -212,7 +212,7 @@ namespace Microsoft.PowerShell } else if (s_cpp.NamedPipeServerMode) { - ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("NamedPipe"); + ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("NamedPipe", s_cpp.ParametersUsedAsDouble); ProfileOptimization.StartProfile("StartupProfileData-NamedPipeServerMode"); RemoteSessionNamedPipeServer.RunServerMode( configurationName: s_cpp.ConfigurationName); @@ -220,7 +220,7 @@ namespace Microsoft.PowerShell } else if (s_cpp.SocketServerMode) { - ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SocketServerMode"); + ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("SocketServerMode", s_cpp.ParametersUsedAsDouble); ProfileOptimization.StartProfile("StartupProfileData-SocketServerMode"); HyperVSocketMediator.Run( initialCommand: s_cpp.InitialCommand, @@ -255,7 +255,7 @@ namespace Microsoft.PowerShell PSHost.IsStdOutputRedirected = Console.IsOutputRedirected; // Send startup telemetry for ConsoleHost startup - ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("Normal"); + ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry("Normal", s_cpp.ParametersUsedAsDouble); exitCode = s_theConsoleHost.Run(s_cpp, false); } diff --git a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs index 8f503cbe46..8e178c0cb1 100644 --- a/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs +++ b/src/System.Management.Automation/engine/Modules/ImportModuleCommand.cs @@ -619,7 +619,7 @@ namespace Microsoft.PowerShell.Commands // avoid double reporting for WinCompat modules that go through CommandDiscovery\AutoloadSpecifiedModule if (!foundModule.IsWindowsPowerShellCompatModule) { - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name); + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name, foundModule.Version?.ToString()); #if LEGACYTELEMETRY TelemetryAPI.ReportModuleLoad(foundModule); #endif @@ -896,7 +896,7 @@ namespace Microsoft.PowerShell.Commands if (foundModule != null) { - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name); + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, foundModule.Name, foundModule.Version?.ToString()); SetModuleBaseForEngineModules(foundModule.Name, this.Context); } @@ -938,7 +938,7 @@ namespace Microsoft.PowerShell.Commands // Send telemetry on the imported modules foreach (PSModuleInfo moduleInfo in remotelyImportedModules) { - ApplicationInsightsTelemetry.SendTelemetryMetric(usingWinCompat ? TelemetryType.WinCompatModuleLoad : TelemetryType.ModuleLoad, moduleInfo.Name); + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(usingWinCompat ? TelemetryType.WinCompatModuleLoad : TelemetryType.ModuleLoad, moduleInfo.Name, moduleInfo.Version?.ToString()); } return remotelyImportedModules; @@ -1369,7 +1369,8 @@ namespace Microsoft.PowerShell.Commands foreach (RemoteDiscoveryHelper.CimModule remoteCimModule in remotePsCimModules) { ImportModule_RemotelyViaCimModuleData(importModuleOptions, remoteCimModule, cimSession); - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, remoteCimModule.ModuleName); + // we don't know the version of the module + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, remoteCimModule.ModuleName); } } @@ -1874,7 +1875,7 @@ namespace Microsoft.PowerShell.Commands // of doing Get-Module -list foreach (PSModuleInfo module in ModuleInfo) { - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, module.Name); + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, module.Name, module.Version?.ToString()); RemoteDiscoveryHelper.DispatchModuleInfoProcessing( module, localAction: () => @@ -1902,7 +1903,8 @@ namespace Microsoft.PowerShell.Commands // Now load all of the supplied assemblies... foreach (Assembly suppliedAssembly in Assembly) { - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name); + // we don't know what the version of the module is. + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, suppliedAssembly.GetName().Name); ImportModule_ViaAssembly(importModuleOptions, suppliedAssembly); } } @@ -1933,7 +1935,7 @@ namespace Microsoft.PowerShell.Commands ImportModule_RemotelyViaPsrpSession(importModuleOptions, null, FullyQualifiedName, this.PSSession); foreach (ModuleSpecification modulespec in FullyQualifiedName) { - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name); + ApplicationInsightsTelemetry.SendModuleTelemetryMetric(TelemetryType.ModuleLoad, modulespec.Name, modulespec.Version?.ToString()); } } else if (this.ParameterSetName.Equals(ParameterSet_ViaWinCompat, StringComparison.OrdinalIgnoreCase) diff --git a/src/System.Management.Automation/engine/pipeline.cs b/src/System.Management.Automation/engine/pipeline.cs index 185e60e072..152b3e771e 100644 --- a/src/System.Management.Automation/engine/pipeline.cs +++ b/src/System.Management.Automation/engine/pipeline.cs @@ -1077,8 +1077,6 @@ namespace System.Management.Automation.Internal CommandState.Started, commandProcessor.Command.MyInvocation); - // Send telemetry that includes the type of command. - ApplicationInsightsTelemetry.SendTelemetryMetric(TelemetryType.ApplicationType, commandProcessor.Command.CommandInfo.CommandType.ToString()); #if LEGACYTELEMETRY Microsoft.PowerShell.Telemetry.Internal.TelemetryAPI.TraceExecutedCommand(commandProcessor.Command.CommandInfo, commandProcessor.Command.CommandOrigin); #endif diff --git a/src/System.Management.Automation/utils/Telemetry.cs b/src/System.Management.Automation/utils/Telemetry.cs index 1c1f1b8d2c..625f957165 100644 --- a/src/System.Management.Automation/utils/Telemetry.cs +++ b/src/System.Management.Automation/utils/Telemetry.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Management.Automation; using System.Runtime.InteropServices; @@ -94,8 +95,15 @@ namespace Microsoft.PowerShell.Telemetry // private const string _psCoreTelemetryKey = "ee4b2115-d347-47b0-adb6-b19c2c763808"; // Production private const string _psCoreTelemetryKey = "d26a5ef4-d608-452c-a6b8-a4a55935f70d"; // V7 Preview 3 + // In the event there is a problem in creating the node identifier file, use the default identifier. + // This can happen if we are running in a system which has a read-only filesystem. + private static readonly Guid _defaultNodeIdentifier = new Guid("2f998828-3f4a-4741-bf50-d11c6be42f50"); + // Use "anonymous" as the string to return when you can't report a name - private const string _anonymous = "anonymous"; + private const string Anonymous = "anonymous"; + + // Use '0.0' as the string for an anonymous module version + private const string AnonymousVersion = "0.0"; // the telemetry failure string private const string _telemetryFailure = "TELEMETRY_FAILURE"; @@ -240,6 +248,7 @@ namespace Microsoft.PowerShell.Telemetry "Az.StorageTable", "Az.StreamAnalytics", "Az.Subscription", + "Az.Tools.Predictor", "Az.TrafficManager", "Az.Websites", "Azs.Azurebridge.Admin", @@ -642,6 +651,28 @@ namespace Microsoft.PowerShell.Telemetry return defaultValue; } + /// + /// Send module load telemetry as a metric. + /// For modules we send the module name (if allowed), and the version. + /// Some modules (CIM) will continue use the string alternative method. + /// + /// The type of telemetry that we'll be sending. + /// The module name to report. If it is not allowed, then it is set to 'anonymous'. + /// The module version to report. The default value is the anonymous version '0.0.0.0'. + internal static void SendModuleTelemetryMetric(TelemetryType telemetryType, string moduleName, string moduleVersion = AnonymousVersion) + { + try + { + string allowedModuleName = GetModuleName(moduleName); + string allowedModuleVersion = allowedModuleName == Anonymous ? AnonymousVersion : moduleVersion; + s_telemetryClient.GetMetric(telemetryType.ToString(), "uuid", "SessionId", "ModuleName", "Version").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, allowedModuleName, allowedModuleVersion); + } + catch + { + // Ignore errors. + } + } + /// /// Send telemetry as a metric. /// @@ -654,7 +685,9 @@ namespace Microsoft.PowerShell.Telemetry return; } - SendPSCoreStartupTelemetry("hosted"); + // These should be handled by SendModuleTelemetryMetric. + Debug.Assert(metricId != TelemetryType.ModuleLoad, "ModuleLoad should be handled by SendModuleTelemetryMetric."); + Debug.Assert(metricId != TelemetryType.WinCompatModuleLoad, "WinCompatModuleLoad should be handled by SendModuleTelemetryMetric."); string metricName = metricId.ToString(); try @@ -671,11 +704,6 @@ namespace Microsoft.PowerShell.Telemetry string experimentalFeatureName = GetExperimentalFeatureName(data); s_telemetryClient.GetMetric(metricName, "uuid", "SessionId", "Detail").TrackValue(metricValue: 1.0, s_uniqueUserIdentifier, s_sessionId, experimentalFeatureName); break; - case TelemetryType.ModuleLoad: - case TelemetryType.WinCompatModuleLoad: - 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 @@ -697,7 +725,7 @@ namespace Microsoft.PowerShell.Telemetry return featureNameToValidate; } - return _anonymous; + return Anonymous; } // Get the module name. If we can report it, we'll return the name, otherwise, we'll return "anonymous" @@ -708,7 +736,7 @@ namespace Microsoft.PowerShell.Telemetry return moduleNameToValidate; } - return _anonymous; + return Anonymous; } /// @@ -716,7 +744,8 @@ namespace Microsoft.PowerShell.Telemetry /// This is done only once during for the console host. /// /// The "mode" of the startup. - internal static void SendPSCoreStartupTelemetry(string mode) + /// The parameter bitmap used when starting. + internal static void SendPSCoreStartupTelemetry(string mode, double parametersUsed) { // Check if we already sent startup telemetry if (Interlocked.CompareExchange(ref s_startupEventSent, 1, 0) == 1) @@ -729,22 +758,30 @@ namespace Microsoft.PowerShell.Telemetry return; } + // This is the payload which reports the startup information of OS and shell details. var properties = new Dictionary(); - // 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 + // This is the payload for the parameter data which is sent as a metric. + var parameters = new Dictionary(); + + // The variable POWERSHELL_DISTRIBUTION_CHANNEL is set in our docker images and + // by various other environments. 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"); + // Construct the payload for the OS and shell details. 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); + + // Construct the payload for the parameters used. + parameters.Add("Param", parametersUsed); try { - s_telemetryClient.TrackEvent("ConsoleHostStartup", properties, null); + s_telemetryClient.TrackEvent("ConsoleHostStartup", properties, parameters); } catch { @@ -798,50 +835,53 @@ namespace Microsoft.PowerShell.Telemetry /// Try to create a unique identifier and persist it to the telemetry.uuid file. /// /// The path to the persisted telemetry.uuid file. - /// The created identifier. /// - /// The method returns a bool indicating success or failure of creating the id. + /// The method node id. /// - private static bool TryCreateUniqueIdentifierAndFile(string telemetryFilePath, out Guid id) + private static Guid CreateUniqueIdentifierAndFile(string telemetryFilePath) { // one last attempt to retrieve before creating incase we have a lot of simultaneous entry into the mutex. - id = Guid.Empty; + Guid id = Guid.Empty; if (TryGetIdentifier(telemetryFilePath, out id)) { - return true; + return id; } // The directory may not exist, so attempt to create it // CreateDirectory will simply return the directory if exists + bool attemptFileCreation = true; 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; + // There was a problem in creating the directory for the file, do not attempt to create the file. + // We don't send telemetry here because there are valid reasons for the directory to not exist + // and not be able to be created. + attemptFileCreation = false; } - // Create and save the new identifier, and if there's a problem, disable telemetry - try + // If we were able to create the directory, try to create the file, + // if this fails we will send telemetry to indicate this and then use the default identifier. + if (attemptFileCreation) { - 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"); + try + { + id = Guid.NewGuid(); + File.WriteAllBytes(telemetryFilePath, id.ToByteArray()); + return id; + } + 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; + // all attempts to create an identifier have failed, so use the default node id. + id = _defaultNodeIdentifier; + return id; } /// @@ -865,15 +905,12 @@ namespace Microsoft.PowerShell.Telemetry // simultaneous shell starts without the persisted file which attempt to create the file. try { - // TryCreateUniqueIdentifierAndFile shouldn't throw, but the mutex might + // CreateUniqueIdentifierAndFile shouldn't throw, but the mutex might using var m = new Mutex(true, "CreateUniqueUserId"); m.WaitOne(); try { - if (TryCreateUniqueIdentifierAndFile(uuidPath, out id)) - { - return id; - } + return CreateUniqueIdentifierAndFile(uuidPath); } finally { diff --git a/test/powershell/engine/Basic/Telemetry.Tests.ps1 b/test/powershell/engine/Basic/Telemetry.Tests.ps1 index 0ea3594b86..2378b9e5a6 100644 --- a/test/powershell/engine/Basic/Telemetry.Tests.ps1 +++ b/test/powershell/engine/Basic/Telemetry.Tests.ps1 @@ -129,28 +129,14 @@ Describe "Telemetry for shell startup" -Tag CI { $result | Should -Be $expectedValue } - It "Should resend startup event if the semaphore says we haven't sent telemetry" { - + It "Should send startup event" { $resultJson = & $PWSH -NoProfile -c { + # this should ensure that the startup telemetry event is sent. + $null = Get-Date | Out-String $telemetryType = [Microsoft.PowerShell.Telemetry.ApplicationInsightsTelemetry] $bindingFlags = [System.Reflection.BindingFlags]"NonPublic,Static" - $initialValue = ${telemetryType}.GetMember("s_startupEventSent", $bindingFlags)[0].GetValue($null) - # force a resend of the startup telemetry - $null = ${telemetryType}.GetMember("s_startupEventSent", $bindingFlags)[0].SetValue($null,0) - $null = Get-Date | Out-String - # now check it again, it should be true now that something has executed - $finalValue = ${telemetryType}.GetMember("s_startupEventSent", $bindingFlags)[0].GetValue($null) - @{ - initialValue = $initialValue - finalValue = $finalValue - } | ConvertTo-Json -Compress + $observedValue = ${telemetryType}.GetMember("s_startupEventSent", $bindingFlags)[0].GetValue($null) + $observedValue | Should -Be 1 -Because "Should have sent telemetry on console startup" } - - $result = $resultJson | ConvertFrom-Json - - $result.InitialValue | Should -Be 1 -Because "Should have sent telemetry on console startup" - - $result.FinalValue | Should -Be 1 -Because "Should have resent telemetry" } - }