File: AppInsightsTelemetryClient.cs
Web Access
Project: src\src\dotnet-svcutil\lib\src\dotnet-svcutil-lib.csproj (dotnet-svcutil-lib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
 
namespace Microsoft.Tools.ServiceModel.Svcutil
{
    // Provides the TelemetryClient instance for logging telemetry using AppInsights.
    internal class AppInsightsTelemetryClient
    {
        private const string instrumentationKey = "97d0a8a2-1954-4c71-b95d-89df9627dccb";
        internal const string OptOutVariable = "DOTNET_SVCUTIL_TELEMETRY_OPTOUT";
        private const string eventNamePrefix = "VS/dotnetSvcutil/";
        private const string testModeVariable = "DOTNET_SVCUTIL_TEST_MODE";
 
        private static bool? s_isUserOptedIn = null;
        public static bool IsUserOptedIn
        {
            get
            {
                if (!s_isUserOptedIn.HasValue)
                {
                    string optOut = Environment.GetEnvironmentVariable(OptOutVariable);
                    if (string.IsNullOrEmpty(optOut))
                    {
                        s_isUserOptedIn = true;
                    }
                    else
                    {
                        // We parse the same values here as the dotnet SDK's opt out.
                        switch (optOut.ToLowerInvariant())
                        {
                            case "true":
                            case "1":
                            case "yes":
                                s_isUserOptedIn = false;
                                break;
                            case "false":
                            case "0":
                            case "no":
                            default:
                                s_isUserOptedIn = true;
                                break;
                        }
                    }
                }
 
                return s_isUserOptedIn.Value;
            }
            set
            {
                s_isUserOptedIn = value;
            }
        }
 
        private static readonly object s_lockObj = new object();
        private static AppInsightsTelemetryClient s_instance = null;
        private TelemetryClient _telemetryClient = null;
 
        private AppInsightsTelemetryClient(TelemetryClient telemetryClient)
        {
            _telemetryClient = telemetryClient;
        }
 
        public static async Task<AppInsightsTelemetryClient> GetInstanceAsync(CancellationToken cancellationToken)
        {
            if (s_instance == null)
            {
                try
                {
                    if (!bool.TryParse(Environment.GetEnvironmentVariable(testModeVariable), out bool testMode))
                    {
                        testMode = false;
                    }
 
                    lock (s_lockObj)
                    {
                        if (s_instance == null)
                        {
                            if (!IsUserOptedIn)
                            {
                                // If the user hasn't opted in return now with a null telemetry client to ensure we don't create any telemetry context.
                                return new AppInsightsTelemetryClient(null);
                            }
 
                            TelemetryConfiguration config;
                            
                            config = new TelemetryConfiguration();
 
                            config.TelemetryChannel.DeveloperMode = testMode;
 
                            s_instance = new AppInsightsTelemetryClient(new TelemetryClient(config));
                        }
                    }
 
                    var telemetryClient = s_instance._telemetryClient;
                    telemetryClient.InstrumentationKey = instrumentationKey;
 
                    // Populate context with properties that are common and should be logged for all events.
                    var context = telemetryClient.Context;
                    context.Device.OperatingSystem = GetOperatingSystemString();
 
#if !NETCORE10
                    // Set the user id to a stable hash of the user's current username. Users with the same username 
                    // or those with hash collisions will show up as the same id. So the user id won't be perfectly unique.
                    // However, it will give us some idea of how many different users are using the tool.
                    context.User.Id = GetStableHashCode(Environment.UserName).ToString();
#endif
 
                    // DebugLogger tracks telemetry when adding exceptions. We pass null for the logger to avoid the possibility of an endless cyclic call if something goes wrong in GetSdkVersionAsync.
                    var sdkVersion = await ProjectPropertyResolver.GetSdkVersionAsync(System.IO.Directory.GetCurrentDirectory(), null /* logger */, cancellationToken).ConfigureAwait(false);
                    context.GlobalProperties["SvcUtil.Version"] = Tool.PackageVersion;
                    context.GlobalProperties["Dotnet.Version"] = string.IsNullOrEmpty(sdkVersion) ? "unknown" : sdkVersion;
                    context.GlobalProperties["TestMode"] = testMode.ToString();
                }
                catch (Exception ex)
                {
#if DEBUG
                    ToolConsole.WriteWarning(ex.Message);
#endif
                    s_isUserOptedIn = false;
                }
            }
 
            return s_instance;
        }
 
        // This is copied from the 32 bit implementation from String.GetHashCode.
        // It's a stable string hashing algorithm so it won't change with each run of the tool.
        private static int GetStableHashCode(string str)
        {
            unsafe
            {
                fixed (char* src = str)
                {
                    int hash1 = (5381 << 16) + 5381;
                    int hash2 = hash1;
 
                    int* pint = (int*)src;
                    int len = str.Length;
                    while (len > 2)
                    {
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
                        hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1];
                        pint += 2;
                        len -= 4;
                    }
 
                    if (len > 0)
                    {
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
                    }
 
                    return hash1 + (hash2 * 1566083941);
                }
            }
        }
 
        private static string GetOperatingSystemString()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return "Windows";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                return "macOS";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                return "Linux";
            }
            else
            {
                return "Unknown";
            }
        }
 
        public void TrackEvent(string eventName)
        {
            if (IsUserOptedIn)
            {
                _telemetryClient.TrackEvent(eventNamePrefix + eventName);
                _telemetryClient.Flush();
            }
        }
 
        public void TrackEvent(string eventName, Dictionary<string, string> properties)
        {
            if (IsUserOptedIn)
            {
                _telemetryClient.TrackEvent(eventNamePrefix + eventName, properties);
                _telemetryClient.Flush();
            }
        }
 
        public void TrackError(string eventName, Exception exceptionObject)
        {
            this.TrackError(eventName, exceptionObject.ToString());
        }
 
        public void TrackError(string eventName, string exceptionString)
        {
            if (IsUserOptedIn)
            {
                var properties = new Dictionary<string, string>();
                properties.Add("ExceptionString", exceptionString);
 
                _telemetryClient.TrackEvent(eventNamePrefix + eventName, properties);
                _telemetryClient.Flush();
            }
        }
    }
}