File: Telemetry\TelemetryReporterTests.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.VisualStudio.LanguageServices.Razor.UnitTests\Microsoft.VisualStudio.LanguageServices.Razor.UnitTests.csproj (Microsoft.VisualStudio.LanguageServices.Razor.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.Razor.Telemetry;
using Microsoft.VisualStudio.Editor.Razor.Test.Shared;
using Microsoft.VisualStudio.Telemetry;
using Microsoft.VisualStudio.Telemetry.Metrics;
using StreamJsonRpc;
using Xunit;
using Xunit.Abstractions;
 
namespace Microsoft.VisualStudio.Razor.Telemetry;
 
public class TelemetryReporterTests(ITestOutputHelper testOutput) : ToolingTestBase(testOutput)
{
    [Fact]
    public void NoArgument()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        reporter.ReportEvent("EventName", Severity.Normal);
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.False(actualEvent.HasProperties);
    }
 
    [Fact]
    public void OneArgument()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        reporter.ReportEvent("EventName", Severity.Normal, new Property("P1", false));
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
        Assert.Single(actualEvent.Properties);
 
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
    }
 
    [Fact]
    public void TwoArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        reporter.ReportEvent("EventName", Severity.Normal, new("P1", false), new("P2", "test"));
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
    }
 
    [Fact]
    public void ThreeArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var p3Value = Guid.NewGuid();
        reporter.ReportEvent("EventName",
            Severity.Normal,
            new("P1", false),
            new("P2", "test"),
            new("P3", p3Value));
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
 
        var p3 = Assert.IsType<TelemetryComplexProperty>(actualEvent.Properties["dotnet.razor.p3"]);
        Assert.Equal(p3Value, p3.Value);
    }
 
    [Fact]
    public void FourArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var p3Value = Guid.NewGuid();
        reporter.ReportEvent("EventName",
            Severity.Normal,
            new("P1", false),
            new("P2", "test"),
            new("P3", p3Value),
            new("P4", 100));
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
 
        var p3 = Assert.IsType<TelemetryComplexProperty>(actualEvent.Properties["dotnet.razor.p3"]);
        Assert.Equal(p3Value, p3.Value);
 
        Assert.Equal(100, actualEvent.Properties["dotnet.razor.p4"]);
    }
 
    [Fact]
    public void Block_NoArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        using (var scope = reporter.BeginBlock("EventName", Severity.Normal))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
    }
 
    [Fact]
    public void Block_OneArgument()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        using (reporter.BeginBlock("EventName", Severity.Normal, new Property("P1", false)))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
    }
 
    [Fact]
    public void Block_TwoArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        using (reporter.BeginBlock("EventName", Severity.Normal, TimeSpan.Zero, new("P1", false), new("P2", "test")))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
    }
 
    [Fact]
    public void Block_ThreeArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var p3Value = Guid.NewGuid();
        using (reporter.BeginBlock("EventName",
            Severity.Normal,
            TimeSpan.Zero,
            new("P1", false),
            new("P2", "test"),
            new("P3", p3Value)))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
 
        var p3 = actualEvent.Properties["dotnet.razor.p3"] as TelemetryComplexProperty;
        Assert.NotNull(p3);
        Assert.Equal(p3Value, p3.Value);
    }
 
    [Fact]
    public void Block_FourArguments()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var p3Value = Guid.NewGuid();
        using (reporter.BeginBlock("EventName",
            Severity.Normal,
            TimeSpan.Zero,
            new("P1", false),
            new("P2", "test"),
            new("P3", p3Value),
            new("P4", 100)))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/eventname", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
        Assert.Equal(false, actualEvent.Properties["dotnet.razor.p1"]);
        Assert.Equal("test", actualEvent.Properties["dotnet.razor.p2"]);
 
        var p3 = actualEvent.Properties["dotnet.razor.p3"] as TelemetryComplexProperty;
        Assert.NotNull(p3);
        Assert.Equal(p3Value, p3.Value);
 
        Assert.Equal(100, actualEvent.Properties["dotnet.razor.p4"]);
    }
 
    [Fact]
    public void HandleRIEWithInnerException()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
 
        var ae = new ApplicationException("expectedText");
        var rie = new RemoteInvocationException("a", 0, ae);
 
        reporter.ReportFault(rie, rie.Message);
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.High, actualEvent.Severity);
        Assert.Equal("dotnet/razor/fault", actualEvent.Name);
        // faultEvent doesn't expose any interesting properties,
        // like the ExceptionObject, or the resulting Description,
        // or really anything we would explicitly want to verify against.
        Assert.IsType<FaultEvent>(actualEvent);
    }
 
    [Fact]
    public void HandleRIEWithNoInnerException()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
 
        var rie = new RemoteInvocationException("a", 0, errorData: null);
 
        reporter.ReportFault(rie, rie.Message);
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.High, actualEvent.Severity);
        Assert.Equal("dotnet/razor/fault", actualEvent.Name);
        // faultEvent doesn't expose any interesting properties,
        // like the ExceptionObject, or the resulting Description,
        // or really anything we would explicitly want to verify against.
        Assert.IsType<FaultEvent>(actualEvent);
    }
 
    [Fact]
    public void TrackLspRequest()
    {
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var correlationId = Guid.NewGuid();
        using (reporter.TrackLspRequest("MethodName", "ServerName", TimeSpan.Zero, correlationId))
        {
        }
 
        var actualEvent = Assert.Single(reporter.Events);
        Assert.Equal(TelemetrySeverity.Normal, actualEvent.Severity);
        Assert.Equal("dotnet/razor/tracklsprequest", actualEvent.Name);
        Assert.True(actualEvent.HasProperties);
 
        Assert.IsType<long>(actualEvent.Properties["dotnet.razor.eventscope.ellapsedms"]);
        Assert.Equal("MethodName", actualEvent.Properties["dotnet.razor.eventscope.method"]);
        Assert.Equal("ServerName", actualEvent.Properties["dotnet.razor.eventscope.languageservername"]);
 
        var correlationProperty = actualEvent.Properties["dotnet.razor.eventscope.correlationid"] as TelemetryComplexProperty;
        Assert.NotNull(correlationProperty);
        Assert.Equal(correlationId, correlationProperty.Value);
    }
 
    [Fact]
    public void ReportFault_OperationCanceledExceptionWithoutInnerException_SkipsFaultReport()
    {
        // Arrange
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var exception = new OperationCanceledException("OCE", innerException: null);
 
        // Act
        reporter.ReportFault(exception, "Test message");
 
        // Assert
        Assert.Empty(reporter.Events);
    }
 
    [Fact]
    public void ReportFault_TaskCanceledExceptionWithoutInnerException_SkipsFaultReport()
    {
        // Arrange
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var exception = new TaskCanceledException("TCE", innerException: null);
 
        // Act
        reporter.ReportFault(exception, "Test message");
 
        // Assert
        Assert.Empty(reporter.Events);
    }
 
    [Fact]
    public void ReportFault_InnerExceptionOfOCEIsNotAnOCE_ReportsFault()
    {
        // Arrange
        var depth = 3;
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var innerMostException = new Exception();
        var exception = new OperationCanceledException("Test", innerMostException);
        for (var i = 0; i < depth; i++)
        {
            exception = new OperationCanceledException("Test", exception);
        }
 
        // Act
        reporter.ReportFault(exception, "Test message");
 
        // Assert
        Assert.NotEmpty(reporter.Events);
    }
 
    [Fact]
    public void ReportFault_InnerMostExceptionIsOperationCanceledException_SkipsFaultReport()
    {
        // Arrange
        var depth = 3;
        using var reporter = new TestTelemetryReporter(LoggerFactory);
        var innerMostException = new OperationCanceledException();
        var exception = new OperationCanceledException("Test", innerMostException);
        for (var i = 0; i < depth; i++)
        {
            exception = new OperationCanceledException("Test", exception);
        }
 
        // Act
        reporter.ReportFault(exception, "Test message");
 
        // Assert
        Assert.Empty(reporter.Events);
    }
 
    [Fact]
    public void ReportHistogram()
    {
        // Arrange
        var reporter = new TestTelemetryReporter(LoggerFactory);
 
        // Act
        reporter.ReportRequestTiming(
            Methods.TextDocumentCodeActionName,
            WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(100),
            TimeSpan.FromMilliseconds(100),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Succeeded);
 
        reporter.ReportRequestTiming(
            Methods.TextDocumentCodeActionName,
            WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(200),
            TimeSpan.FromMilliseconds(200),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Cancelled);
 
        reporter.ReportRequestTiming(
            Methods.TextDocumentCodeActionName,
            WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(300),
            TimeSpan.FromMilliseconds(300),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Failed);
 
        reporter.ReportRequestTiming(
            Methods.TextDocumentCompletionName,
             WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(100),
            TimeSpan.FromMilliseconds(100),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Succeeded);
 
        reporter.ReportRequestTiming(
            Methods.TextDocumentCompletionName,
             WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(200),
            TimeSpan.FromMilliseconds(200),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Cancelled);
 
        reporter.ReportRequestTiming(
            Methods.TextDocumentCompletionName,
             WellKnownLspServerKinds.RazorLspServer.GetContractName(),
            TimeSpan.FromMilliseconds(300),
            TimeSpan.FromMilliseconds(300),
            CodeAnalysis.Razor.Telemetry.TelemetryResult.Failed);
 
        reporter.Dispose();
 
        // Assert
        reporter.AssertMetrics(
            static evt =>
            {
                var histogram = Assert.IsAssignableFrom<IHistogram<long>>(evt.Instrument);
                Assert.Equal("TimeInQueue", histogram.Name);
 
                var telemetryEvent = evt.Event;
                Assert.Equal("dotnet/razor/lsp_timeinqueue", telemetryEvent.Name);
 
                var prop = Assert.Single(telemetryEvent.Properties);
                Assert.Equal("dotnet.razor.method", prop.Key);
                Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value);
            },
            static evt =>
            {
                var histogram = Assert.IsAssignableFrom<IHistogram<long>>(evt.Instrument);
                Assert.Equal(Methods.TextDocumentCodeActionName, histogram.Name);
 
                var telemetryEvent = evt.Event;
                Assert.Equal("dotnet/razor/lsp_requestduration", telemetryEvent.Name);
 
                var prop = Assert.Single(telemetryEvent.Properties);
                Assert.Equal("dotnet.razor.method", prop.Key);
                Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value);
            },
            static evt =>
            {
                var histogram = Assert.IsAssignableFrom<IHistogram<long>>(evt.Instrument);
                Assert.Equal(Methods.TextDocumentCompletionName, histogram.Name);
 
                var telemetryEvent = evt.Event;
                Assert.Equal("dotnet/razor/lsp_requestduration", telemetryEvent.Name);
 
                var prop = Assert.Single(telemetryEvent.Properties);
                Assert.Equal("dotnet.razor.method", prop.Key);
                Assert.Equal(Methods.TextDocumentCompletionName, prop.Value);
            });
 
        Assert.Collection(reporter.Events,
            static evt =>
            {
                Assert.Equal("dotnet/razor/lsp_requestcounter", evt.Name);
                Assert.Collection(evt.Properties,
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.method", prop.Key);
                        Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.successful", prop.Key);
                        Assert.Equal(1, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.failed", prop.Key);
                        Assert.Equal(1, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.cancelled", prop.Key);
                        Assert.Equal(1, prop.Value);
                    });
            },
            static evt =>
            {
                Assert.Equal("dotnet/razor/lsp_requestcounter", evt.Name);
                Assert.Collection(evt.Properties,
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.method", prop.Key);
                        Assert.Equal(Methods.TextDocumentCompletionName, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.successful", prop.Key);
                        Assert.Equal(1, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.failed", prop.Key);
                        Assert.Equal(1, prop.Value);
                    },
                    static prop =>
                    {
                        Assert.Equal("dotnet.razor.cancelled", prop.Key);
                        Assert.Equal(1, prop.Value);
                    });
            });
    }
 
    [Fact]
    public void GetModifiedFaultParameters_FiltersCorrectly()
    {
        // The expected module name should be the primary module of this test assembly.
        // The expected method name should be AssumeNotNullFailure() below.
        var currentModule = Assert.Single(Assembly.GetExecutingAssembly().Modules);
        var expectedModuleName = Path.GetFileNameWithoutExtension(currentModule.Name);
        var expectedMethodName = nameof(AssumeNotNullFailure);
 
        var exception = Assert.Throws<InvalidOperationException>(AssumeNotNullFailure);
 
        var (moduleName, methodName) = TestTelemetryReporter.GetModifiedFaultParameters(exception);
 
        Assert.Equal(expectedModuleName, moduleName);
        Assert.Equal(expectedMethodName, methodName);
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void AssumeNotNullFailure()
    {
        ((object?)null).AssumeNotNull();
    }
}