File: Telemetry\TelemetryFilter.cs
Web Access
Project: src\src\sdk\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.CommandLine;
using System.Globalization;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.VSTest;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Cli.Telemetry;

internal class TelemetryFilter(Func<string, string>? hash) : ITelemetryFilter
{
    private const string ExceptionEventName = "mainCatchException/exception";
    private readonly Func<string, string> _hash = hash ?? throw new ArgumentNullException(nameof(hash));

    public IEnumerable<TelemetryEntryFormat> Filter(ParseResult parseResult) =>
        Hash(FilterImpl(parseResult, globalJsonState: null));

    public IEnumerable<TelemetryEntryFormat> Filter(ParseResultWithGlobalJsonState parseData) =>
        Hash(FilterImpl(parseData.ParseResult, parseData.GlobalJsonState));

    public IEnumerable<TelemetryEntryFormat> Filter(InstallerSuccessReport report)
    {
        var reportProperties = new Dictionary<string, string?>
        {
            { "exeName", report.ExeName }
        };
        return Hash([new TelemetryEntryFormat("install/reportsuccess", reportProperties)]);
    }

    public IEnumerable<TelemetryEntryFormat> Filter(Exception exception)
    {
        var exceptionProperties = new Dictionary<string, string?>
        {
            { "exceptionType", exception.GetType().ToString() },
            { "detail", ExceptionToStringWithoutMessage(exception) }
        };
        return Hash([new TelemetryEntryFormat(ExceptionEventName, exceptionProperties)]);
    }

    private static IEnumerable<TelemetryEntryFormat> FilterImpl(ParseResult parseResult, string? globalJsonState)
    {
        var topLevelCommandName = parseResult.RootSubCommandResult();
        if (topLevelCommandName is null)
        {
            yield break;
        }

        Dictionary<string, string?> properties = new() { ["verb"] = topLevelCommandName };
        if (!string.IsNullOrEmpty(globalJsonState))
        {
            properties["globalJson"] = globalJsonState;
        }

        yield return new TelemetryEntryFormat("toplevelparser/command", properties);

        if (parseResult.IsDotnetBuiltInCommand() &&
            parseResult.SafelyGetValueForOption<VerbosityOptions>("--verbosity") is VerbosityOptions verbosity)
        {
            var verbosityProperties = new Dictionary<string, string?>()
            {
                { "verb", topLevelCommandName},
                { "verbosity", Enum.GetName(verbosity)}
            };
            yield return new TelemetryEntryFormat("sublevelparser/command", verbosityProperties);
        }

        if (topLevelCommandName == "package" &&
            parseResult.CommandResult.Command != null &&
            parseResult.CommandResult.Command.Name == "update")
        {
            var hasVulnerableOption = parseResult.HasOption("--vulnerable");
            var vulnerableProperties = new Dictionary<string, string?>()
            {
                { "verb", "package update" },
                { "vulnerable", hasVulnerableOption.ToString()}
            };
            yield return new TelemetryEntryFormat("sublevelparser/command", vulnerableProperties);
        }

        foreach (IParseResultLogRule rule in ParseResultLogRules)
        {
            foreach (TelemetryEntryFormat allowList in rule.AllowList(parseResult))
            {
                yield return allowList;
            }
        }
    }

    public IEnumerable<TelemetryEntryFormat> Hash(IEnumerable<TelemetryEntryFormat> entries) =>
        entries.Select(entry => entry.EventName == ExceptionEventName ? entry : entry.WithAppliedToPropertiesValue(_hash));

    private static List<IParseResultLogRule> ParseResultLogRules =>
    [
        new AllowListToSendFirstArgument(["new", "help"]),
        new AllowListToSendFirstAppliedOptions(["add", "remove", "list", "solution", "nuget"]),
        new TopLevelCommandNameAndOptionToLog
        (
            topLevelCommandName: ["build", "publish"],
            optionsToLog: [ CommonOptions.FrameworkOptionName, TargetPlatformOptions.RuntimeOptionName, CommonOptions.ConfigurationOptionName ]
        ),
        new TopLevelCommandNameAndOptionToLog
        (
            topLevelCommandName: ["run", "clean", "test"],
            optionsToLog: [CommonOptions.FrameworkOptionName, CommonOptions.ConfigurationOptionName]
        ),
        new TopLevelCommandNameAndOptionToLog
        (
            topLevelCommandName: ["pack"],
            optionsToLog: [CommonOptions.ConfigurationOptionName]
        ),
        new TopLevelCommandNameAndOptionToLog
        (
            topLevelCommandName: ["vstest"],
            optionsToLog: [VSTestCommandDefinition.TestPlatformOptionName, VSTestCommandDefinition.TestFrameworkOptionName, VSTestCommandDefinition.TestLoggerOptionName]
        ),
        new TopLevelCommandNameAndOptionToLog
        (
            topLevelCommandName: ["publish"],
            optionsToLog: [TargetPlatformOptions.RuntimeOptionName]
        ),
        new AllowListToSendVerbSecondVerbFirstArgument(["workload", "tool", "new"]),
    ];

    private static string ExceptionToStringWithoutMessage(Exception e)
    {
        const string AggregateException_ToString = "{0}{1}---> (Inner Exception #{2}) {3}{4}{5}";
        if (e is AggregateException aggregate)
        {
            string text = NonAggregateExceptionToStringWithoutMessage(aggregate);

            for (int i = 0; i < aggregate.InnerExceptions.Count; i++)
            {
                text = string.Format(CultureInfo.InvariantCulture,
                                     AggregateException_ToString,
                                     text,
                                     Environment.NewLine,
                                     i,
                                     ExceptionToStringWithoutMessage(aggregate.InnerExceptions[i]),
                                     "<---",
                                     Environment.NewLine);
            }

            return text;
        }
        else
        {
            return NonAggregateExceptionToStringWithoutMessage(e);
        }
    }

    private static string NonAggregateExceptionToStringWithoutMessage(Exception e)
    {
        string s;
        const string Exception_EndOfInnerExceptionStack = "--- End of inner exception stack trace ---";

        s = e.GetType().ToString();
        if (e.InnerException != null)
        {
            s = s + " ---> " + ExceptionToStringWithoutMessage(e.InnerException) + Environment.NewLine +
            "   " + Exception_EndOfInnerExceptionStack;
        }

        var stackTrace = e.StackTrace;
        if (stackTrace != null)
        {
            s += Environment.NewLine + stackTrace;
        }
        return s;
    }
}