File: ComponentsMetricsTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.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.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.InternalTesting;
using Moq;
 
namespace Microsoft.AspNetCore.Components;
 
public class ComponentsMetricsTest
{
    private readonly TestMeterFactory _meterFactory;
 
    public ComponentsMetricsTest()
    {
        _meterFactory = new TestMeterFactory();
    }
 
    [Fact]
    public void Constructor_CreatesMetersCorrectly()
    {
        // Arrange & Act
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Assert
        Assert.Equal(2, _meterFactory.Meters.Count);
        Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.MeterName);
        Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.LifecycleMeterName);
    }
 
    [Fact]
    public void Navigation_RecordsMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var navigationCounter = new MetricCollector<long>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
 
        // Act
        componentsMetrics.Navigation("TestComponent", "/test-route");
 
        // Assert
        var measurements = navigationCounter.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.Equal(1, measurements[0].Value);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("/test-route", Assert.Contains("aspnetcore.components.route", measurements[0].Tags));
    }
 
    [Fact]
    public void IsNavigationEnabled_ReturnsCorrectState()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Create a collector to ensure the meter is enabled
        using var navigationCounter = new MetricCollector<long>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
 
        // Act & Assert
        Assert.True(componentsMetrics.IsNavigationEnabled);
    }
 
    [Fact]
    public async Task CaptureEventDuration_RecordsSuccessMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp,
            "TestComponent", "OnClick", "onclick");
 
        // Assert
        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
        Assert.DoesNotContain("error.type", measurements[0].Tags);
    }
 
    [Fact]
    public async Task CaptureEventDuration_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureEventDuration(Task.FromException(new InvalidOperationException()),
            startTimestamp, "TestComponent", "OnClick", "onclick");
 
        // Assert
        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void FailEventSync_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
        var exception = new InvalidOperationException();
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        componentsMetrics.FailEventSync(exception, startTimestamp,
            "TestComponent", "OnClick", "onclick");
 
        // Assert
        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void IsEventEnabled_ReturnsCorrectState()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Create a collector to ensure the meter is enabled
        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
 
        // Act & Assert
        Assert.True(componentsMetrics.IsEventEnabled);
    }
 
    [Fact]
    public async Task CaptureParametersDuration_RecordsSuccessMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureParametersDuration(Task.CompletedTask, startTimestamp, "TestComponent");
 
        // Assert
        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.DoesNotContain("error.type", measurements[0].Tags);
    }
 
    [Fact]
    public async Task CaptureParametersDuration_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureParametersDuration(Task.FromException(new InvalidOperationException()),
            startTimestamp, "TestComponent");
 
        // Assert
        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void FailParametersSync_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
        var exception = new InvalidOperationException();
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        componentsMetrics.FailParametersSync(exception, startTimestamp, "TestComponent");
 
        // Assert
        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void IsParametersEnabled_ReturnsCorrectState()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Create a collector to ensure the meter is enabled
        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
 
        // Act & Assert
        Assert.True(componentsMetrics.IsParametersEnabled);
    }
 
    [Fact]
    public async Task CaptureBatchDuration_RecordsSuccessMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureBatchDuration(Task.CompletedTask, startTimestamp, 25);
 
        // Assert
        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
        Assert.DoesNotContain("error.type", measurements[0].Tags);
    }
 
    [Fact]
    public async Task CaptureBatchDuration_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        await Task.Delay(10); // Small delay to ensure measureable duration
        await componentsMetrics.CaptureBatchDuration(Task.FromException(new InvalidOperationException()),
            startTimestamp, 25);
 
        // Assert
        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void FailBatchSync_RecordsErrorMetric()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
        var exception = new InvalidOperationException();
 
        // Act
        var startTimestamp = Stopwatch.GetTimestamp();
        componentsMetrics.FailBatchSync(exception, startTimestamp);
 
        // Assert
        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(measurements);
        Assert.True(measurements[0].Value > 0);
        Assert.Equal(0, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
    }
 
    [Fact]
    public void IsBatchEnabled_ReturnsCorrectState()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Create a collector to ensure the meter is enabled
        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
 
        // Act & Assert
        Assert.True(componentsMetrics.IsBatchEnabled);
    }
 
    [Fact]
    public async Task ComponentLifecycle_RecordsAllMetricsCorrectly()
    {
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
        using var navigationCounter = new MetricCollector<long>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
 
        // Act - Simulate a component lifecycle
        // 1. Navigation
        componentsMetrics.Navigation("TestComponent", "/test-route");
 
        // 2. Parameters update
        var startTimestamp1 = Stopwatch.GetTimestamp();
        await Task.Delay(10);
        await componentsMetrics.CaptureParametersDuration(Task.CompletedTask, startTimestamp1, "TestComponent");
 
        // 3. Event handler
        var startTimestamp2 = Stopwatch.GetTimestamp();
        await Task.Delay(10);
        await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2,
            "TestComponent", "OnClick", "onclick");
 
        // 4. Rendering batch
        var startTimestamp3 = Stopwatch.GetTimestamp();
        await Task.Delay(10);
        await componentsMetrics.CaptureBatchDuration(Task.CompletedTask, startTimestamp3, 15);
 
        // Assert
        var navigationMeasurements = navigationCounter.GetMeasurementSnapshot();
        var eventMeasurements = eventDurationHistogram.GetMeasurementSnapshot();
        var parametersMeasurements = parametersDurationHistogram.GetMeasurementSnapshot();
        var batchMeasurements = batchDurationHistogram.GetMeasurementSnapshot();
 
        Assert.Single(navigationMeasurements);
        Assert.Single(eventMeasurements);
        Assert.Single(parametersMeasurements);
        Assert.Single(batchMeasurements);
 
        // Check navigation
        Assert.Equal(1, navigationMeasurements[0].Value);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", navigationMeasurements[0].Tags));
        Assert.Equal("/test-route", Assert.Contains("aspnetcore.components.route", navigationMeasurements[0].Tags));
 
        // Check event duration
        Assert.True(eventMeasurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", eventMeasurements[0].Tags));
        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", eventMeasurements[0].Tags));
        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", eventMeasurements[0].Tags));
 
        // Check parameters duration
        Assert.True(parametersMeasurements[0].Value > 0);
        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", parametersMeasurements[0].Tags));
 
        // Check batch duration
        Assert.True(batchMeasurements[0].Value > 0);
        Assert.Equal(20, Assert.Contains("aspnetcore.components.diff.length", batchMeasurements[0].Tags));
    }
 
    [Fact]
    public void Dispose_DisposesAllMeters()
    {
        // This test verifies that disposing ComponentsMetrics properly disposes its meters
        // Since we can't easily test disposal directly, we'll verify meters are created and assume
        // the dispose method works as expected
 
        // Arrange
        var componentsMetrics = new ComponentsMetrics(_meterFactory);
 
        // Act - We're not actually asserting anything here, just ensuring no exceptions are thrown
        componentsMetrics.Dispose();
 
        // Assert - MeterFactory.Create was called twice in constructor
        Assert.Equal(2, _meterFactory.Meters.Count);
    }
}