Skip to content

Add X-Gusto-CLI-Command request header per command - #166

Merged
jeff-gusto merged 9 commits into
mainfrom
aint-805-cli-command-header
Sep 16, 2026
Merged

jeff-gusto merged 9 commits into
mainfrom
aint-805-cli-command-header

Conversation

@azrosen92

@azrosen92 azrosen92 commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds an X-Gusto-CLI-Command: <slug> header to CLI API requests so request traffic can be grouped by command.
  • Threads the dispatched command through GlobalFlags without widening every API helper signature.
  • Honors the GUSTO_TELEMETRY opt-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:all passes locally
  • bun run typecheck passes locally
  • bun run lint passes locally
  • bun run format:check passes locally
  • Header propagation, telemetry opt-out, OAuth request attribution, MCP attribution, and near-expiry session refresh have regression coverage

DCO

  • Every commit is signed off (git commit -s) per the DCO

azrosen92 and others added 2 commits August 18, 2026 15:23
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>
@azrosen92
azrosen92 requested review from a team and ashieh as code owners August 18, 2026 21:52
jeff-gusto
jeff-gusto previously approved these changes Aug 18, 2026
…header

Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>

# Conflicts:
#	src/lib/api-client.ts
#	src/lib/api-context.ts
@jeff-gusto
jeff-gusto requested a review from a team September 8, 2026 21:46
…header

Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>

# Conflicts:
#	src/lib/api-client.ts
@tmfahey
tmfahey self-requested a review September 9, 2026 00:07

@tmfahey tmfahey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/api-context.ts Outdated
Comment thread src/lib/api-client.ts Outdated
Comment thread src/lib/mcp.test.ts Outdated
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
@jeff-gusto
jeff-gusto requested a review from tmfahey September 9, 2026 20:35
tmfahey
tmfahey previously approved these changes Sep 9, 2026

@tmfahey tmfahey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/oauth/endpoints.ts
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
@jeff-gusto
jeff-gusto requested review from a team and tmfahey September 11, 2026 18:18

@tmfahey tmfahey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/oauth/context.ts Outdated
Comment thread src/lib/oauth/endpoints.test.ts
Signed-off-by: Jeff Stephens <jeff.stephens@gusto.com>
@tmfahey
tmfahey self-requested a review September 14, 2026 20:55

@tmfahey tmfahey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeff-gusto
jeff-gusto merged commit cdbd70e into main Sep 16, 2026
24 checks passed
@jeff-gusto
jeff-gusto deleted the aint-805-cli-command-header branch September 16, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants