Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
21 changes: 21 additions & 0 deletions src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ public override bool IsMatch()
}
}

/// <summary>
/// Rule that matches when an environment variable contains a specific value (case-insensitive).
/// </summary>
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);
}
}

/// <summary>
/// Rule that matches when any of the specified environment variables is present and not null/empty,
/// and returns the associated result value.
Expand Down
30 changes: 28 additions & 2 deletions src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,39 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector
{
private static readonly EnvironmentDetectionRuleWithResult<string>[] _detectionRules = [
// Claude Code
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE")),
new EnvironmentDetectionRuleWithResult<string>("claude", new AnyPresentEnvironmentRule("CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT")),
// Cursor AI
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR")),
new EnvironmentDetectionRuleWithResult<string>("cursor", new AnyPresentEnvironmentRule("CURSOR_EDITOR", "CURSOR_AI")),
// Gemini
new EnvironmentDetectionRuleWithResult<string>("gemini", new BooleanEnvironmentRule("GEMINI_CLI")),
// GitHub Copilot
new EnvironmentDetectionRuleWithResult<string>("copilot", new BooleanEnvironmentRule("GITHUB_COPILOT_CLI_MODE")),
// Codex CLI
new EnvironmentDetectionRuleWithResult<string>("codex", new AnyPresentEnvironmentRule("CODEX_CLI", "CODEX_SANDBOX")),
// Aider
new EnvironmentDetectionRuleWithResult<string>("aider", new EnvironmentVariableValueRule("OR_APP_NAME", "Aider")),
// Amp
new EnvironmentDetectionRuleWithResult<string>("amp", new AnyPresentEnvironmentRule("AMP_HOME")),
// Qwen Code
new EnvironmentDetectionRuleWithResult<string>("qwen", new AnyPresentEnvironmentRule("QWEN_CODE")),
// Droid
new EnvironmentDetectionRuleWithResult<string>("droid", new BooleanEnvironmentRule("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")),
// OpenHands
new EnvironmentDetectionRuleWithResult<string>("openhands", new EnvironmentVariableValueRule("OR_APP_NAME", "OpenHands")),
// Goose
new EnvironmentDetectionRuleWithResult<string>("goose", new AnyPresentEnvironmentRule("GOOSE_TERMINAL")),
// 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")),
// (proposed) generic flag for Agentic usage
new EnvironmentDetectionRuleWithResult<string>("generic_agent", new BooleanEnvironmentRule("AGENT_CLI")),
];
Expand Down
27 changes: 27 additions & 0 deletions test/dotnet.Tests/TelemetryCommonPropertiesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,17 +232,44 @@ 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_ENTRYPOINT", "some_value" } }, "claude" },
{ new Dictionary<string, string> { { "CURSOR_EDITOR", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "CURSOR_AI", "1" } }, "cursor" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" } }, "gemini" },
{ new Dictionary<string, string> { { "GITHUB_COPILOT_CLI_MODE", "true" } }, "copilot" },
{ new Dictionary<string, string> { { "CODEX_CLI", "1" } }, "codex" },
{ new Dictionary<string, string> { { "CODEX_SANDBOX", "1" } }, "codex" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "Aider" } }, "aider" },
{ new Dictionary<string, string> { { "OR_APP_NAME", "aider" } }, "aider" },
{ new Dictionary<string, string> { { "AMP_HOME", "/path/to/amp" } }, "amp" },
{ new Dictionary<string, string> { { "QWEN_CODE", "1" } }, "qwen" },
{ new Dictionary<string, string> { { "DROID_CLI", "true" } }, "droid" },
{ new Dictionary<string, string> { { "OPENCODE_AI", "1" } }, "opencode" },
{ new Dictionary<string, string> { { "ZED_ENVIRONMENT", "1" } }, "zed" },
{ new Dictionary<string, string> { { "ZED_TERM", "1" } }, "zed" },
{ new Dictionary<string, string> { { "KIMI_CLI", "true" } }, "kimi" },
{ 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> { { "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> { { "AGENT_CLI", "true" } }, "generic_agent" },
// Test combinations of older tools
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "CURSOR_EDITOR", "1" } }, "claude, cursor" },
{ new Dictionary<string, string> { { "GEMINI_CLI", "true" }, { "GITHUB_COPILOT_CLI_MODE", "true" } }, "gemini, copilot" },
{ new Dictionary<string, string> { { "CLAUDECODE", "1" }, { "GEMINI_CLI", "true" }, { "AGENT_CLI", "true" } }, "claude, gemini, generic_agent" },
{ new Dictionary<string, string> { { "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<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 },
{ new Dictionary<string, string> { { "OR_APP_NAME", "SomeOtherApp" } }, null },
{ new Dictionary<string, string>(), null },
};

Expand Down
Loading