diff --git a/documentation/project-docs/telemetry.md b/documentation/project-docs/telemetry.md index e559f60bb25b..6db82f712ee1 100644 --- a/documentation/project-docs/telemetry.md +++ b/documentation/project-docs/telemetry.md @@ -61,7 +61,7 @@ Every telemetry event automatically includes these common properties: | **Telemetry Profile** | Custom telemetry profile (if set via env var) | Custom value or null | | **Docker Container** | Whether running in Docker container | `True` or `False` | | **CI** | Whether running in CI environment | `True` or `False` | -| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude`, `cursor`, `gemini`, `copilot`, `generic_agent` | +| **LLM** | Detected LLM/assistant environment identifiers (comma-separated) | `claude`, `cursor`, `gemini`, `copilot`, `codex`, `aider`, `amp`, `qwen`, `droid`, `opencode`, `zed`, `kimi`, `openhands`, `goose`, `cline`, `roo`, `windsurf`, `generic_agent` | | **Current Path Hash** | SHA256 hash of current directory path | Hashed value | | **Machine ID** | SHA256 hash of machine MAC address (or GUID if unavailable) | Hashed value | | **Machine ID Old** | Legacy machine ID for compatibility | Hashed value | diff --git a/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/EnvironmentDetectionRule.cs b/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/EnvironmentDetectionRule.cs index 80d710d66cc0..ad9322a22633 100644 --- a/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/EnvironmentDetectionRule.cs +++ b/src/Cli/Microsoft.DotNet.Cli.Definitions/Telemetry/EnvironmentDetectionRule.cs @@ -69,6 +69,27 @@ public override bool IsMatch() } } +/// +/// Rule that matches when an environment variable contains a specific value (case-insensitive). +/// +internal class EnvironmentVariableValueRule : EnvironmentDetectionRule +{ + private readonly string _variable; + private readonly string _expectedValue; + + public EnvironmentVariableValueRule(string variable, string expectedValue) + { + _variable = variable ?? throw new ArgumentNullException(nameof(variable)); + _expectedValue = expectedValue ?? throw new ArgumentNullException(nameof(expectedValue)); + } + + public override bool IsMatch() + { + var value = Environment.GetEnvironmentVariable(_variable); + return !string.IsNullOrEmpty(value) && value.Equals(_expectedValue, StringComparison.OrdinalIgnoreCase); + } +} + /// /// Rule that matches when any of the specified environment variables is present and not null/empty, /// and returns the associated result value. diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index b37f9b5d0830..65368c4eef69 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -7,13 +7,39 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [ // Claude Code - new EnvironmentDetectionRuleWithResult("claude", new AnyPresentEnvironmentRule("CLAUDECODE")), + new EnvironmentDetectionRuleWithResult("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT")), // Cursor AI - new EnvironmentDetectionRuleWithResult("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR")), + new EnvironmentDetectionRuleWithResult("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI")), // Gemini new EnvironmentDetectionRuleWithResult("gemini", new BooleanEnvironmentRule("GEMINI_CLI")), // GitHub Copilot new EnvironmentDetectionRuleWithResult("copilot", new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE")), + // Codex CLI + new EnvironmentDetectionRuleWithResult("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX")), + // Aider + new EnvironmentDetectionRuleWithResult("aider", new EnvironmentVariableValueRule("OR_APP_NAME", "Aider")), + // Amp + new EnvironmentDetectionRuleWithResult("amp", new AnyPresentEnvironmentRule("AMP_HOME")), + // Qwen Code + new EnvironmentDetectionRuleWithResult("qwen", new AnyPresentEnvironmentRule("QWEN_CODE")), + // Droid + new EnvironmentDetectionRuleWithResult("droid", new BooleanEnvironmentRule("DROID_CLI")), + // OpenCode + new EnvironmentDetectionRuleWithResult("opencode", new AnyPresentEnvironmentRule("OPENCODE_AI")), + // Zed AI + new EnvironmentDetectionRuleWithResult("zed", new AnyPresentEnvironmentRule("ZED_ENVIRONMENT", "ZED_TERM")), + // Kimi CLI + new EnvironmentDetectionRuleWithResult("kimi", new BooleanEnvironmentRule("KIMI_CLI")), + // OpenHands + new EnvironmentDetectionRuleWithResult("openhands", new EnvironmentVariableValueRule("OR_APP_NAME", "OpenHands")), + // Goose + new EnvironmentDetectionRuleWithResult("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL")), + // Cline + new EnvironmentDetectionRuleWithResult("cline", new AnyPresentEnvironmentRule("CLINE_TASK_ID")), + // Roo Code + new EnvironmentDetectionRuleWithResult("roo", new AnyPresentEnvironmentRule("ROO_CODE_TASK_ID")), + // Windsurf + new EnvironmentDetectionRuleWithResult("windsurf", new AnyPresentEnvironmentRule("WINDSURF_SESSION")), // (proposed) generic flag for Agentic usage new EnvironmentDetectionRuleWithResult("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")), ]; diff --git a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs index 4e28b92479d7..188faf237128 100644 --- a/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryCommonPropertiesTests.cs @@ -231,17 +231,44 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) public static TheoryData?, string?> LLMTelemetryTestCases => new() { { new Dictionary { {"CLAUDECODE", "1" } }, "claude" }, + { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" }, { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, + { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" }, { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, + { new Dictionary { { "CODEX_CLI", "1" } }, "codex" }, + { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" }, + { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" }, + { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" }, + { new Dictionary { { "AMP_HOME", "/path/to/amp" } }, "amp" }, + { new Dictionary { { "QWEN_CODE", "1" } }, "qwen" }, + { new Dictionary { { "DROID_CLI", "true" } }, "droid" }, + { new Dictionary { { "OPENCODE_AI", "1" } }, "opencode" }, + { new Dictionary { { "ZED_ENVIRONMENT", "1" } }, "zed" }, + { new Dictionary { { "ZED_TERM", "1" } }, "zed" }, + { new Dictionary { { "KIMI_CLI", "true" } }, "kimi" }, + { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" }, + { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" }, + { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" }, + { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" }, + { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" }, + { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" }, { new Dictionary { { "AGENT_CLI", "true" } }, "generic_agent" }, + // Test combinations of older tools { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" }, { new Dictionary { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" }, { new Dictionary { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" }, { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot, generic_agent" }, + // Test combinations of newer tools + { new Dictionary { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" }, + { new Dictionary { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" }, + { new Dictionary { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" }, { new Dictionary { { "GEMINI_CLI", "false" } }, null }, { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null }, { new Dictionary { { "AGENT_CLI", "false" } }, null }, + { new Dictionary { { "DROID_CLI", "false" } }, null }, + { new Dictionary { { "KIMI_CLI", "false" } }, null }, + { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null }, { new Dictionary(), null }, };