diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandContext.cs b/src/Microsoft.DotNet.Cli.Utils/CommandContext.cs index 28a7aaa3e6..8209dd2f74 100644 --- a/src/Microsoft.DotNet.Cli.Utils/CommandContext.cs +++ b/src/Microsoft.DotNet.Cli.Utils/CommandContext.cs @@ -14,8 +14,8 @@ public static class Variables public static readonly string AnsiPassThru = Prefix + "ANSI_PASS_THRU"; } - private static Lazy _verbose = new Lazy(() => GetBool(Variables.Verbose)); - private static Lazy _ansiPassThru = new Lazy(() => GetBool(Variables.AnsiPassThru)); + private static Lazy _verbose = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.Verbose)); + private static Lazy _ansiPassThru = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.AnsiPassThru)); public static bool IsVerbose() { @@ -25,29 +25,6 @@ public static bool IsVerbose() public static bool ShouldPassAnsiCodesThrough() { return _ansiPassThru.Value; - } - - private static bool GetBool(string name, bool defaultValue = false) - { - 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; - } - } + } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/Env.cs b/src/Microsoft.DotNet.Cli.Utils/Env.cs index 30a117bcce..5d0d1382ba 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Env.cs +++ b/src/Microsoft.DotNet.Cli.Utils/Env.cs @@ -33,5 +33,10 @@ public static string GetCommandPathFromRootPath(string rootPath, string commandN { return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions); } + + public static bool GetEnvironmentVariableAsBool(string name, bool defaultValue = false) + { + return _environment.GetEnvironmentVariableAsBool(name, defaultValue); + } } } diff --git a/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs b/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs index 1060443ff3..b44ffef717 100644 --- a/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs +++ b/src/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs @@ -93,5 +93,29 @@ public string GetCommandPathFromRootPath(string rootPath, string commandName, IE return GetCommandPathFromRootPath(rootPath, commandName, extensionsArr); } + + public 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; + } + } + } } diff --git a/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs b/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs index db04ebc0b6..754bf54a3e 100644 --- a/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs +++ b/src/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs @@ -16,5 +16,7 @@ public interface IEnvironmentProvider string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions); string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions); + + bool GetEnvironmentVariableAsBool(string name, bool defaultValue); } } diff --git a/src/Microsoft.DotNet.Cli.Utils/ITelemetry.cs b/src/Microsoft.DotNet.Cli.Utils/ITelemetry.cs new file mode 100644 index 0000000000..9cfba17fc9 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/ITelemetry.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface ITelemetry + { + void TrackEvent(string eventName, IDictionary properties, IDictionary measurements); + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/Product.cs b/src/Microsoft.DotNet.Cli.Utils/Product.cs new file mode 100644 index 0000000000..8d2a8f05bf --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/Product.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class Product + { + public static readonly string LongName = ".NET Command Line Tools"; + public static readonly string Version = GetProductVersion(); + + private static string GetProductVersion() + { + var attr = typeof(Product).GetTypeInfo().Assembly.GetCustomAttribute(); + return attr?.InformationalVersion; + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/Telemetry.cs b/src/Microsoft.DotNet.Cli.Utils/Telemetry.cs new file mode 100644 index 0000000000..fafd07bba7 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/Telemetry.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using Microsoft.ApplicationInsights; +using Microsoft.Extensions.PlatformAbstractions; +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class Telemetry : ITelemetry + { + private bool _isInitialized = false; + private TelemetryClient _client = null; + + private Dictionary _commonProperties = null; + private Dictionary _commonMeasurements = null; + + private const string InstrumentationKey = "74cc1c9e-3e6e-4d05-b3fc-dde9101d0254"; + private const string TelemetryOptout = "DOTNET_CLI_TELEMETRY_OPTOUT"; + private const string OSVersion = "OS Version"; + private const string OSPlatform = "OS Platform"; + private const string RuntimeId = "Runtime Id"; + private const string ProductVersion = "Product Version"; + + public Telemetry() + { + bool optout = Env.GetEnvironmentVariableAsBool(TelemetryOptout); + + if (optout) + { + return; + } + + try + { + _client = new TelemetryClient(); + _client.InstrumentationKey = InstrumentationKey; + _client.Context.Session.Id = Guid.NewGuid().ToString(); + + var runtimeEnvironment = PlatformServices.Default.Runtime; + _client.Context.Device.OperatingSystem = runtimeEnvironment.OperatingSystem; + + _commonProperties = new Dictionary(); + _commonProperties.Add(OSVersion, runtimeEnvironment.OperatingSystemVersion); + _commonProperties.Add(OSPlatform, runtimeEnvironment.OperatingSystemPlatform.ToString()); + _commonProperties.Add(RuntimeId, runtimeEnvironment.GetRuntimeIdentifier()); + _commonProperties.Add(ProductVersion, Product.Version); + _commonMeasurements = new Dictionary(); + + _isInitialized = true; + } + catch (Exception) + { + // we dont want to fail the tool if telemetry fais. We should be able to detect abnormalities from data + // at the server end + Debug.Fail("Exception during telemetry initialization"); + } + } + + public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements) + { + if (!_isInitialized) + { + return; + } + + Dictionary eventMeasurements = GetEventMeasures(measurements); + Dictionary eventProperties = GetEventProperties(properties); + + try + { + _client.TrackEvent(eventName, eventProperties, eventMeasurements); + _client.Flush(); + } + catch (Exception) + { + Debug.Fail("Exception during TrackEvent"); + } + } + + + private Dictionary GetEventMeasures(IDictionary measurements) + { + Dictionary eventMeasurements = new Dictionary(_commonMeasurements); + if (measurements != null) + { + foreach (var measurement in measurements) + { + if (eventMeasurements.ContainsKey(measurement.Key)) + { + eventMeasurements[measurement.Key] = measurement.Value; + } + else + { + eventMeasurements.Add(measurement.Key, measurement.Value); + } + } + } + return eventMeasurements; + } + + private Dictionary GetEventProperties(IDictionary properties) + { + if (properties != null) + { + var eventProperties = new Dictionary(_commonProperties); + foreach (var property in properties) + { + if (eventProperties.ContainsKey(property.Key)) + { + eventProperties[property.Key] = property.Value; + } + else + { + eventProperties.Add(property.Key, property.Value); + } + } + return eventProperties; + } + else + { + return _commonProperties; + } + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/project.json b/src/Microsoft.DotNet.Cli.Utils/project.json index 07a1fd577b..dd2833dcd3 100644 --- a/src/Microsoft.DotNet.Cli.Utils/project.json +++ b/src/Microsoft.DotNet.Cli.Utils/project.json @@ -5,6 +5,7 @@ "warningsAsErrors": true }, "dependencies": { + "Microsoft.ApplicationInsights": "2.0.0-rc1", "Microsoft.DotNet.ProjectModel": "1.0.0-*", "Microsoft.Extensions.PlatformAbstractions": "1.0.0-rc2-20100", "NuGet.Versioning": "3.5.0-beta-1130", diff --git a/src/dotnet/Program.cs b/src/dotnet/Program.cs index 7d8e02cb31..8833881c72 100644 --- a/src/dotnet/Program.cs +++ b/src/dotnet/Program.cs @@ -26,14 +26,13 @@ namespace Microsoft.DotNet.Cli { public class Program { - public static int Main(string[] args) { DebugHelper.HandleDebugSwitch(ref args); try { - return ProcessArgs(args); + return Program.ProcessArgs(args, new Telemetry()); } catch (CommandUnknownException e) { @@ -44,7 +43,7 @@ public static int Main(string[] args) } - private static int ProcessArgs(string[] args) + internal static int ProcessArgs(string[] args, ITelemetry telemetryClient) { // CommandLineApplication is a bit restrictive, so we parse things ourselves here. Individual apps should use CLA. @@ -117,22 +116,36 @@ private static int ProcessArgs(string[] args) ["test"] = TestCommand.Run }; + int exitCode; Func builtIn; if (builtIns.TryGetValue(command, out builtIn)) { - return builtIn(appArgs.ToArray()); + exitCode = builtIn(appArgs.ToArray()); + } + else + { + CommandResult result = Command.Create("dotnet-" + command, appArgs, FrameworkConstants.CommonFrameworks.NetStandardApp15) + .ForwardStdErr() + .ForwardStdOut() + .Execute(); + exitCode = result.ExitCode; } - return Command.Create("dotnet-" + command, appArgs, FrameworkConstants.CommonFrameworks.NetStandardApp15) - .ForwardStdErr() - .ForwardStdOut() - .Execute() - .ExitCode; + telemetryClient.TrackEvent( + command, + null, + new Dictionary + { + ["ExitCode"] = exitCode + }); + + return exitCode; + } private static void PrintVersion() { - Reporter.Output.WriteLine(HelpCommand.ProductVersion); + Reporter.Output.WriteLine(Product.Version); } private static void PrintInfo() @@ -142,7 +155,7 @@ private static void PrintInfo() var commitSha = GetCommitSha() ?? "N/A"; Reporter.Output.WriteLine(); Reporter.Output.WriteLine("Product Information:"); - Reporter.Output.WriteLine($" Version: {HelpCommand.ProductVersion}"); + Reporter.Output.WriteLine($" Version: {Product.Version}"); Reporter.Output.WriteLine($" Commit Sha: {commitSha}"); Reporter.Output.WriteLine(); var runtimeEnvironment = PlatformServices.Default.Runtime; diff --git a/src/dotnet/Properties/AssemblyInfo.cs b/src/dotnet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..07c3005ab7 --- /dev/null +++ b/src/dotnet/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("dotnet.Tests")] \ No newline at end of file diff --git a/src/dotnet/commands/dotnet-help/HelpCommand.cs b/src/dotnet/commands/dotnet-help/HelpCommand.cs index c64735f31e..30ecb35d08 100644 --- a/src/dotnet/commands/dotnet-help/HelpCommand.cs +++ b/src/dotnet/commands/dotnet-help/HelpCommand.cs @@ -8,7 +8,6 @@ namespace Microsoft.DotNet.Tools.Help { public class HelpCommand { - private const string ProductLongName = ".NET Command Line Tools"; private const string UsageText = @"Usage: dotnet [common-options] [command] [arguments] Arguments: @@ -29,13 +28,6 @@ public class HelpCommand test Executes tests in a test project repl Launch an interactive session (read, eval, print, loop) pack Creates a NuGet package"; - public static readonly string ProductVersion = GetProductVersion(); - - private static string GetProductVersion() - { - var attr = typeof(HelpCommand).GetTypeInfo().Assembly.GetCustomAttribute(); - return attr?.InformationalVersion; - } public static int Run(string[] args) { @@ -58,10 +50,10 @@ public static void PrintHelp() public static void PrintVersionHeader() { - var versionString = string.IsNullOrEmpty(ProductVersion) ? + var versionString = string.IsNullOrEmpty(Product.Version) ? string.Empty : - $" ({ProductVersion})"; - Reporter.Output.WriteLine(ProductLongName + versionString); + $" ({Product.Version})"; + Reporter.Output.WriteLine(Product.LongName + versionString); } } } diff --git a/test/dotnet.Tests/TelemetryCommandTest.cs b/test/dotnet.Tests/TelemetryCommandTest.cs new file mode 100644 index 0000000000..95c2818a8b --- /dev/null +++ b/test/dotnet.Tests/TelemetryCommandTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections.Generic; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Tools.Test.Utilities; +using Xunit; +using FluentAssertions; + +namespace Microsoft.DotNet.Tests +{ + public class MockTelemetry : ITelemetry + { + public string EventName{get;set;} + public void TrackEvent(string eventName, IDictionary properties, IDictionary measurements) + { + EventName = eventName; + } + } + + + public class TelemetryCommandTests : TestBase + { + [Fact] + public void TestProjectDependencyIsNotAvailableThroughDriver() + { + MockTelemetry mockTelemetry = new MockTelemetry(); + string[] args = { "help" }; + Program.ProcessArgs(args, mockTelemetry); + Assert.Equal(mockTelemetry.EventName, args[0]); + } + } +} diff --git a/test/dotnet.Tests/project.json b/test/dotnet.Tests/project.json index 65fe64ce42..634931a576 100644 --- a/test/dotnet.Tests/project.json +++ b/test/dotnet.Tests/project.json @@ -6,6 +6,9 @@ "Microsoft.DotNet.Tools.Tests.Utilities": { "target": "project" }, + "dotnet": { + "target": "project" + }, "Microsoft.DotNet.Cli.Utils": { "target": "project", "type": "build"