feat: log AI coding agents in user-agent, telemetry, and tracing#550
Conversation
When the Slack CLI is invoked by an AI coding agent (Claude Code, Codex, Gemini CLI, Cline, Cursor, Goose, Amp, or others), detect the agent via environment variables and append an AI-Agent product token to the HTTP user-agent header. Also propagates the agent name to telemetry events and Jaeger tracing spans. User-Agent format: slack-cli/2.38.1 (os: darwin) AI-Agent (name=claude-code, entry=cli)
Removes duplicate detectAIAgentName/detectAgentName helpers from main.go and tracking.go in favor of useragent.DetectName().
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #550 +/- ##
==========================================
- Coverage 71.64% 71.62% -0.02%
==========================================
Files 225 226 +1
Lines 19074 19104 +30
==========================================
+ Hits 13665 13684 +19
- Misses 4202 4211 +9
- Partials 1207 1209 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
mwbrooks
left a comment
There was a problem hiding this comment.
Comments for the kind reviewers! 🖊️
| rootCmd.SetContext(ctx) | ||
| // Debug logging | ||
| clients.IO.PrintDebug(ctx, "system_id: %s", clients.Config.SystemID) | ||
| clients.IO.PrintDebug(ctx, "user_agent: %s", useragent.BuildUserAgent(version.Raw())) |
There was a problem hiding this comment.
note: This is a new addition to our verbose logs. For a while, I've wanted to see our user-agent which is used for each API request. This may also be helpful when diagnosing user reported issues.
| span.SetTag("hashed_hostname", ioutils.GetHostname()) | ||
| span.SetTag("slack_cli_process", processName) | ||
| if agentName := useragent.DetectName(); agentName != "" { | ||
| span.SetTag("ai_agent", agentName) |
There was a problem hiding this comment.
note: I chose ai_agent instead of agent because it's less ambiguous and more specific that it's an AI "agent".
|
|
||
| // EventContext contains information / metadata about the CLI session | ||
| type EventContext struct { | ||
| AIAgent string `json:"ai_agent,omitempty"` |
There was a problem hiding this comment.
note: I chose ai_agent instead of agent because it's less ambiguous and more specific that it's an AI "agent".
| ua := fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS) | ||
| if agent := Detect(); agent != nil { | ||
| var parts []string | ||
| parts = append(parts, "name="+agent.Name) |
There was a problem hiding this comment.
| parts = append(parts, "name="+agent.Name) | |
| parts = append(parts, "name: "+agent.Name) |
☎️ question: A ":" separator might match the os adjacent but I understand can make queries more difficult. If this was intentional please ignore but I did want to ask?
| // BuildUserAgent constructs the HTTP User-Agent header value for the CLI. If an | ||
| // AI agent is detected, an "AI-Agent (name=..., entry=...)" suffix is appended. |
There was a problem hiding this comment.
📚 quibble: It'd be helpful to have a full example here but no blocker!
|
|
||
| // DetectName returns the normalized name of the detected AI agent, or an empty | ||
| // string if no agent is detected. | ||
| func DetectName() string { |
There was a problem hiding this comment.
| func DetectName() string { | |
| func DetectHarness() string { |
🪬 question: Forgive these words but this is an unusual export for the value found I think but I also admit to not being so familiar with practices encouraged here!
There was a problem hiding this comment.
🌃 ramble: It does seem solid if we're framing this as the... actual user agent though? I'm understanding more I hope!
srtaalej
left a comment
There was a problem hiding this comment.
very cool addition ⭐ ⭐ ⭐
| if agent.Entry != "" { | ||
| parts = append(parts, "entry="+agent.Entry) | ||
| } | ||
| ua += " AI-Agent (" + strings.Join(parts, ", ") + ")" |
There was a problem hiding this comment.
| ua += " AI-Agent (" + strings.Join(parts, ", ") + ")" | |
| ua += " AI-Agent: (" + strings.Join(parts, ", ") + ")" |
could we have a : separator here to to match the user_agent also 🤔 if unintentional ^^ ofc
There was a problem hiding this comment.
Good suggestion @srtaalej - I see where you're coming from in that it may improve parsing. However, most user-agent strings would not add a colon there.
User-Agent strings are a loose standard, but the general format is Some-Name/<Version> (metadata...). Since we can't reliably get a version, I'm just going with the high-level "AI-Agent" and letting the metadata section describe the details. The metadata section is something (key1: value1, key2: value2, ...) and something (value1; value2...). We've adopted the key/value pair because it's easier to parse reliably.
Here's a mega list of User-Agents to give a sense of the messy world of UA:
https://gist.github.com/pzb/b4b6f57144aea7827ae4
Use "name: value" / "entry: value" to match the existing "(os: linux)" segment instead of "name=value" / "entry=value".
Show concrete User-Agent strings for both the no-agent and AI-agent-detected cases so readers can see the full assembled header at a glance.
…Name The package was renamed from agent to useragent and generalized to build the full User-Agent string. The old Detect / DetectName names were ambiguous in the new package context (detect what?). Rename to make their purpose explicit, and rename internal "agent" variables to "aiAgent" to avoid shadowing the broader user-agent concept.
Changelog
Summary
This pull request adds detection of AI coding agents and surfaces the detected agent across HTTP requests, telemetry, and tracing.
Format:
slack-cli/v4.0.1 (os: darwin) AI-Agent (name: claude-code, entry: cli)slack-cli/v4.0.1 (os: darwin) AI-Agent (name: claude-code, entry: desktop)slack-cli/v4.0.1 (os: darwin) AI-Agent (name: codex)Analytics:
name:.entry:) but it's very limited today.Detected AI agents environment variables:
CLAUDECODE→claude-codeCLAUDE_CODE_ENTRYPOINT→cliordesktopor possibly evenvscodeCODEX_CI→codexGEMINI_CLI→gemini-cligeminiif we want, but since the env var is specific the CLI we may want to take advantage of thatCLINE_ACTIVE→clineCURSOR_AGENT→cursorAGENTWhen detected:
AI-Agent (name:<name>, [entry:<entrypoint>])additionagentfield in the contextai_agentspan tagTesting
Requirements