Repository navigation
Add X-Gusto-CLI-Command request header per command - #166
Conversation
Stamp every Gusto API request with an X-Gusto-CLI-Command header carrying a slug of the command that issued it (e.g. employee-list, api-request), so request-log analytics can break CLI traffic down by command. Both structured commands and the raw `api request` escape hatch now carry it. Threaded via GlobalFlags -> buildApiClient -> ApiClient with no handler or helper signature changes: the runner injects the dispatched command onto a fresh globals copy, buildApiClient slugs it, and the client sets the header alongside the existing Authorization/Accept/version/User-Agent headers. The header rides every attempt, including a 404'd request. Surfaces with no command context (OAuth login) send nothing. Signed-off-by: Aaron Rosen <aaron.rosen@gusto.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The feedback command and any callMcpTool request build their client via the same buildApiClient path as the REST surfaces, so they carry X-Gusto-CLI-Command too. This is intended: the header attributes each API request to the command that issued it, and feedback is a real command. Add a focused unit test that stubs the transport, drives callMcpTool with a feedback command context, and asserts the outbound request carries the `feedback` slug. Also reflow a runner test line to satisfy the formatter. Signed-off-by: Aaron Rosen <aaron.rosen@gusto.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…header Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com> # Conflicts: # src/lib/api-client.ts # src/lib/api-context.ts
…header Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com> # Conflicts: # src/lib/api-client.ts
tmfahey
left a comment
There was a problem hiding this comment.
Threading it through GlobalFlags is the right call. buildApiClient is only reached via resolveApiContext and the fetch*/create*/put* helpers, all of which take just globals, so the alternative means widening every one of them to carry a string. Slug derivation, the per-attempt header build, and the copy-not-mutate in the runner all check out, and I couldn't get an argument or UUID into the slug from any command literal.
Two things I'd settle before this merges: whether GUSTO_TELEMETRY=0 should suppress this header the way it suppresses the install id, and the doc comment claiming OAuth login has no command context - it does. Both inline, along with some smaller test and doc notes.
Separately, and not about the code: the description carries an internal ticket key, and so does the branch name. CLAUDE.md:71 asks to keep those out of anything public. The description is the part that's still editable.
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
tmfahey
left a comment
There was a problem hiding this comment.
Nice threading - putting command on a fresh globals copy in the runner keeps every handler and helper signature untouched, and both new ApiClient sites pick it up.
One gap worth closing before merge: command rides on OAuthHttpOptions but only the bearer client reads it, so the raw /v1/mcp/oauth/token POSTs - including every session refresh a REST command triggers - still go out unattributed. Details inline, plus one test-placement nit.
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
tmfahey
left a comment
There was a problem hiding this comment.
Two notes that are more about the write-up than the code.
The summary's third bullet now undersells the change. It says the OAuth login token_info request gets attributed, but 4c3336e pushed the header into withInstallIdHeader, so the DCR register, the code exchange, and every mid-command token refresh carry it too. The refresh case is the one that'll dominate the volume, and it's worth having in the body so a reader can size the change from the description.
Second, GUSTO_TELEMETRY still doesn't appear in any markdown in the repo - not README, not ARCHITECTURE, not --help. fe484eb deferred that to a follow-up when the install id shipped, which was a fair call for an opaque UUID. A command name is a more legible signal than a UUID, so this feels like the change that should carry the README section, or at least land behind the follow-up rather than ahead of it.
Otherwise the threading looks right: the runner is the single injection point, every Gusto-API-bound client picks the slug up from there, and the non-runner surfaces (auto-update, upgrade) make no Gusto API calls, so there's no unattributed traffic left over.
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
Summary
X-Gusto-CLI-Command: <slug>header to CLI API requests so request traffic can be grouped by command.GlobalFlagswithout widening every API helper signature.GUSTO_TELEMETRYopt-out across regular API traffic and OAuth registration, code exchange, token refresh, and token inspection requests.Linked issue
No public issue.
Test plan
env NO_COLOR= bun run test:allpasses locallybun run typecheckpasses locallybun run lintpasses locallybun run format:checkpasses locallyDCO
git commit -s) per the DCO