Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion documentation/project-docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
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