From 09091caa55472706b1a14e865b7e1aa267c370ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:50:58 +0000 Subject: [PATCH 1/7] Initial plan From 57b78378821b143021e67e294488da8efdc66c89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:08:34 +0000 Subject: [PATCH 2/7] Add LLM environment detection telemetry with CLAUDECODE support Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../Telemetry/ILLMEnvironmentDetector.cs | 11 ++++++ .../LLMEnvironmentDetectorForTelemetry.cs | 28 +++++++++++++++ .../Telemetry/TelemetryCommonProperties.cs | 6 +++- .../TelemetryCommonPropertiesTests.cs | 36 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs create mode 100644 src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs diff --git a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs new file mode 100644 index 000000000000..e3f9a31937fc --- /dev/null +++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.DotNet.Cli.Telemetry; + +internal interface ILLMEnvironmentDetector +{ + bool IsLLMEnvironment(); +} \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs new file mode 100644 index 000000000000..87039a07687e --- /dev/null +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.DotNet.Cli.Telemetry; + +internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector +{ + // Systems where the variable must be present and not-null + private static readonly string[] _ifNonNullVariables = [ + // Claude Code + "CLAUDECODE" + ]; + + public bool IsLLMEnvironment() + { + foreach (var variable in _ifNonNullVariables) + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index 9f58d488a12c..4eb9b44cac21 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -18,10 +18,12 @@ internal class TelemetryCommonProperties( Func getDeviceId = null, IDockerContainerDetector dockerContainerDetector = null, IUserLevelCacheWriter userLevelCacheWriter = null, - ICIEnvironmentDetector ciEnvironmentDetector = null) + ICIEnvironmentDetector ciEnvironmentDetector = null, + ILLMEnvironmentDetector llmEnvironmentDetector = null) { private readonly IDockerContainerDetector _dockerContainerDetector = dockerContainerDetector ?? new DockerContainerDetectorForTelemetry(); private readonly ICIEnvironmentDetector _ciEnvironmentDetector = ciEnvironmentDetector ?? new CIEnvironmentDetectorForTelemetry(); + private readonly ILLMEnvironmentDetector _llmEnvironmentDetector = llmEnvironmentDetector ?? new LLMEnvironmentDetectorForTelemetry(); private readonly Func _getCurrentDirectory = getCurrentDirectory ?? Directory.GetCurrentDirectory; private readonly Func _hasher = hasher ?? Sha256Hasher.Hash; private readonly Func _getMACAddress = getMACAddress ?? MacAddressGetter.GetMacAddress; @@ -46,6 +48,7 @@ internal class TelemetryCommonProperties( private const string LibcVersion = "Libc Version"; private const string CI = "Continuous Integration"; + private const string LLM = "LLM Environment"; private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE"; private const string CannotFindMacAddress = "Unknown"; @@ -66,6 +69,7 @@ public FrozenDictionary GetTelemetryCommonProperties() {TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable)}, {DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )}, {CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() }, + {LLM, _llmEnvironmentDetector.IsLLMEnvironment().ToString() }, {CurrentPathHash, _hasher(_getCurrentDirectory())}, {MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId)}, // we don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions. diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 9b35cf3ee457..edf6dd9aaafd 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -163,6 +163,13 @@ public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() } } + [Fact] + public void TelemetryCommonPropertiesShouldReturnIsLLMDetection() + { + var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); + unitUnderTest.GetTelemetryCommonProperties()["LLM Environment"].Should().BeOneOf("True", "False"); + } + [Theory] [MemberData(nameof(CITelemetryTestCases))] public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected) @@ -184,6 +191,35 @@ public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool } } + [Theory] + [MemberData(nameof(LLMTelemetryTestCases))] + public void CanDetectLLMStatusForEnvVars(Dictionary envVars, bool expected) + { + try + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, value); + } + new LLMEnvironmentDetectorForTelemetry().IsLLMEnvironment().Should().Be(expected); + } + finally + { + foreach (var (key, value) in envVars) + { + Environment.SetEnvironmentVariable(key, null); + } + } + } + + public static IEnumerable LLMTelemetryTestCases => new List{ + new object[] { new Dictionary { { "CLAUDECODE", "1" } }, true }, + new object[] { new Dictionary { { "CLAUDECODE", "true" } }, true }, + new object[] { new Dictionary { { "CLAUDECODE", "anything" } }, true }, + new object[] { new Dictionary { { "CLAUDECODE", "" } }, false }, + new object[] { new Dictionary { { "SomethingElse", "hi" } }, false }, + }; + public static IEnumerable CITelemetryTestCases => new List{ new object[] { new Dictionary { { "TF_BUILD", "true" } }, true }, new object[] { new Dictionary { { "GITHUB_ACTIONS", "true" } }, true }, From dc5494ea7da491976ce43b7c3383e5cd59901644 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:44:53 +0000 Subject: [PATCH 3/7] Change LLM telemetry to use simpler property name and descriptive values - Changed property name from "LLM Environment" to "llm" for simplicity - Modified interface and implementation to return tool name instead of boolean - CLAUDECODE detection now returns "claude" instead of True/False - Updated all tests to expect string values instead of boolean - Tests now validate "claude" and null values appropriately Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../dotnet/Telemetry/ILLMEnvironmentDetector.cs | 2 +- .../LLMEnvironmentDetectorForTelemetry.cs | 10 +++++----- .../Telemetry/TelemetryCommonProperties.cs | 4 ++-- .../TelemetryCommonPropertiesTests.cs | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs index e3f9a31937fc..f6589160617d 100644 --- a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs +++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs @@ -7,5 +7,5 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal interface ILLMEnvironmentDetector { - bool IsLLMEnvironment(); + string GetLLMEnvironment(); } \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index 87039a07687e..b1e9ef4b0f49 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -8,21 +8,21 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { // Systems where the variable must be present and not-null - private static readonly string[] _ifNonNullVariables = [ + private static readonly string[] _claudeVariables = [ // Claude Code "CLAUDECODE" ]; - public bool IsLLMEnvironment() + public string GetLLMEnvironment() { - foreach (var variable in _ifNonNullVariables) + foreach (var variable in _claudeVariables) { if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) { - return true; + return "claude"; } } - return false; + return null; } } \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs index 4eb9b44cac21..d5731c097e0b 100644 --- a/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs +++ b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs @@ -48,7 +48,7 @@ internal class TelemetryCommonProperties( private const string LibcVersion = "Libc Version"; private const string CI = "Continuous Integration"; - private const string LLM = "LLM Environment"; + private const string LLM = "llm"; private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE"; private const string CannotFindMacAddress = "Unknown"; @@ -69,7 +69,7 @@ public FrozenDictionary GetTelemetryCommonProperties() {TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable)}, {DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )}, {CI, _ciEnvironmentDetector.IsCIEnvironment().ToString() }, - {LLM, _llmEnvironmentDetector.IsLLMEnvironment().ToString() }, + {LLM, _llmEnvironmentDetector.GetLLMEnvironment() }, {CurrentPathHash, _hasher(_getCurrentDirectory())}, {MachineIdOld, _userLevelCacheWriter.RunWithCache(MachineIdCacheKey, GetMachineId)}, // we don't want to recalcuate a new id for every new SDK version. Reuse the same path across versions. diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index edf6dd9aaafd..84ef03b4a216 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -167,7 +167,7 @@ public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() public void TelemetryCommonPropertiesShouldReturnIsLLMDetection() { var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties()["LLM Environment"].Should().BeOneOf("True", "False"); + unitUnderTest.GetTelemetryCommonProperties()["llm"].Should().BeOneOf("claude", null); } [Theory] @@ -193,7 +193,7 @@ public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool [Theory] [MemberData(nameof(LLMTelemetryTestCases))] - public void CanDetectLLMStatusForEnvVars(Dictionary envVars, bool expected) + public void CanDetectLLMStatusForEnvVars(Dictionary envVars, string expected) { try { @@ -201,7 +201,7 @@ public void CanDetectLLMStatusForEnvVars(Dictionary envVars, boo { Environment.SetEnvironmentVariable(key, value); } - new LLMEnvironmentDetectorForTelemetry().IsLLMEnvironment().Should().Be(expected); + new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected); } finally { @@ -213,11 +213,11 @@ public void CanDetectLLMStatusForEnvVars(Dictionary envVars, boo } public static IEnumerable LLMTelemetryTestCases => new List{ - new object[] { new Dictionary { { "CLAUDECODE", "1" } }, true }, - new object[] { new Dictionary { { "CLAUDECODE", "true" } }, true }, - new object[] { new Dictionary { { "CLAUDECODE", "anything" } }, true }, - new object[] { new Dictionary { { "CLAUDECODE", "" } }, false }, - new object[] { new Dictionary { { "SomethingElse", "hi" } }, false }, + new object[] { new Dictionary { { "CLAUDECODE", "1" } }, "claude" }, + new object[] { new Dictionary { { "CLAUDECODE", "true" } }, "claude" }, + new object[] { new Dictionary { { "CLAUDECODE", "anything" } }, "claude" }, + new object[] { new Dictionary { { "CLAUDECODE", "" } }, null }, + new object[] { new Dictionary { { "SomethingElse", "hi" } }, null }, }; public static IEnumerable CITelemetryTestCases => new List{ From e4c52b451e5c50f9c94418012f0458449d64783f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:13:05 +0000 Subject: [PATCH 4/7] Simplify LLM telemetry tests to only test essential scenarios Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- test/dotnet.Tests/TelemetryCommonPropertiesTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 84ef03b4a216..39bdd2ec7b37 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -214,10 +214,7 @@ public void CanDetectLLMStatusForEnvVars(Dictionary envVars, str public static IEnumerable LLMTelemetryTestCases => new List{ new object[] { new Dictionary { { "CLAUDECODE", "1" } }, "claude" }, - new object[] { new Dictionary { { "CLAUDECODE", "true" } }, "claude" }, - new object[] { new Dictionary { { "CLAUDECODE", "anything" } }, "claude" }, - new object[] { new Dictionary { { "CLAUDECODE", "" } }, null }, - new object[] { new Dictionary { { "SomethingElse", "hi" } }, null }, + new object[] { new Dictionary(), null }, }; public static IEnumerable CITelemetryTestCases => new List{ From 92ec5d84dd73fe60260db047773f6cbe55814eed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:48:57 +0000 Subject: [PATCH 5/7] Implement rule-based environment detection system with nullable reference types Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../CIEnvironmentDetectorForTelemetry.cs | 89 ++++++--------- .../Telemetry/EnvironmentDetectionRule.cs | 103 ++++++++++++++++++ .../Telemetry/ICIEnvironmentDetector.cs | 4 +- .../Telemetry/ILLMEnvironmentDetector.cs | 4 +- .../LLMEnvironmentDetectorForTelemetry.cs | 20 +--- .../TelemetryCommonPropertiesTests.cs | 3 +- 6 files changed, 146 insertions(+), 77 deletions(-) create mode 100644 src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs diff --git a/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs index 4281480cedaa..b4d6a384a45c 100644 --- a/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/CIEnvironmentDetectorForTelemetry.cs @@ -1,73 +1,50 @@ // 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.Linq; namespace Microsoft.DotNet.Cli.Telemetry; internal class CIEnvironmentDetectorForTelemetry : ICIEnvironmentDetector { - // Systems that provide boolean values only, so we can simply parse and check for true - private static readonly string[] _booleanVariables = [ - // Azure Pipelines - https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables#system-variables-devops-services - "TF_BUILD", - // GitHub Actions - https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - "GITHUB_ACTIONS", - // AppVeyor - https://www.appveyor.com/docs/environment-variables/ - "APPVEYOR", - // A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI. - // Given this, we could potentially remove all of these other options? - "CI", - // Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables - "TRAVIS", - // CircleCI - https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables - "CIRCLECI", - ]; - - // Systems where every variable must be present and not-null before returning true - private static readonly string[][] _allNotNullVariables = [ + private static readonly EnvironmentDetectionRule[] _detectionRules = [ + // Systems that provide boolean values only, so we can simply parse and check for true + new BooleanEnvironmentRule( + // Azure Pipelines - https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables#system-variables-devops-services + "TF_BUILD", + // GitHub Actions - https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + "GITHUB_ACTIONS", + // AppVeyor - https://www.appveyor.com/docs/environment-variables/ + "APPVEYOR", + // A general-use flag - Many of the major players support this: AzDo, GitHub, GitLab, AppVeyor, Travis CI, CircleCI. + // Given this, we could potentially remove all of these other options? + "CI", + // Travis CI - https://docs.travis-ci.com/user/environment-variables/#default-environment-variables + "TRAVIS", + // CircleCI - https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables + "CIRCLECI" + ), + + // Systems where every variable must be present and not-null before returning true // AWS CodeBuild - https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html - ["CODEBUILD_BUILD_ID", "AWS_REGION"], + new AllPresentEnvironmentRule("CODEBUILD_BUILD_ID", "AWS_REGION"), // Jenkins - https://github.com/jenkinsci/jenkins/blob/master/core/src/main/resources/jenkins/model/CoreEnvironmentContributor/buildEnv.groovy - ["BUILD_ID", "BUILD_URL"], + new AllPresentEnvironmentRule("BUILD_ID", "BUILD_URL"), // Google Cloud Build - https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#using_default_substitutions - ["BUILD_ID", "PROJECT_ID"] - ]; - - // Systems where the variable must be present and not-null - private static readonly string[] _ifNonNullVariables = [ - // TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters - "TEAMCITY_VERSION", - // JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general - "JB_SPACE_API_URL" + new AllPresentEnvironmentRule("BUILD_ID", "PROJECT_ID"), + + // Systems where the variable must be present and not-null + new AnyPresentEnvironmentRule( + // TeamCity - https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters + "TEAMCITY_VERSION", + // JetBrains Space - https://www.jetbrains.com/help/space/automation-environment-variables.html#general + "JB_SPACE_API_URL" + ) ]; public bool IsCIEnvironment() { - foreach (var booleanVariable in _booleanVariables) - { - if (bool.TryParse(Environment.GetEnvironmentVariable(booleanVariable), out bool envVar) && envVar) - { - return true; - } - } - - foreach (var variables in _allNotNullVariables) - { - if (variables.All((variable) => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable)))) - { - return true; - } - } - - foreach (var variable in _ifNonNullVariables) - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) - { - return true; - } - } - - return false; + return _detectionRules.Any(rule => rule.IsMatch()); } } diff --git a/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs new file mode 100644 index 000000000000..5cd73f53abb8 --- /dev/null +++ b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs @@ -0,0 +1,103 @@ +// 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.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Telemetry; + +/// +/// Base class for environment detection rules that can be evaluated against environment variables. +/// +internal abstract class EnvironmentDetectionRule +{ + /// + /// Evaluates the rule against the current environment. + /// + /// True if the rule matches the current environment; otherwise, false. + public abstract bool IsMatch(); +} + +/// +/// Rule that matches when any of the specified environment variables is set to "true". +/// +internal class BooleanEnvironmentRule : EnvironmentDetectionRule +{ + private readonly string[] _variables; + + public BooleanEnvironmentRule(params string[] variables) + { + _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + } + + public override bool IsMatch() + { + return _variables.Any(variable => + bool.TryParse(Environment.GetEnvironmentVariable(variable), out bool value) && value); + } +} + +/// +/// Rule that matches when all specified environment variables are present and not null/empty. +/// +internal class AllPresentEnvironmentRule : EnvironmentDetectionRule +{ + private readonly string[] _variables; + + public AllPresentEnvironmentRule(params string[] variables) + { + _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + } + + public override bool IsMatch() + { + return _variables.All(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))); + } +} + +/// +/// Rule that matches when any of the specified environment variables is present and not null/empty. +/// +internal class AnyPresentEnvironmentRule : EnvironmentDetectionRule +{ + private readonly string[] _variables; + + public AnyPresentEnvironmentRule(params string[] variables) + { + _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + } + + public override bool IsMatch() + { + return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))); + } +} + +/// +/// Rule that matches when any of the specified environment variables is present and not null/empty, +/// and returns the associated result value. +/// +/// The type of the result value. +internal class EnvironmentDetectionRuleWithResult where T : class +{ + private readonly string[] _variables; + private readonly T _result; + + public EnvironmentDetectionRuleWithResult(T result, params string[] variables) + { + _variables = variables ?? throw new ArgumentNullException(nameof(variables)); + _result = result ?? throw new ArgumentNullException(nameof(result)); + } + + /// + /// Evaluates the rule and returns the result if matched. + /// + /// The result value if the rule matches; otherwise, null. + public T? GetResult() + { + return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) + ? _result + : null; + } +} \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/ICIEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ICIEnvironmentDetector.cs index d65c4c87feeb..4c36e8faea01 100644 --- a/src/Cli/dotnet/Telemetry/ICIEnvironmentDetector.cs +++ b/src/Cli/dotnet/Telemetry/ICIEnvironmentDetector.cs @@ -1,8 +1,6 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - namespace Microsoft.DotNet.Cli.Telemetry; internal interface ICIEnvironmentDetector diff --git a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs index f6589160617d..fe599569aa6c 100644 --- a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs +++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - namespace Microsoft.DotNet.Cli.Telemetry; internal interface ILLMEnvironmentDetector { - string GetLLMEnvironment(); + string? GetLLMEnvironment(); } \ No newline at end of file diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index b1e9ef4b0f49..da0f0b35c20a 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -1,28 +1,20 @@ // 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.Linq; namespace Microsoft.DotNet.Cli.Telemetry; internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { - // Systems where the variable must be present and not-null - private static readonly string[] _claudeVariables = [ + private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [ // Claude Code - "CLAUDECODE" + new EnvironmentDetectionRuleWithResult("claude", "CLAUDECODE") ]; - public string GetLLMEnvironment() + public string? GetLLMEnvironment() { - foreach (var variable in _claudeVariables) - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) - { - return "claude"; - } - } - - return null; + return _detectionRules.Select(rule => rule.GetResult()).FirstOrDefault(result => result != null); } } \ No newline at end of file diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 53fcbca03377..1f9c29d6ee21 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -167,7 +167,7 @@ public void TelemetryCommonPropertiesShouldContainLibcReleaseAndVersion() public void TelemetryCommonPropertiesShouldReturnIsLLMDetection() { var unitUnderTest = new TelemetryCommonProperties(getMACAddress: () => null, userLevelCacheWriter: new NothingCache()); - unitUnderTest.GetTelemetryCommonProperties()["llm"].Should().BeOneOf("claude", null); + unitUnderTest.GetTelemetryCommonProperties("dummySessionId")["llm"].Should().BeOneOf("claude", null); } [Theory] @@ -212,6 +212,7 @@ public void CanDetectLLMStatusForEnvVars(Dictionary envVars, str } } + [Theory] [InlineData("dummySessionId")] [InlineData(null)] public void TelemetryCommonPropertiesShouldContainSessionId(string sessionId) From 7736af5e77e8e5dc8c200685419f36924514c7c1 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Wed, 17 Sep 2025 17:58:50 -0500 Subject: [PATCH 6/7] Update src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs Co-authored-by: Michael Simons --- src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index da0f0b35c20a..37e32ad55658 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -15,6 +15,7 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector public string? GetLLMEnvironment() { - return _detectionRules.Select(rule => rule.GetResult()).FirstOrDefault(result => result != null); + return string.Join(", ", _detectionRules.Select(r => r.GetResult()).Where(r => r != null); +); } } \ No newline at end of file From 984a512f707bd0dccd06977b82971331db0c3635 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Sep 2025 23:11:32 +0000 Subject: [PATCH 7/7] Fix LLM detection syntax and add multiple environment support with test coverage Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../Telemetry/LLMEnvironmentDetectorForTelemetry.cs | 8 +++++--- test/dotnet.Tests/TelemetryCommonPropertiesTests.cs | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index 37e32ad55658..16d13a6879e7 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -10,12 +10,14 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [ // Claude Code - new EnvironmentDetectionRuleWithResult("claude", "CLAUDECODE") + new EnvironmentDetectionRuleWithResult("claude", "CLAUDECODE"), + // Cursor AI + new EnvironmentDetectionRuleWithResult("cursor", "CURSOR_EDITOR") ]; public string? GetLLMEnvironment() { - return string.Join(", ", _detectionRules.Select(r => r.GetResult()).Where(r => r != null); -); + var results = _detectionRules.Select(r => r.GetResult()).Where(r => r != null).ToArray(); + return results.Length > 0 ? string.Join(", ", results) : null; } } \ No newline at end of file diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 1f9c29d6ee21..a77f0bf81765 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -227,6 +227,8 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string sessionId) public static IEnumerable LLMTelemetryTestCases => new List{ new object[] { new Dictionary { { "CLAUDECODE", "1" } }, "claude" }, + new object[] { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, + new object[] { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" }, new object[] { new Dictionary(), null }, };