File: Telemetry\Telemetry.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Frozen;
using System.Diagnostics;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Configurer;
using CLIRuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment;
 
namespace Microsoft.DotNet.Cli.Telemetry;
 
public class Telemetry : ITelemetry
{
    internal static string? CurrentSessionId = null;
    internal static bool DisabledForTests = false;
    private readonly int _senderCount;
    private TelemetryClient? _client = null;
    private FrozenDictionary<string, string>? _commonProperties = null;
    private FrozenDictionary<string, double>? _commonMeasurements = null;
    private Task? _trackEventTask = null;
 
    private const string ConnectionString = "InstrumentationKey=74cc1c9e-3e6e-4d05-b3fc-dde9101d0254";
 
    public bool Enabled { get; }
 
    public Telemetry() : this(null) { }
 
    public Telemetry(IFirstTimeUseNoticeSentinel? sentinel) : this(sentinel, null) { }
 
    public Telemetry(
        IFirstTimeUseNoticeSentinel? sentinel,
        string? sessionId,
        bool blockThreadInitialization = false,
        IEnvironmentProvider? environmentProvider = null,
        int senderCount = 3)
    {
 
        if (DisabledForTests)
        {
            return;
        }
 
        environmentProvider ??= new EnvironmentProvider();
 
        Enabled = !environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault)
                    && PermissionExists(sentinel);
 
        if (!Enabled)
        {
            return;
        }
 
        // Store the session ID in a static field so that it can be reused
        CurrentSessionId = sessionId ?? Guid.NewGuid().ToString();
        _senderCount = senderCount;
        if (blockThreadInitialization)
        {
            InitializeTelemetry();
        }
        else
        {
            //initialize in task to offload to parallel thread
            _trackEventTask = Task.Run(() => InitializeTelemetry());
        }
    }
 
    internal static void DisableForTests()
    {
        DisabledForTests = true;
        CurrentSessionId = null;
    }
 
    internal static void EnableForTests()
    {
        DisabledForTests = false;
    }
 
    private static bool PermissionExists(IFirstTimeUseNoticeSentinel? sentinel)
    {
        if (sentinel == null)
        {
            return false;
        }
 
        return sentinel.Exists();
    }
 
    public void TrackEvent(string eventName, IDictionary<string, string> properties,
        IDictionary<string, double> measurements)
    {
        if (!Enabled)
        {
            return;
        }
 
        //continue the task in different threads
        if (_trackEventTask == null)
        {
            _trackEventTask = Task.Run(() => TrackEventTask(eventName, properties, measurements));
            return;
        }
        else
        {
            _trackEventTask = _trackEventTask.ContinueWith(
                x => TrackEventTask(eventName, properties, measurements)
            );
        }
    }
 
    public void Flush()
    {
        if (!Enabled || _trackEventTask == null)
        {
            return;
        }
 
        _trackEventTask.Wait();
    }
 
    // Adding dispose on graceful shutdown per https://github.com/microsoft/ApplicationInsights-dotnet/issues/1152#issuecomment-518742922
    public void Dispose()
    {
        if (_client != null)
        {
            _client.TelemetryConfiguration.Dispose();
            _client = null;
        }
    }
 
    public void ThreadBlockingTrackEvent(string eventName, IDictionary<string, string> properties, IDictionary<string, double> measurements)
    {
        if (!Enabled)
        {
            return;
        }
        TrackEventTask(eventName, properties, measurements);
    }
 
    private void InitializeTelemetry()
    {
        try
        {
            var persistenceChannel = new PersistenceChannel.PersistenceChannel(sendersCount: _senderCount)
            {
                SendingInterval = TimeSpan.FromMilliseconds(1)
            };
 
            var config = TelemetryConfiguration.CreateDefault();
            config.TelemetryChannel = persistenceChannel;
            config.ConnectionString = ConnectionString;
            _client = new TelemetryClient(config);
            _client.Context.Session.Id = CurrentSessionId;
            _client.Context.Device.OperatingSystem = CLIRuntimeEnvironment.OperatingSystem;
 
            _commonProperties = new TelemetryCommonProperties().GetTelemetryCommonProperties(CurrentSessionId);
            _commonMeasurements = FrozenDictionary<string, double>.Empty;
        }
        catch (Exception e)
        {
            _client = null;
            // we dont want to fail the tool if telemetry fails.
            Debug.Fail(e.ToString());
        }
    }
 
    private void TrackEventTask(
        string eventName,
        IDictionary<string, string> properties,
        IDictionary<string, double> measurements)
    {
        if (_client == null)
        {
            return;
        }
 
        try
        {
            var eventProperties = GetEventProperties(properties);
            var eventMeasurements = GetEventMeasures(measurements);
 
            eventProperties ??= new Dictionary<string, string>();
            eventProperties.Add("event id", Guid.NewGuid().ToString());
 
            _client.TrackEvent(PrependProducerNamespace(eventName), eventProperties, eventMeasurements);
            Activity.Current?.AddEvent(CreateActivityEvent(eventName, eventProperties, eventMeasurements));
        }
        catch (Exception e)
        {
            Debug.Fail(e.ToString());
        }
    }
 
    private static ActivityEvent CreateActivityEvent(
        string eventName,
        IDictionary<string, string>? properties,
        IDictionary<string, double>? measurements)
    {
        var tags = MakeTags(properties, measurements);
        return new ActivityEvent(
            PrependProducerNamespace(eventName),
            tags: tags);
    }
 
    private static ActivityTagsCollection? MakeTags(
        IDictionary<string, string>? properties,
        IDictionary<string, double>? measurements)
    {
        if (properties == null && measurements == null)
        {
            return null;
        }
        else if (properties != null && measurements == null)
        {
            return [.. properties.Select(p => new KeyValuePair<string, object?>(p.Key, p.Value))];
        }
        else if (properties == null && measurements != null)
        {
            return [.. measurements.Select(m => new KeyValuePair<string, object?>(m.Key, m.Value.ToString()))];
        }
        else return [ .. properties!.Select(p => new KeyValuePair<string, object?>(p.Key, p.Value)),
                 .. measurements!.Select(m => new KeyValuePair<string, object?>(m.Key, m.Value.ToString())) ];
    }
 
    private static string PrependProducerNamespace(string eventName) => $"dotnet/cli/{eventName}";
 
    private IDictionary<string, double>? GetEventMeasures(IDictionary<string, double>? measurements)
    {
        return (measurements, _commonMeasurements) switch
        {
            (null, null) => null,
            (null, not null) => _commonMeasurements == FrozenDictionary<string, double>.Empty ? null : new Dictionary<string, double>(_commonMeasurements),
            (not null, null) => measurements,
            (not null, not null) => Combine(_commonMeasurements, measurements),
        };
    }
 
    private IDictionary<string, string>? GetEventProperties(IDictionary<string, string>? properties)
    {
        return (properties, _commonProperties) switch
        {
            (null, null) => null,
            (null, not null) => _commonProperties == FrozenDictionary<string, string>.Empty ? null : new Dictionary<string, string>(_commonProperties),
            (not null, null) => properties,
            (not null, not null) => Combine(_commonProperties, properties),
        };
    }
 
    static IDictionary<TKey, TValue> Combine<TKey, TValue>(IDictionary<TKey, TValue> common, IDictionary<TKey, TValue> specific) where TKey : notnull
    {
        IDictionary<TKey, TValue> eventMeasurements = new Dictionary<TKey, TValue>(capacity: common.Count + specific.Count);
        foreach (KeyValuePair<TKey, TValue> measurement in common)
        {
            eventMeasurements[measurement.Key] = measurement.Value;
        }
        foreach (KeyValuePair<TKey, TValue> measurement in specific)
        {
            eventMeasurements[measurement.Key] = measurement.Value;
        }
            return eventMeasurements;
        }
}