File: BackEnd\KnownTelemetry_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
using System;
using System.Globalization;
using Microsoft.Build.Framework.Telemetry;
using Shouldly;
using Xunit;
using static Microsoft.Build.BackEnd.Logging.BuildErrorTelemetryTracker;
using static Microsoft.Build.Framework.Telemetry.BuildInsights;
using static Microsoft.Build.Framework.Telemetry.TelemetryDataUtils;
 
namespace Microsoft.Build.UnitTests.Telemetry;
 
public class KnownTelemetry_Tests
{
    [Fact]
    public void BuildTelemetryCanBeSetToNull()
    {
        KnownTelemetry.PartialBuildTelemetry = new BuildTelemetry();
        KnownTelemetry.PartialBuildTelemetry = null;
 
        KnownTelemetry.PartialBuildTelemetry.ShouldBeNull();
    }
 
    [Fact]
    public void BuildTelemetryCanBeSet()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        KnownTelemetry.PartialBuildTelemetry = buildTelemetry;
 
        KnownTelemetry.PartialBuildTelemetry.ShouldBeSameAs(buildTelemetry);
    }
 
    [Fact]
    public void BuildTelemetryConstructedHasNoProperties()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
 
        buildTelemetry.BuildEngineDisplayVersion.ShouldBeNull();
        buildTelemetry.EventName.ShouldBe("build");
        buildTelemetry.FinishedAt.ShouldBeNull();
        buildTelemetry.BuildEngineFrameworkName.ShouldBeNull();
        buildTelemetry.BuildEngineHost.ShouldBeNull();
        buildTelemetry.InitialMSBuildServerState.ShouldBeNull();
        buildTelemetry.InnerStartAt.ShouldBeNull();
        buildTelemetry.ProjectPath.ShouldBeNull();
        buildTelemetry.ServerFallbackReason.ShouldBeNull();
        buildTelemetry.StartAt.ShouldBeNull();
        buildTelemetry.BuildSuccess.ShouldBeNull();
        buildTelemetry.BuildTarget.ShouldBeNull();
        buildTelemetry.BuildEngineVersion.ShouldBeNull();
        buildTelemetry.BuildCheckEnabled.ShouldBeNull();
        buildTelemetry.MultiThreadedModeEnabled.ShouldBeNull();
        buildTelemetry.SACEnabled.ShouldBeNull();
 
        buildTelemetry.GetProperties().ShouldBeEmpty();
    }
 
    [Fact]
    public void BuildTelemetryCreateProperProperties()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
 
        DateTime startAt = new DateTime(2023, 01, 02, 10, 11, 22);
        DateTime innerStartAt = new DateTime(2023, 01, 02, 10, 20, 30);
        DateTime finishedAt = new DateTime(2023, 12, 13, 14, 15, 16);
 
        buildTelemetry.BuildEngineDisplayVersion = "Some Display Version";
        buildTelemetry.FinishedAt = finishedAt;
        buildTelemetry.BuildEngineFrameworkName = "new .NET";
        buildTelemetry.BuildEngineHost = "Host description";
        buildTelemetry.InitialMSBuildServerState = "hot";
        buildTelemetry.InnerStartAt = innerStartAt;
        buildTelemetry.ProjectPath = "C:/dev/theProject";
        buildTelemetry.ServerFallbackReason = "busy";
        buildTelemetry.StartAt = startAt;
        buildTelemetry.BuildSuccess = true;
        buildTelemetry.BuildTarget = "Clean";
        buildTelemetry.BuildEngineVersion = new Version(1, 2, 3, 4);
        buildTelemetry.BuildCheckEnabled = true;
        buildTelemetry.MultiThreadedModeEnabled = false;
        buildTelemetry.SACEnabled = true;
 
        var properties = buildTelemetry.GetProperties();
 
        properties.Count.ShouldBe(14);
 
        properties["BuildEngineDisplayVersion"].ShouldBe("Some Display Version");
        properties["BuildEngineFrameworkName"].ShouldBe("new .NET");
        properties["BuildEngineHost"].ShouldBe("Host description");
        properties["InitialMSBuildServerState"].ShouldBe("hot");
        properties["ProjectPath"].ShouldBe("theProject");
        properties["ServerFallbackReason"].ShouldBe("busy");
        properties["BuildSuccess"].ShouldBe("True");
        properties["BuildTarget"].ShouldBe("Clean");
        properties["BuildEngineVersion"].ShouldBe("1.2.3.4");
        properties["BuildCheckEnabled"].ShouldBe("True");
        properties["MultiThreadedModeEnabled"].ShouldBe("False");
        properties["SACEnabled"].ShouldBe("True");
 
        // verify computed
        properties["BuildDurationInMilliseconds"] = (finishedAt - startAt).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
        properties["InnerBuildDurationInMilliseconds"] = (finishedAt - innerStartAt).TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
    }
 
    [Fact]
    public void BuildTelemetryHandleNullsInRecordedTimes()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
 
        buildTelemetry.StartAt = DateTime.MinValue;
        buildTelemetry.FinishedAt = null;
        buildTelemetry.GetProperties().ShouldBeEmpty();
 
        buildTelemetry.StartAt = null;
        buildTelemetry.FinishedAt = DateTime.MaxValue;
        buildTelemetry.GetProperties().ShouldBeEmpty();
 
        buildTelemetry.InnerStartAt = DateTime.MinValue;
        buildTelemetry.FinishedAt = null;
        buildTelemetry.GetProperties().ShouldBeEmpty();
 
        buildTelemetry.InnerStartAt = null;
        buildTelemetry.FinishedAt = DateTime.MaxValue;
        buildTelemetry.GetProperties().ShouldBeEmpty();
    }
 
    [Fact]
    public void BuildTelemetryIncludesFailureCategoryProperties()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
 
        buildTelemetry.BuildSuccess = false;
        buildTelemetry.FailureCategory = nameof(ErrorCategory.Compiler);
        buildTelemetry.ErrorCounts = new ErrorCountsInfo(
            Compiler: 5,
            MsBuildGeneral: 2,
            MsBuildEvaluation: null,
            MsBuildExecution: null,
            MsBuildGraph: null,
            Task: 1,
            SdkResolvers: null,
            NetSdk: null,
            NuGet: 3,
            BuildCheck: null,
            NativeToolchain: null,
            CodeAnalysis: null,
            Razor: null,
            Wpf: null,
            AspNet: null,
            Other: 1);
 
        var properties = buildTelemetry.GetProperties();
 
        properties["BuildSuccess"].ShouldBe("False");
        properties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Compiler));
        properties.ContainsKey("ErrorCounts").ShouldBeTrue();
 
        var activityProperties = buildTelemetry.GetActivityProperties();
        activityProperties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Compiler));
        var errorCounts = activityProperties["ErrorCounts"] as ErrorCountsInfo;
        errorCounts.ShouldNotBeNull();
        errorCounts.Compiler.ShouldBe(5);
        errorCounts.MsBuildGeneral.ShouldBe(2);
        errorCounts.Task.ShouldBe(1);
        errorCounts.NuGet.ShouldBe(3);
        errorCounts.Other.ShouldBe(1);
        errorCounts.SdkResolvers.ShouldBeNull();
        errorCounts.NetSdk.ShouldBeNull();
        errorCounts.BuildCheck.ShouldBeNull();
    }
 
    [Fact]
    public void BuildTelemetryActivityPropertiesIncludesFailureData()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
 
        buildTelemetry.BuildSuccess = false;
        buildTelemetry.FailureCategory = nameof(ErrorCategory.Tasks);
        buildTelemetry.ErrorCounts = new ErrorCountsInfo(
            Compiler: null,
            MsBuildGeneral: null,
            MsBuildEvaluation: null,
            MsBuildExecution: null,
            MsBuildGraph: null,
            Task: 10,
            SdkResolvers: null,
            NetSdk: null,
            NuGet: null,
            BuildCheck: null,
            NativeToolchain: null,
            CodeAnalysis: null,
            Razor: null,
            Wpf: null,
            AspNet: null,
            Other: null);
 
        var activityProperties = buildTelemetry.GetActivityProperties();
 
        activityProperties["BuildSuccess"].ShouldBe(false);
        activityProperties["FailureCategory"].ShouldBe(nameof(ErrorCategory.Tasks));
        var errorCounts = activityProperties["ErrorCounts"] as ErrorCountsInfo;
        errorCounts.ShouldNotBeNull();
        errorCounts.Task.ShouldBe(10);
    }
 
    [Fact]
    public void BuildTelemetryProjectPathEmitsOnlyFileName()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        buildTelemetry.ProjectPath = "C:/Users/useralias/repos/MyProject/MyProject.csproj";
        buildTelemetry.StartAt = DateTime.UtcNow;
        buildTelemetry.FinishedAt = DateTime.UtcNow;
 
        var properties = buildTelemetry.GetProperties();
 
        // Should only contain the file name, not the directory path
        properties["ProjectPath"].ShouldBe("MyProject.csproj");
        properties["ProjectPath"].ShouldNotContain("useralias");
        properties["ProjectPath"].ShouldNotContain("Users");
    }
 
    [Fact]
    public void BuildTelemetryBuildTargetHashesCustomTargets()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        buildTelemetry.BuildTarget = "MySecretCustomTarget";
        buildTelemetry.StartAt = DateTime.UtcNow;
        buildTelemetry.FinishedAt = DateTime.UtcNow;
 
        var properties = buildTelemetry.GetProperties();
 
        // Custom target name should be hashed
        properties["BuildTarget"].ShouldNotBe("MySecretCustomTarget");
        properties["BuildTarget"].ShouldBe(GetHashed("MySecretCustomTarget"));
    }
 
    [Fact]
    public void BuildTelemetryBuildTargetPreservesKnownTargets()
    {
        string[] knownTargets = { "Build", "Clean", "Rebuild", "Restore", "Pack", "Publish", "Test" };
 
        foreach (string target in knownTargets)
        {
            BuildTelemetry buildTelemetry = new BuildTelemetry();
            buildTelemetry.BuildTarget = target;
            buildTelemetry.StartAt = DateTime.UtcNow;
            buildTelemetry.FinishedAt = DateTime.UtcNow;
 
            var properties = buildTelemetry.GetProperties();
            properties["BuildTarget"].ShouldBe(target, $"Known target '{target}' should not be hashed");
        }
    }
 
    [Fact]
    public void BuildTelemetryActivityPropertiesHashCustomTarget()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        buildTelemetry.BuildTarget = "InternalCustomTarget";
 
        var activityProperties = buildTelemetry.GetActivityProperties();
 
        activityProperties["BuildTarget"].ShouldBe(GetHashed("InternalCustomTarget"));
    }
 
    [Fact]
    public void BuildTelemetryBuildTargetHandlesCommaSeparatedTargets()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        buildTelemetry.BuildTarget = "Build,Clean";
        buildTelemetry.StartAt = DateTime.UtcNow;
        buildTelemetry.FinishedAt = DateTime.UtcNow;
 
        var properties = buildTelemetry.GetProperties();
 
        // Both known targets should be preserved individually, not hashed as a whole string
        properties["BuildTarget"].ShouldBe("Build,Clean");
    }
 
    [Fact]
    public void BuildTelemetryBuildTargetHashesMixedTargets()
    {
        BuildTelemetry buildTelemetry = new BuildTelemetry();
        buildTelemetry.BuildTarget = "Build,MyCustomTarget,Restore";
        buildTelemetry.StartAt = DateTime.UtcNow;
        buildTelemetry.FinishedAt = DateTime.UtcNow;
 
        var properties = buildTelemetry.GetProperties();
 
        // Known targets preserved, custom target hashed
        properties["BuildTarget"].ShouldBe($"Build,{GetHashed("MyCustomTarget")},Restore");
    }
}