Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@ namespace Microsoft.DotNet.Cli.Telemetry;

internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
{
// All rules are presence-based: a variable being set (non-empty) is enough to trip detection.
private static readonly EnvironmentDetectionRuleWithResult<string>[] _detectionRules = [
// Cowork (Claude Code cowork mode) - placed before Claude so the more specific variable wins ordering
new EnvironmentDetectionRuleWithResult<string>("cowork", new AnyPresentEnvironmentRule("CLAUDE_CODE_IS_COWORK")),
Comment thread
baronfel marked this conversation as resolved.
Outdated
// Claude Code
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT")),
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE", "CLAUDE_CODE_ENTRYPOINT")),
// Cursor AI
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI")),
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI", "CURSOR_TRACE_ID", "CURSOR_AGENT")),
// Gemini
new EnvironmentDetectionRuleWithResult<string>("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<string>("copilot", new AnyMatchEnvironmentRule(
new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE"),
new AnyPresentEnvironmentRule("GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_AGENT"))),
new EnvironmentDetectionRuleWithResult<string>("gemini", new AnyPresentEnvironmentRule("GEMINI_CLI")),
// GitHub Copilot (legacy gh extension: GITHUB_COPILOT_CLI_MODE; new Copilot CLI: GH_COPILOT_WORKING_DIRECTORY, COPILOT_CLI, COPILOT_AGENT, COPILOT_MODEL, COPILOT_ALLOW_ALL, or COPILOT_GITHUB_TOKEN is set)
new EnvironmentDetectionRuleWithResult<string>("copilot", new AnyPresentEnvironmentRule(
Comment thread
baronfel marked this conversation as resolved.
Outdated
"GITHUB_COPILOT_CLI_MODE", "GH_COPILOT_WORKING_DIRECTORY", "COPILOT_CLI", "COPILOT_AGENT", "COPILOT_MODEL", "COPILOT_ALLOW_ALL", "COPILOT_GITHUB_TOKEN")),
// Codex CLI
new EnvironmentDetectionRuleWithResult<string>("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX")),
new EnvironmentDetectionRuleWithResult<string>("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID")),
// Aider
new EnvironmentDetectionRuleWithResult<string>("aider", new EnvironmentVariableValueRule("OR_APP_NAME", "Aider")),
// Plandex
Expand All @@ -30,25 +32,31 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
// Qwen Code
new EnvironmentDetectionRuleWithResult<string>("qwen", new AnyPresentEnvironmentRule("QWEN_CODE")),
// Droid
new EnvironmentDetectionRuleWithResult<string>("droid", new BooleanEnvironmentRule("DROID_CLI")),
new EnvironmentDetectionRuleWithResult<string>("droid", new AnyPresentEnvironmentRule("DROID_CLI")),
// OpenCode
new EnvironmentDetectionRuleWithResult<string>("opencode", new AnyPresentEnvironmentRule("OPENCODE_AI")),
// Zed AI
new EnvironmentDetectionRuleWithResult<string>("zed", new AnyPresentEnvironmentRule("ZED_ENVIRONMENT", "ZED_TERM")),
// Kimi CLI
new EnvironmentDetectionRuleWithResult<string>("kimi", new BooleanEnvironmentRule("KIMI_CLI")),
new EnvironmentDetectionRuleWithResult<string>("kimi", new AnyPresentEnvironmentRule("KIMI_CLI")),
// OpenHands
new EnvironmentDetectionRuleWithResult<string>("openhands", new EnvironmentVariableValueRule("OR_APP_NAME", "OpenHands")),
// Goose
new EnvironmentDetectionRuleWithResult<string>("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL")),
new EnvironmentDetectionRuleWithResult<string>("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL", "GOOSE_PROVIDER")),
// Cline
new EnvironmentDetectionRuleWithResult<string>("cline", new AnyPresentEnvironmentRule("CLINE_TASK_ID")),
// Roo Code
new EnvironmentDetectionRuleWithResult<string>("roo", new AnyPresentEnvironmentRule("ROO_CODE_TASK_ID")),
// Windsurf
new EnvironmentDetectionRuleWithResult<string>("windsurf", new AnyPresentEnvironmentRule("WINDSURF_SESSION")),
// Replit
new EnvironmentDetectionRuleWithResult<string>("replit", new AnyPresentEnvironmentRule("REPL_ID")),
// Augment
new EnvironmentDetectionRuleWithResult<string>("augment", new AnyPresentEnvironmentRule("AUGMENT_AGENT")),
// Antigravity
new EnvironmentDetectionRuleWithResult<string>("antigravity", new AnyPresentEnvironmentRule("ANTIGRAVITY_AGENT")),
// (proposed) generic flag for Agentic usage
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")),
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new AnyPresentEnvironmentRule("AGENT_CLI")),
];

/// <inheritdoc/>
Expand Down
28 changes: 23 additions & 5 deletions test/dotnet.Tests/TelemetryTests/TelemetryCommonPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,27 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
public static TheoryData<Dictionary<string, string>?, string?> LLMTelemetryTestCases => new()
{
{ new Dictionary<string, string> { {"CLAUDECODE", "1" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE", "1" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE_ENTRYPOINT", "some_value" } }, "claude" },
{ new Dictionary<string, string> { {"CLAUDE_CODE_IS_COWORK", "1" } }, "cowork" },
{ new Dictionary<string, string> { { "CURSOR_EDITOR", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_AI", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_TRACE_ID", "abc" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_AGENT", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" } }, "gemini" },
// Existence-based: any non-empty value now matches, even "0"
{ new Dictionary<string, string> { { "GEMINI_CLI", "0" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" },
{ new Dictionary<string, string> { { "GH_COPILOT_WORKING_DIRECTORY", "/repo" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_CLI", "1" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_AGENT", "1" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_MODEL", "gpt" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_ALLOW_ALL", "1" } }, "copilot" },
{ new Dictionary<string, string> { { "COPILOT_GITHUB_TOKEN", "token" } }, "copilot" },
{ new Dictionary<string, string> { { "CODEX_CLI", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_SANDBOX", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_CI", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_THREAD_ID", "thread1" } }, "codex" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "Aider" } }, "aider" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "aider" } }, "aider" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "plandex" } }, "plandex" },
Expand All @@ -268,9 +279,13 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
{ new Dictionary<string, string> { { "OR_APP_NAME", "OpenHands" } }, "openhands" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "openhands" } }, "openhands" },
{ new Dictionary<string, string> { { "GOOSE_TERMINAL", "1" } }, "goose" },
{ new Dictionary<string, string> { { "GOOSE_PROVIDER", "openai" } }, "goose" },
{ new Dictionary<string, string> { { "CLINE_TASK_ID", "task123" } }, "cline" },
{ new Dictionary<string, string> { { "ROO_CODE_TASK_ID", "task456" } }, "roo" },
{ new Dictionary<string, string> { { "WINDSURF_SESSION", "session789" } }, "windsurf" },
{ new Dictionary<string, string> { { "REPL_ID", "repl1" } }, "replit" },
{ new Dictionary<string, string> { { "AUGMENT_AGENT", "1" } }, "augment" },
{ new Dictionary<string, string> { { "ANTIGRAVITY_AGENT", "1" } }, "antigravity" },
{ new Dictionary<string, string> { { "AGENT_CLI", "true" } }, "generic_agent" },
// Test combinations of older tools
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" },
Expand All @@ -281,11 +296,14 @@ public void TelemetryCommonPropertiesShouldContainSessionId(string? sessionId)
{ new Dictionary<string, string> { { "OR_APP_NAME", "Aider" }, { "CLINE_TASK_ID", "task123" } }, "aider, cline" },
{ new Dictionary<string, string> { { "CODEX_CLI", "1" }, { "WINDSURF_SESSION", "session789" } }, "codex, windsurf" },
{ new Dictionary<string, string> { { "GOOSE_TERMINAL", "1" }, { "ROO_CODE_TASK_ID", "task456" } }, "goose, roo" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "false" } }, null },
{ new Dictionary<string, string> { { "AGENT_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "DROID_CLI", "false" } }, null },
{ new Dictionary<string, string> { { "KIMI_CLI", "false" } }, null },
// Existence-based loosened vars now match regardless of value (e.g. "false" is still a non-empty value)
{ new Dictionary<string, string> { { "GEMINI_CLI", "false" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "false" } }, "copilot" },
{ new Dictionary<string, string> { { "AGENT_CLI", "false" } }, "generic_agent" },
{ new Dictionary<string, string> { { "DROID_CLI", "false" } }, "droid" },
{ new Dictionary<string, string> { { "KIMI_CLI", "false" } }, "kimi" },
// Cowork is distinct from claude and reported independently
{ new Dictionary<string, string> { { "CLAUDE_CODE_IS_COWORK", "1" }, { "CLAUDE_CODE", "1" } }, "cowork, claude" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "SomeOtherApp" } }, null },
{ new Dictionary<string, string>(), null },
};
Expand Down
Loading