File: Commands\Run\RunTelemetry.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.Immutable;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;
using Microsoft.DotNet.Cli.Utils;
 
namespace Microsoft.DotNet.Cli.Commands.Run;
 
/// <summary>
/// Provides telemetry functionality for dotnet run command.
/// </summary>
internal static class RunTelemetry
{
    private const string RunEventName = "run";
 
    /// <summary>
    /// Sends telemetry for a dotnet run operation.
    /// </summary>
    /// <param name="isFileBased">True if this is a file-based app run, false for project-based</param>
    /// <param name="projectIdentifier">Obfuscated unique project identifier</param>
    /// <param name="launchProfile">The launch profile name if specified</param>
    /// <param name="noLaunchProfile">True if --no-launch-profile was specified</param>
    /// <param name="launchSettings">The applied launch settings model if any</param>
    /// <param name="sdkCount">Number of SDKs used (defaults to 1)</param>
    /// <param name="packageReferenceCount">Number of PackageReferences (defaults to 0)</param>
    /// <param name="projectReferenceCount">Number of ProjectReferences (defaults to 0)</param>
    /// <param name="additionalPropertiesCount">Number of additional properties (file-based only, defaults to 0)</param>
    /// <param name="usedMSBuild">Whether MSBuild was used (file-based only)</param>
    /// <param name="usedRoslynCompiler">Whether Roslyn compiler was used directly (file-based only)</param>
    public static void TrackRunEvent(
        bool isFileBased,
        string projectIdentifier,
        string? launchProfile = null,
        bool noLaunchProfile = false,
        ProjectLaunchSettingsModel? launchSettings = null,
        int sdkCount = 1,
        int packageReferenceCount = 0,
        int projectReferenceCount = 0,
        int additionalPropertiesCount = 0,
        bool? usedMSBuild = null,
        bool? usedRoslynCompiler = null)
    {
        var properties = new Dictionary<string, string?>
        {
            ["app_type"] = isFileBased ? "file_based" : "project_based",
            ["project_id"] = projectIdentifier,
            ["sdk_count"] = sdkCount.ToString(),
            ["package_reference_count"] = packageReferenceCount.ToString(),
            ["project_reference_count"] = projectReferenceCount.ToString(),
        };
 
        // Launch profile telemetry
        if (noLaunchProfile)
        {
            properties["launch_profile_requested"] = "none";
        }
        else if (!string.IsNullOrEmpty(launchProfile))
        {
            properties["launch_profile_requested"] = "explicit";
            properties["launch_profile_is_default"] = IsDefaultProfile(launchProfile) ? "true" : "false";
        }
        else if (launchSettings != null)
        {
            properties["launch_profile_requested"] = "default_used";
            properties["launch_profile_is_default"] = IsDefaultProfile(launchSettings.LaunchProfileName) ? "true" : "false";
        }
        else
        {
            properties["launch_profile_requested"] = "false";
        }
 
        // File-based app specific telemetry
        if (isFileBased)
        {
            properties["additional_properties_count"] = additionalPropertiesCount.ToString();
            if (usedMSBuild.HasValue)
            {
                properties["used_msbuild"] = usedMSBuild.Value ? "true" : "false";
            }
            if (usedRoslynCompiler.HasValue)
            {
                properties["used_roslyn_compiler"] = usedRoslynCompiler.Value ? "true" : "false";
            }
        }
 
        TelemetryEventEntry.TrackEvent(RunEventName, properties, measurements: null);
    }
 
    /// <summary>
    /// Generates an obfuscated unique identifier for a project-based app.
    /// </summary>
    /// <param name="projectFilePath">Full path to the project file</param>
    /// <param name="repoRoot">Repository root path if available</param>
    /// <param name="hasher">Optional hasher function, defaults to Sha256Hasher.Hash</param>
    /// <returns>Hashed project identifier</returns>
    public static string GetProjectBasedIdentifier(string projectFilePath, string? repoRoot = null, Func<string, string>? hasher = null)
    {
        // Use relative path from repo root if available, otherwise use full path
        string pathToHash = repoRoot != null && projectFilePath.StartsWith(repoRoot, StringComparison.OrdinalIgnoreCase)
            ? Path.GetRelativePath(repoRoot, projectFilePath)
            : projectFilePath;
 
        // For backward compatibility, normalize case before hashing when using default hasher
        if (hasher == null)
        {
            return Sha256Hasher.Hash(pathToHash.ToLowerInvariant());
        }
        else
        {
            return hasher(pathToHash);
        }
    }
 
    /// <summary>
    /// Generates an obfuscated unique identifier for a file-based app.
    /// This leverages the same caching infrastructure identifier already used.
    /// </summary>
    /// <param name="entryPointFilePath">Full path to the entry point file</param>
    /// <param name="hasher">Optional hasher function, defaults to Sha256Hasher.Hash</param>
    /// <returns>Hashed file identifier</returns>
    public static string GetFileBasedIdentifier(string entryPointFilePath, Func<string, string>? hasher = null)
    {
        // For backward compatibility, normalize case before hashing when using default hasher
        if (hasher == null)
        {
            return Sha256Hasher.Hash(entryPointFilePath.ToLowerInvariant());
        }
        else
        {
            return hasher(entryPointFilePath);
        }
    }
 
    /// <summary>
    /// Counts the number of SDKs used in a file-based app.
    /// </summary>
    /// <param name="directives">Directives for file-based apps</param>
    /// <returns>Number of SDKs</returns>
    public static int CountSdks(ImmutableArray<CSharpDirective> directives)
    {
        if (directives.IsDefaultOrEmpty)
        {
            return 1; // Default assumption - Microsoft.NET.Sdk
        }
 
        // File-based: count SDK directives
        var sdkDirectives = directives.OfType<CSharpDirective.Sdk>().Count();
        // If no explicit SDK directives, there's still the default Microsoft.NET.Sdk
        return sdkDirectives > 0 ? sdkDirectives : 1;
    }
 
    /// <summary>
    /// Counts the number of PackageReferences in a file-based app.
    /// </summary>
    /// <param name="directives">Directives for file-based apps</param>
    /// <returns>Number of package references</returns>
    public static int CountPackageReferences(ImmutableArray<CSharpDirective> directives)
    {
        if (directives.IsDefaultOrEmpty)
        {
            return 0;
        }
 
        // File-based: count package directives
        return directives.OfType<CSharpDirective.Package>().Count();
    }
 
    /// <summary>
    /// Counts the number of direct ProjectReferences in a file-based app.
    /// </summary>
    /// <param name="directives">Directives for file-based apps</param>
    /// <returns>Number of project references</returns>
    public static int CountProjectReferences(ImmutableArray<CSharpDirective> directives)
    {
        if (directives.IsDefaultOrEmpty)
        {
            return 0;
        }
 
        // File-based: count project directives
        return directives.OfType<CSharpDirective.Project>().Count();
    }
 
    /// <summary>
    /// Counts the number of PackageReferences in a project-based app.
    /// </summary>
    /// <param name="project">Project instance for project-based apps</param>
    /// <returns>Number of package references</returns>
    public static int CountPackageReferences(ProjectInstance project)
    {
        return project.GetItems("PackageReference").Count;
    }
 
    /// <summary>
    /// Counts the number of direct ProjectReferences in a project-based app.
    /// </summary>
    /// <param name="project">Project instance for project-based apps</param>
    /// <returns>Number of project references</returns>
    public static int CountProjectReferences(ProjectInstance project)
    {
        return project.GetItems("ProjectReference").Count;
    }
 
    /// <summary>
    /// Counts the number of additional properties for file-based apps.
    /// </summary>
    /// <param name="directives">Directives for file-based apps</param>
    /// <returns>Number of additional properties</returns>
    public static int CountAdditionalProperties(ImmutableArray<CSharpDirective> directives)
    {
        if (directives.IsDefaultOrEmpty)
        {
            return 0;
        }
 
        return directives.OfType<CSharpDirective.Property>().Count();
    }
 
    /// <summary>
    /// Determines if a launch profile name is one of the default ones.
    /// </summary>
    /// <param name="profileName">The profile name to check</param>
    /// <returns>True if it's a default profile name</returns>
    private static bool IsDefaultProfile(string? profileName)
    {
        if (string.IsNullOrEmpty(profileName))
        {
            return false;
        }
 
        // The default profile name at this point is "(Default)"
        return profileName.Equals("(Default)", StringComparison.OrdinalIgnoreCase);
    }
}