diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index fdbf17f5c6c0..0682d3bf641c 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -8,19 +8,27 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector { + // Most rules are presence-based (a variable being set/non-empty is enough); a few use exact-value + // matching (e.g. OR_APP_NAME, which several tools share). All matching rules contribute to the result. private static readonly EnvironmentDetectionRuleWithResult[] _detectionRules = [ + // Cowork (Claude Code cowork mode) - placed before Claude so the more specific variable is listed first + new EnvironmentDetectionRuleWithResult("cowork", new AnyPresentEnvironmentRule("CLAUDE_CODE_IS_COWORK")), // Claude Code - new EnvironmentDetectionRuleWithResult("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT")), + new EnvironmentDetectionRuleWithResult("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT")), // Cursor AI - new EnvironmentDetectionRuleWithResult("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI")), + new EnvironmentDetectionRuleWithResult("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI", "CURSOR_TRACE_ID", "CURSOR_AGENT")), // Gemini - new EnvironmentDetectionRuleWithResult("gemini", new BooleanEnvironmentRule("GEMINI_CLI")), - // GitHub Copilot (legacy gh extension: GITHUB_COPILOT_CLI_MODE=true; new Copilot CLI: GH_COPILOT_WORKING_DIRECTORY, COPILOT_CLI, or COPILOT_AGENT is set) - new EnvironmentDetectionRuleWithResult("copilot", new AnyMatchEnvironmentRule( - new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE"), - new AnyPresentEnvironmentRule("GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_AGENT"))), + new EnvironmentDetectionRuleWithResult("gemini", new AnyPresentEnvironmentRule("GEMINI_CLI")), + // GitHub Copilot CLI (legacy gh extension: GITHUB_COPILOT_CLI_MODE; new Copilot CLI: GH_COPILOT_WORKING_DIRECTORY, COPILOT_CLI, COPILOT_MODEL, COPILOT_ALLOW_ALL, or COPILOT_GITHUB_TOKEN is set). + new EnvironmentDetectionRuleWithResult("copilot-cli", new AnyPresentEnvironmentRule( + "GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN")), + // GitHub Copilot agent mode in VS Code, which sets AI_AGENT=github_copilot_vscode_agent and COPILOT_AGENT=1 on the terminals it runs commands in. + // See https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts + new EnvironmentDetectionRuleWithResult("copilot-vscode", new AnyMatchEnvironmentRule( + new EnvironmentVariableValueRule("AI_AGENT", "github_copilot_vscode_agent"), + new AnyPresentEnvironmentRule("COPILOT_AGENT"))), // Codex CLI - new EnvironmentDetectionRuleWithResult("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX")), + new EnvironmentDetectionRuleWithResult("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID")), // Aider new EnvironmentDetectionRuleWithResult("aider", new EnvironmentVariableValueRule("OR_APP_NAME", "Aider")), // Plandex @@ -30,25 +38,31 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector // Qwen Code new EnvironmentDetectionRuleWithResult("qwen", new AnyPresentEnvironmentRule("QWEN_CODE")), // Droid - new EnvironmentDetectionRuleWithResult("droid", new BooleanEnvironmentRule("DROID_CLI")), + new EnvironmentDetectionRuleWithResult("droid", new AnyPresentEnvironmentRule("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")), + new EnvironmentDetectionRuleWithResult("kimi", new AnyPresentEnvironmentRule("KIMI_CLI")), // OpenHands new EnvironmentDetectionRuleWithResult("openhands", new EnvironmentVariableValueRule("OR_APP_NAME", "OpenHands")), // Goose - new EnvironmentDetectionRuleWithResult("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL")), + new EnvironmentDetectionRuleWithResult("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL", "GOOSE_PROVIDER")), // 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")), + // Replit + new EnvironmentDetectionRuleWithResult("replit", new AnyPresentEnvironmentRule("REPL_ID")), + // Augment + new EnvironmentDetectionRuleWithResult("augment", new AnyPresentEnvironmentRule("AUGMENT_AGENT")), + // Antigravity + new EnvironmentDetectionRuleWithResult("antigravity", new AnyPresentEnvironmentRule("ANTIGRAVITY_AGENT")), // (proposed) generic flag for Agentic usage - new EnvironmentDetectionRuleWithResult("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")), + new EnvironmentDetectionRuleWithResult("generic_agent", new AnyPresentEnvironmentRule("AGENT_CLI")), ]; /// diff --git a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs b/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs index 5822119daba2..1ad44d46b1d4 100644 --- a/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs +++ b/test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs @@ -244,16 +244,29 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) public static TheoryData?, string?> LLMTelemetryTestCases => new() { { new Dictionary { {"CLAUDECODE", "1" } }, "claude" }, + { new Dictionary { {"CLAUDE_CODE", "1" } }, "claude" }, { new Dictionary { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" }, + { new Dictionary { {"CLAUDE_CODE_IS_COWORK", "1" } }, "cowork" }, { new Dictionary { { "CURSOR_EDITOR", "1" } }, "cursor" }, { new Dictionary { { "CURSOR_AI", "1" } }, "cursor" }, + { new Dictionary { { "CURSOR_TRACE_ID", "abc" } }, "cursor" }, + { new Dictionary { { "CURSOR_AGENT", "1" } }, "cursor" }, { new Dictionary { { "GEMINI_CLI", "true" } }, "gemini" }, - { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" }, - { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" }, - { new Dictionary { { "COPILOT_CLI", "1" } }, "copilot" }, - { new Dictionary { { "COPILOT_AGENT", "1" } }, "copilot" }, + // Existence-based: any non-empty value now matches, even "0" + { new Dictionary { { "GEMINI_CLI", "0" } }, "gemini" }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot-cli" }, + { new Dictionary { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot-cli" }, + { new Dictionary { { "COPILOT_CLI", "1" } }, "copilot-cli" }, + { new Dictionary { { "COPILOT_MODEL", "gpt" } }, "copilot-cli" }, + { new Dictionary { { "COPILOT_ALLOW_ALL", "1" } }, "copilot-cli" }, + { new Dictionary { { "COPILOT_GITHUB_TOKEN", "token" } }, "copilot-cli" }, + // GitHub Copilot agent mode in VS Code + { new Dictionary { { "COPILOT_AGENT", "1" } }, "copilot-vscode" }, + { new Dictionary { { "AI_AGENT", "github_copilot_vscode_agent" } }, "copilot-vscode" }, { new Dictionary { { "CODEX_CLI", "1" } }, "codex" }, { new Dictionary { { "CODEX_SANDBOX", "1" } }, "codex" }, + { new Dictionary { { "CODEX_CI", "1" } }, "codex" }, + { new Dictionary { { "CODEX_THREAD_ID", "thread1" } }, "codex" }, { new Dictionary { { "OR_APP_NAME", "Aider" } }, "aider" }, { new Dictionary { { "OR_APP_NAME", "aider" } }, "aider" }, { new Dictionary { { "OR_APP_NAME", "plandex" } }, "plandex" }, @@ -268,24 +281,31 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId) { new Dictionary { { "OR_APP_NAME", "OpenHands" } }, "openhands" }, { new Dictionary { { "OR_APP_NAME", "openhands" } }, "openhands" }, { new Dictionary { { "GOOSE_TERMINAL", "1" } }, "goose" }, + { new Dictionary { { "GOOSE_PROVIDER", "openai" } }, "goose" }, { new Dictionary { { "CLINE_TASK_ID", "task123" } }, "cline" }, { new Dictionary { { "ROO_CODE_TASK_ID", "task456" } }, "roo" }, { new Dictionary { { "WINDSURF_SESSION", "session789" } }, "windsurf" }, + { new Dictionary { { "REPL_ID", "repl1" } }, "replit" }, + { new Dictionary { { "AUGMENT_AGENT", "1" } }, "augment" }, + { new Dictionary { { "ANTIGRAVITY_AGENT", "1" } }, "antigravity" }, { 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 { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot-cli" }, { 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" }, + { new Dictionary { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" }, { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" }, { "AGENT_CLI", "true" } }, "claude, cursor, gemini, copilot-cli, 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 }, + // Existence-based loosened vars now match regardless of value (e.g. "false" is still a non-empty value) + { new Dictionary { { "GEMINI_CLI", "false" } }, "gemini" }, + { new Dictionary { { "GITHUB_COPILOT_CLI_MODE", "false" } }, "copilot-cli" }, + { new Dictionary { { "AGENT_CLI", "false" } }, "generic_agent" }, + { new Dictionary { { "DROID_CLI", "false" } }, "droid" }, + { new Dictionary { { "KIMI_CLI", "false" } }, "kimi" }, + // Cowork is distinct from claude and reported independently + { new Dictionary { { "CLAUDE_CODE_IS_COWORK", "1" }, { "CLAUDE_CODE", "1" } }, "cowork, claude" }, { new Dictionary { { "OR_APP_NAME", "SomeOtherApp" } }, null }, { new Dictionary(), null }, };