File: CommandTests\Run\RunTelemetryTests.cs
Web Access
Project: ..\..\..\test\dotnet.Tests\dotnet.Tests.csproj (dotnet.Tests)
// 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;
using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;
using Microsoft.DotNet.Cli.Utils;
 
namespace Microsoft.DotNet.Cli.Run.Tests;
 
public class RunTelemetryTests : SdkTest
{
    public RunTelemetryTests(ITestOutputHelper log) : base(log)
    {
    }
 
    [Fact]
    public void GetFileBasedIdentifier_ReturnsDifferentHashesForDifferentPaths()
    {
        // Arrange
        var path1 = "/some/path/to/file1.cs";
        var path2 = "/some/path/to/file2.cs";
 
        // Act
        var hash1 = RunTelemetry.GetFileBasedIdentifier(path1);
        var hash2 = RunTelemetry.GetFileBasedIdentifier(path2);
 
        // Assert
        hash1.Should().NotBe(hash2);
        hash1.Should().HaveLength(64); // SHA256 hex string length
        hash2.Should().HaveLength(64);
    }
 
    [Fact]
    public void GetProjectBasedIdentifier_ReturnsSameHashForSamePath()
    {
        // Arrange
        var path = "/some/path/to/project.csproj";
 
        // Act
        var hash1 = RunTelemetry.GetProjectBasedIdentifier(path);
        var hash2 = RunTelemetry.GetProjectBasedIdentifier(path);
 
        // Assert
        hash1.Should().Be(hash2);
        hash1.Should().HaveLength(64);
    }
 
    [Fact]
    public void GetProjectBasedIdentifier_UsesRelativePathWhenRepoRootProvided()
    {
        // Arrange
        var repoRoot = "/repo/root".Replace('/', Path.DirectorySeparatorChar);
        var projectPath = "/repo/root/src/project.csproj".Replace('/', Path.DirectorySeparatorChar);
        var expectedRelativePath = "src/project.csproj".Replace('/', Path.DirectorySeparatorChar);
 
        // Act
        var hashWithRepo = RunTelemetry.GetProjectBasedIdentifier(projectPath, repoRoot);
        var hashOfRelative = RunTelemetry.GetProjectBasedIdentifier(expectedRelativePath);
 
        // Assert
        hashWithRepo.Should().Be(hashOfRelative);
    }
 
    [Fact]
    public void CountSdks_FileBasedApp_CountsDirectives()
    {
        // Arrange
        var directives = ImmutableArray.Create<CSharpDirective>(
            new CSharpDirective.Sdk(default) { Name = "Microsoft.NET.Sdk" },
            new CSharpDirective.Sdk(default) { Name = "Microsoft.NET.Sdk.Web", Version = "1.0.0" }
        );
 
        // Act
        var count = RunTelemetry.CountSdks(directives);
 
        // Assert
        count.Should().Be(2);
    }
 
    [Fact]
    public void CountSdks_FileBasedApp_NoDirectives_ReturnsDefaultOne()
    {
        // Arrange
        var directives = ImmutableArray<CSharpDirective>.Empty;
 
        // Act
        var count = RunTelemetry.CountSdks(directives);
 
        // Assert
        count.Should().Be(1); // Default Microsoft.NET.Sdk
    }
 
    [Fact]
    public void CountPackageReferences_FileBasedApp_CountsDirectives()
    {
        // Arrange
        var directives = ImmutableArray.Create<CSharpDirective>(
            new CSharpDirective.Package(default) { Name = "Newtonsoft.Json", Version = "13.0.1" },
            new CSharpDirective.Package(default) { Name = "Microsoft.Extensions.DependencyInjection" }
        );
 
        // Act
        var count = RunTelemetry.CountPackageReferences(directives);
 
        // Assert
        count.Should().Be(2);
    }
 
    [Fact]
    public void CountProjectReferences_FileBasedApp_CountsDirectives()
    {
        // Arrange
        var directives = ImmutableArray.Create<CSharpDirective>(
            new CSharpDirective.Project(default) { Name = "../lib/Library.csproj" },
            new CSharpDirective.Project(default) { Name = "../common/Common.csproj" }
        );
 
        // Act
        var count = RunTelemetry.CountProjectReferences(directives);
 
        // Assert
        count.Should().Be(2);
    }
 
    [Fact]
    public void CountAdditionalProperties_CountsPropertyDirectives()
    {
        // Arrange
        var directives = ImmutableArray.Create<CSharpDirective>(
            new CSharpDirective.Property(default) { Name = "TargetFramework", Value = "net8.0" },
            new CSharpDirective.Property(default) { Name = "Nullable", Value = "enable" }
        );
 
        // Act
        var count = RunTelemetry.CountAdditionalProperties(directives);
 
        // Assert
        count.Should().Be(2);
    }
 
    [Fact]
    public void TrackRunEvent_FileBasedApp_SendsCorrectTelemetry()
    {
        // Arrange
        var events = new List<(string? eventName, IDictionary<string, string?>? properties, IDictionary<string, double>? measurements)>();
        
        void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
        
        TelemetryEventEntry.EntryPosted += handler;
 
        try
        {
            // Act
            RunTelemetry.TrackRunEvent(
                isFileBased: true,
                projectIdentifier: "test-hash",
                launchProfile: "(Default)",
                noLaunchProfile: false,
                launchSettings: null,
                sdkCount: 2,
                packageReferenceCount: 3,
                projectReferenceCount: 1,
                additionalPropertiesCount: 2,
                usedMSBuild: true,
                usedRoslynCompiler: false);
 
            // Assert
            events.Should().HaveCount(1);
            var eventData = events[0];
            eventData.eventName.Should().Be("run");
            eventData.properties.Should().NotBeNull();
            
            var props = eventData.properties!;
            props["app_type"].Should().Be("file_based");
            props["project_id"].Should().Be("test-hash");
            props["sdk_count"].Should().Be("2");
            props["package_reference_count"].Should().Be("3");
            props["project_reference_count"].Should().Be("1");
            props["additional_properties_count"].Should().Be("2");
            props["used_msbuild"].Should().Be("true");
            props["used_roslyn_compiler"].Should().Be("false");
            props["launch_profile_requested"].Should().Be("explicit");
            props["launch_profile_is_default"].Should().Be("true");
        }
        finally
        {
            // Cleanup
            TelemetryEventEntry.EntryPosted -= handler;
        }
    }
 
    [Fact]
    public void TrackRunEvent_ProjectBasedApp_SendsCorrectTelemetry()
    {
        // Arrange
        var events = new List<(string? eventName, IDictionary<string, string?>? properties, IDictionary<string, double>? measurements)>();
        
        void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
        
        TelemetryEventEntry.EntryPosted += handler;
 
        try
        {
            // Act
            RunTelemetry.TrackRunEvent(
                isFileBased: false,
                projectIdentifier: "project-hash",
                launchProfile: null,
                noLaunchProfile: true,
                launchSettings: null,
                sdkCount: 1,
                packageReferenceCount: 5,
                projectReferenceCount: 2);
 
            // Assert
            events.Should().HaveCount(1);
            var eventData = events[0];
            eventData.eventName.Should().Be("run");
            eventData.properties.Should().NotBeNull();
            
            var props = eventData.properties!;
            props["app_type"].Should().Be("project_based");
            props["project_id"].Should().Be("project-hash");
            props["sdk_count"].Should().Be("1");
            props["package_reference_count"].Should().Be("5");
            props["project_reference_count"].Should().Be("2");
            props["launch_profile_requested"].Should().Be("none");
            props.Should().NotContainKey("additional_properties_count");
            props.Should().NotContainKey("used_msbuild");
            props.Should().NotContainKey("used_roslyn_compiler");
        }
        finally
        {
            // Cleanup
            TelemetryEventEntry.EntryPosted -= handler;
        }
    }
 
    [Fact]
    public void TrackRunEvent_WithDefaultLaunchProfile_MarksTelemetryCorrectly()
    {
        // Arrange
        var events = new List<(string? eventName, IDictionary<string, string?>? properties, IDictionary<string, double>? measurements)>();
        
        void handler(object? sender, InstrumentationEventArgs args) => events.Add((args.EventName, args.Properties, args.Measurements));
        
        TelemetryEventEntry.EntryPosted += handler;
 
        var launchSettings = new ProjectLaunchSettingsModel
        {
            LaunchProfileName = "(Default)"
        };
 
        try
        {
            // Act
            RunTelemetry.TrackRunEvent(
                isFileBased: false,
                projectIdentifier: "test-hash",
                launchProfile: null,
                noLaunchProfile: false,
                launchSettings: launchSettings,
                sdkCount: 1,
                packageReferenceCount: 0,
                projectReferenceCount: 0);
 
            // Assert
            events.Should().HaveCount(1);
            var props = events[0].properties!;
            props["launch_profile_requested"].Should().Be("default_used");
            props["launch_profile_is_default"].Should().Be("true");
        }
        finally
        {
            // Cleanup
            TelemetryEventEntry.EntryPosted -= handler;
        }
    }
}