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
new file mode 100644
index 000000000000..fe599569aa6c
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Cli.Telemetry;
+
+internal interface ILLMEnvironmentDetector
+{
+ string? GetLLMEnvironment();
+}
\ 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..16d13a6879e7
--- /dev/null
+++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs
@@ -0,0 +1,23 @@
+// 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.Linq;
+
+namespace Microsoft.DotNet.Cli.Telemetry;
+
+internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
+{
+ private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [
+ // Claude Code
+ new EnvironmentDetectionRuleWithResult("claude", "CLAUDECODE"),
+ // Cursor AI
+ new EnvironmentDetectionRuleWithResult("cursor", "CURSOR_EDITOR")
+ ];
+
+ public string? GetLLMEnvironment()
+ {
+ 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/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs b/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs
index 13aaf12883a4..625e343d7589 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;
@@ -47,6 +49,7 @@ internal class TelemetryCommonProperties(
private const string SessionId = "SessionId";
private const string CI = "Continuous Integration";
+ private const string LLM = "llm";
private const string TelemetryProfileEnvironmentVariable = "DOTNET_CLI_TELEMETRY_PROFILE";
private const string CannotFindMacAddress = "Unknown";
@@ -67,6 +70,7 @@ public FrozenDictionary GetTelemetryCommonProperties(string curr
{TelemetryProfile, Environment.GetEnvironmentVariable(TelemetryProfileEnvironmentVariable)},
{DockerContainer, _userLevelCacheWriter.RunWithCache(IsDockerContainerCacheKey, () => _dockerContainerDetector.IsDockerContainer().ToString("G") )},
{CI, _ciEnvironmentDetector.IsCIEnvironment().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 f7c75d576118..a77f0bf81765 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("dummySessionId")["llm"].Should().BeOneOf("claude", null);
+ }
+
[Theory]
[MemberData(nameof(CITelemetryTestCases))]
public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool expected)
@@ -184,6 +191,27 @@ public void CanDetectCIStatusForEnvVars(Dictionary envVars, bool
}
}
+ [Theory]
+ [MemberData(nameof(LLMTelemetryTestCases))]
+ public void CanDetectLLMStatusForEnvVars(Dictionary envVars, string expected)
+ {
+ try
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, value);
+ }
+ new LLMEnvironmentDetectorForTelemetry().GetLLMEnvironment().Should().Be(expected);
+ }
+ finally
+ {
+ foreach (var (key, value) in envVars)
+ {
+ Environment.SetEnvironmentVariable(key, null);
+ }
+ }
+ }
+
[Theory]
[InlineData("dummySessionId")]
[InlineData(null)]
@@ -196,6 +224,14 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string sessionId)
commonProperties["SessionId"].Should().Be(sessionId);
}
+
+ public static IEnumerable