// 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 Aspire.Cli.Acquisition;
using Aspire.Cli.Commands;
using Aspire.Cli.Interaction;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Tests.TestServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
namespace Aspire.Cli.Tests.Telemetry;
public class TelemetryConfigurationTests
{
// The Aspire.Cli.Tests assembly opts out of Azure Monitor telemetry by default
// (see TestTelemetryDefaults). Tests that need the Azure Monitor branch override
// the env-var-derived opt-out by passing this in their in-memory configuration —
// AddInMemoryCollection is added AFTER AddEnvironmentVariables in
// Program.BuildApplicationAsync, so the in-memory value wins.
private static readonly KeyValuePair<string, string?> s_telemetryOptInOverride =
new(AspireCliTelemetry.TelemetryOptOutConfigKey, "false");
private static Dictionary<string, string?> WithTelemetryOptIn(Dictionary<string, string?>? config = null)
{
var result = config is null
? new Dictionary<string, string?>()
: new Dictionary<string, string?>(config);
result[s_telemetryOptInOverride.Key] = s_telemetryOptInOverride.Value;
return result;
}
private static async Task<IHost> BuildHostAsync(Dictionary<string, string?>? config = null)
{
var loggingOptions = Program.ParseLoggingOptions([]);
var errorWriter = new TestStartupErrorWriter();
var logBufferContext = new ConsoleLogBufferContext();
var (loggerFactory, fileLoggerProvider) = Program.CreateLoggerFactory([], loggingOptions, errorWriter, logBufferContext);
var identityChannelReader = new IdentityChannelReader(typeof(Program).Assembly);
var startupContext = new Program.CliStartupContext(loggingOptions, errorWriter, loggerFactory, fileLoggerProvider, logBufferContext, loggerFactory.CreateLogger(Program.RootLoggerName), new ConsoleCancellationManager(finalDrainBudget: Timeout.InfiniteTimeSpan), identityChannelReader);
return await Program.BuildApplicationAsync([], startupContext, config);
}
[Fact]
public async Task AzureMonitor_Enabled_ByDefault()
{
// The Application Insights connection string is hardcoded, so Azure Monitor
// should be enabled when telemetry is not opted out. The test process opts out
// by default (see TestTelemetryDefaults); we explicitly opt back in here to
// exercise the Azure Monitor branch.
using var host = await BuildHostAsync(WithTelemetryOptIn());
var telemetryManager = host.Services.GetService<TelemetryManager>();
Assert.NotNull(telemetryManager);
Assert.True(telemetryManager.HasAzureMonitor, "Expected TelemetryManager to have Azure Monitor enabled by default");
}
[Theory]
[InlineData("1")]
[InlineData("true")]
public async Task AzureMonitor_Disabled_WhenOptOutSetToTrueValues(string optOutValue)
{
var config = new Dictionary<string, string?>
{
[AspireCliTelemetry.TelemetryOptOutConfigKey] = optOutValue
};
using var host = await BuildHostAsync(config);
var telemetryManager = host.Services.GetRequiredService<TelemetryManager>();
// When telemetry is opted out, Azure Monitor should not be enabled
Assert.False(telemetryManager.HasAzureMonitor, $"Expected Azure Monitor to be disabled when telemetry opt-out is '{optOutValue}'");
}
[Theory]
[InlineData("--version")]
[InlineData("-v")]
public async Task ReportedTelemetry_Disabled_WhenVersionFlagProvided(string versionFlag)
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration, [versionFlag]);
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
using var telemetryManager = new TelemetryManager(telemetryConfiguration, tagsSource);
var internalMicrosoftDetector = new TelemetryFixture.TestInternalMicrosoftDetector
{
IsInternalMicrosoft = true
};
var telemetry = new AspireCliTelemetry(
NullLogger<AspireCliTelemetry>.Instance,
new TelemetryFixture.TestMachineInformationProvider(),
new TelemetryFixture.TestCIEnvironmentDetector(),
new TelemetryFixture.TestCodingAgentDetector(),
internalMicrosoftDetector,
telemetryConfiguration,
AspireCliTelemetry.ReportedActivitySourceName,
AspireCliTelemetry.DiagnosticsActivitySourceName,
Utils.TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(AppContext.BaseDirectory)),
tagsSource);
telemetry.Initialize();
await tagsSource.TagsTask;
Assert.False(telemetryManager.HasAzureMonitor);
Assert.Equal(0, internalMicrosoftDetector.InvocationCount);
Assert.Empty(GetInternalMicrosoftTags(await telemetry.GetDefaultTagsAsync()));
}
[Theory]
[InlineData("--help")]
[InlineData("-h")]
[InlineData("-?")]
[InlineData("/h")]
[InlineData("/?")]
public void ReportedTelemetry_Disabled_WhenHelpFlagProvided(string helpFlag)
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration, [helpFlag]);
Assert.False(telemetryConfiguration.ReportedTelemetryEnabled);
}
[Theory]
[InlineData("terminal", "ps", "-v")]
[InlineData("run", "--", "-v")]
[InlineData("add", "docker", "--version", "9.2.0")]
public void ReportedTelemetry_RemainsEnabled_WhenVersionLikeOptionIsNotRootInformational(params string[] args)
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration, args);
Assert.True(telemetryConfiguration.ReportedTelemetryEnabled);
}
[Fact]
public void ReportedTelemetry_Disabled_WhenShortVersionFollowsRootOptionValue()
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration, ["--log-level", "Debug", "-v"]);
Assert.False(telemetryConfiguration.ReportedTelemetryEnabled);
}
[Theory]
[InlineData("--log-level", "--help")]
[InlineData("-l", "-h")]
[InlineData("--capture-profile-output", "--version")]
[InlineData("--capture-profile-delay", "-v")]
[InlineData("--log-file", "-?")]
public void ReportedTelemetry_Disabled_WhenInformationalFlagIsConsumedAsMissingRootOptionValue(string rootOption, string informationalFlag)
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration, [rootOption, informationalFlag]);
Assert.False(telemetryConfiguration.ReportedTelemetryEnabled);
}
[Fact]
public void DetectorDiagnostics_Disabled_ForAgentTelemetryInvocation()
{
var configuration = new ConfigurationBuilder().Build();
var telemetryConfiguration = TelemetryConfiguration.Create(
configuration,
["agent", "telemetry", "--event-type", "skill_invocation"]);
Assert.True(telemetryConfiguration.ReportedTelemetryEnabled);
Assert.False(telemetryConfiguration.EmitInternalMicrosoftDiagnostics);
Assert.Equal(TimeSpan.FromSeconds(5), telemetryConfiguration.InternalMicrosoftDetectionTimeout);
}
[Fact]
public async Task ReportedTelemetry_Disabled_WhenOptOutSet_DoesNotRunInternalMicrosoftDetector()
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[AspireCliTelemetry.TelemetryOptOutConfigKey] = "true"
})
.Build();
var telemetryConfiguration = TelemetryConfiguration.Create(configuration);
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
using var telemetryManager = new TelemetryManager(telemetryConfiguration, tagsSource);
var internalMicrosoftDetector = new TelemetryFixture.TestInternalMicrosoftDetector
{
IsInternalMicrosoft = true
};
var telemetry = new AspireCliTelemetry(
NullLogger<AspireCliTelemetry>.Instance,
new TelemetryFixture.TestMachineInformationProvider(),
new TelemetryFixture.TestCIEnvironmentDetector(),
new TelemetryFixture.TestCodingAgentDetector(),
internalMicrosoftDetector,
telemetryConfiguration,
AspireCliTelemetry.ReportedActivitySourceName,
AspireCliTelemetry.DiagnosticsActivitySourceName,
Utils.TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(AppContext.BaseDirectory)),
tagsSource);
telemetry.Initialize();
await tagsSource.TagsTask;
Assert.False(telemetryManager.HasAzureMonitor);
Assert.Equal(0, internalMicrosoftDetector.InvocationCount);
Assert.Empty(GetInternalMicrosoftTags(await telemetry.GetDefaultTagsAsync()));
}
[Fact]
public async Task OtlpExporter_WithoutProfiling_EnablesOnlyDebugDiagnostics_WhenEndpointProvided()
{
var config = WithTelemetryOptIn(new Dictionary<string, string?>
{
[AspireCliTelemetry.OtlpExporterEndpointConfigKey] = "http://localhost:4317"
});
using var host = await BuildHostAsync(config);
var telemetryManager = host.Services.GetRequiredService<TelemetryManager>();
#if DEBUG
Assert.True(telemetryManager.HasDiagnosticProvider, "Expected TelemetryManager to have diagnostic provider enabled when OTLP endpoint is configured in DEBUG mode");
Assert.False(telemetryManager.HasProfilingProvider, "Expected profiling export to require explicit profiling opt-in");
// Azure Monitor is also enabled since connection string is hardcoded
Assert.True(telemetryManager.HasAzureMonitor, "Expected TelemetryManager to have Azure Monitor enabled (connection string is hardcoded)");
#else
// In RELEASE mode, diagnostic OTLP export requires explicit profiling opt-in.
Assert.False(telemetryManager.HasDiagnosticProvider, "Expected TelemetryManager to require profiling before enabling diagnostic OTLP export in RELEASE mode");
Assert.False(telemetryManager.HasProfilingProvider, "Expected profiling export to require explicit profiling opt-in");
Assert.True(telemetryManager.HasAzureMonitor, "Expected Azure Monitor to be enabled (connection string is hardcoded)");
#endif
}
[Fact]
public void OtlpExporter_WithDetachedNonProfilingContext_DoesNotEnableProfilingProvider()
{
using var listener = CreateActivityListener("test-detached-non-profiling-context");
using var source = new ActivitySource("test-detached-non-profiling-context");
using var activity = source.StartActivity("parent");
Assert.NotNull(activity);
var config = AppHostLauncher.CreateDetachedChildEnvironment(activity, appHostSelectionOrigin: null);
config[AspireCliTelemetry.OtlpExporterEndpointConfigKey] = "http://localhost:4317";
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(config.Select(pair => new KeyValuePair<string, string?>(pair.Key, pair.Value)))
.Build();
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
using var manager = new TelemetryManager(configuration, tagsSource);
Assert.False(manager.HasProfilingProvider, "Expected detached child profiling export to require an actual profiling session");
}
[Fact]
public async Task OtlpExporter_WithProfiling_EnablesProfilingProviderWhenTelemetryOptedOut()
{
var config = new Dictionary<string, string?>
{
[AspireCliTelemetry.TelemetryOptOutConfigKey] = "true",
[AspireCliTelemetry.OtlpExporterEndpointConfigKey] = "http://localhost:4317",
[Aspire.Hosting.KnownConfigNames.ProfilingEnabled] = "true"
};
using var host = await BuildHostAsync(config);
var telemetryManager = host.Services.GetRequiredService<TelemetryManager>();
Assert.False(telemetryManager.HasAzureMonitor, "Expected Azure Monitor to honor telemetry opt-out");
Assert.False(telemetryManager.HasDiagnosticProvider, "Expected profiling export to stay separate from debug diagnostics");
Assert.True(telemetryManager.HasProfilingProvider, "Expected profiling OTLP export to work even when reported telemetry is opted out");
}
[Fact]
public async Task OtlpExporter_WithProfiling_KeepsReportedTelemetryAndProfilingSeparate()
{
var config = WithTelemetryOptIn(new Dictionary<string, string?>
{
[AspireCliTelemetry.OtlpExporterEndpointConfigKey] = "http://localhost:4317",
[Aspire.Hosting.KnownConfigNames.ProfilingEnabled] = "true"
});
using var host = await BuildHostAsync(config);
var telemetryManager = host.Services.GetRequiredService<TelemetryManager>();
Assert.True(telemetryManager.HasAzureMonitor, "Expected reported telemetry to keep using the Azure Monitor provider");
Assert.True(telemetryManager.HasProfilingProvider, "Expected profiling telemetry to use the profiling provider");
Assert.False(telemetryManager.HasDiagnosticProvider, "Expected profiling OTLP export to avoid the debug diagnostics provider");
}
#if DEBUG
[Fact]
public async Task DiagnosticProvider_IncludesDiagnosticActivitySource()
{
// Configure console exporter at Diagnostic level to enable the diagnostic provider
var config = new Dictionary<string, string?>
{
[AspireCliTelemetry.TelemetryOptOutConfigKey] = "true",
[AspireCliTelemetry.ConsoleExporterLevelConfigKey] = "Diagnostic"
};
using var host = await BuildHostAsync(config);
var telemetryManager = host.Services.GetRequiredService<TelemetryManager>();
Assert.False(telemetryManager.HasAzureMonitor);
Assert.True(telemetryManager.HasDiagnosticProvider);
Assert.False(telemetryManager.HasProfilingProvider);
var telemetry = host.Services.GetRequiredService<AspireCliTelemetry>();
telemetry.Initialize();
var tagsSource = host.Services.GetRequiredService<TelemetryTagsSource>();
await tagsSource.TagsTask;
using var diagnosticActivity = telemetry.StartDiagnosticActivity("TestDiagnosticActivity");
Assert.NotNull(diagnosticActivity);
}
#endif
[Fact]
public void AzureMonitor_Disabled_WhenVersionFlagProvided()
{
var configuration = new ConfigurationBuilder().Build();
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
var manager = new TelemetryManager(configuration, tagsSource, ["--version"]);
Assert.False(manager.HasAzureMonitor);
}
[Theory]
[InlineData("--help")]
[InlineData("-h")]
[InlineData("-?")]
public void AzureMonitor_Disabled_ForAllHelpFlags(string flag)
{
var configuration = new ConfigurationBuilder().Build();
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
var manager = new TelemetryManager(configuration, tagsSource, [flag]);
Assert.False(manager.HasAzureMonitor);
}
[Theory]
[InlineData("1")]
[InlineData("true")]
public void AzureMonitor_Disabled_ForAgentTelemetry_WhenGlobalOptOutSet(string optOutValue)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[AspireCliTelemetry.TelemetryOptOutConfigKey] = optOutValue
})
.Build();
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
using var manager = new TelemetryManager(configuration, tagsSource, ["agent", "telemetry", "--event-type", "skill_invocation"]);
Assert.False(manager.HasAzureMonitor);
}
[Fact]
public void AzureMonitor_Enabled_ForAgentTelemetry_WhenNoOptOutSet()
{
var configuration = new ConfigurationBuilder().Build();
var tagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance);
using var manager = new TelemetryManager(configuration, tagsSource, ["agent", "telemetry", "--event-type", "skill_invocation"]);
Assert.True(manager.HasAzureMonitor);
}
private static ActivityListener CreateActivityListener(string sourceName)
{
var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == sourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded
};
ActivitySource.AddActivityListener(listener);
return listener;
}
private static IReadOnlyList<KeyValuePair<string, object?>> GetInternalMicrosoftTags(IReadOnlyList<KeyValuePair<string, object?>> tags)
{
return [.. tags.Where(t => t.Key == TelemetryConstants.Tags.InternalMicrosoft ||
t.Key.StartsWith("aspire.cli.microsoft_internal", StringComparison.Ordinal))];
}
}