feat(telemetry): send x-githits-* client headers on every API request - #24
Merged
Conversation
Emits four `x-githits-*` HTTP headers on every request (REST +
GraphQL) plus an MCP `clientInfo` hook so backend telemetry can
distinguish direct-CLI traffic from MCP-mode traffic, identify
the AI agent / IDE driving the CLI, and group requests by
terminal session — all without leaking raw identifiers.
## Four headers, two transports
| Header | Value |
|---|---|
| `x-githits-client-name` | `githits-cli` (direct CLI) / `githits-cli/mcp` (MCP server) |
| `x-githits-client-version` | Read from `package.json` |
| `x-githits-agent` | `name[/version]` — `GITHITS_AGENT` env, MCP clientInfo, or auto-detect |
| `x-githits-session-id` | SHA-256 truncated to 64 bits; cached per process |
Emitted on both:
- **REST** (`src/services/githits-service.ts`) — `/search`,
`/languages`, `/feedbacks` via the shared `headers()` builder.
- **GraphQL** (`src/shared/pkgseer-graphql.ts`) — spreads into the
fetch `headers` alongside `Authorization` / `Content-Type`.
## Agent detection (priority order)
1. `setAgentInfo()` — explicit runtime override.
2. MCP `clientInfo` via a lazy provider (when running as MCP
server). The MCP SDK sets `_clientVersion` synchronously
inside its `_oninitialize` handler before the initialize
response is returned, so every tool call arriving after the
handshake sees a populated value. Using a lazy provider
(rather than an `oninitialized` notification callback) avoids
the race where the first tool call can slip through before
the async notification dispatches.
3. Env-var auto-detect: `OPENCODE`, `CLAUDECODE`,
`CURSOR_TRACE_ID`, `WINDSURF_CONFIG_DIR`, `ZED_TERM`,
`VSCODE_PID` (first match wins).
4. Explicit `GITHITS_AGENT=name/version` env override —
overrides auto-detect but not `setAgentInfo`.
## Session-id detection
Most-specific terminal-session env var wins: `TERM_SESSION_ID`,
`ITERM_SESSION_ID`, `WEZTERM_PANE`, `KITTY_PID`,
`ALACRITTY_SOCKET`, `WT_SESSION`, `VSCODE_PID`, `SUPERSET_PANE_ID`,
`SUPERSET_WORKSPACE_ID`, `STARSHIP_SESSION_KEY`, `SSH_CONNECTION`.
Falls back to parent PID, then process PID, then a random UUID.
Raw value is SHA-256-hashed and truncated to 16 hex chars (64
bits) before ever touching the wire — raw session identifiers
never leave the client. Result is cached for the lifetime of
the process.
## Safety invariants
- Every header value passes through `sanitizeHeaderValue` —
strips C0 / C1 control chars, trims, enforces a 256-byte cap.
Blank or oversized values are **dropped** rather than
truncated to a half-value.
- `buildClientHeaders()` is wrapped in try/catch and returns
`{}` on any internal error — API requests never fail because
of a header-generation problem.
- Header builder is evaluated **per-request**, so values set
after client creation (`setAgentInfo`, `setMcpClientVersionProvider`)
reach subsequent requests.
- MCP `setMcpClientVersionProvider` callback is try/catch-
wrapped internally — SDK access failures never block a
request or crash the MCP session.
- Header spread order on both transports puts `x-githits-*`
first, then hardcoded `Authorization` / `Content-Type` /
`User-Agent`, so no hypothetical collision could drop the
hardcoded headers.
## Testing
- 71 new unit tests covering session-id resolution priority,
caching, agent detection + env-override, MCP clientInfo
provider (precedence, per-call invocation, throwing-provider
fallback, explicit-override win), header sanitisation, and
`buildClientHeaders` composition.
- Transport-level assertions: new tests in
`pkgseer-graphql.test.ts` and `githits-service.test.ts`
capture request headers and pin that `x-githits-*` headers
actually reach the wire — not just that the builder emits
them.
- MCP integration: `mcp.test.ts` asserts `startMcpServer`
flips clientMode to `githits-cli/mcp` and registers the
lazy MCP clientInfo provider. `afterEach` cleanup prevents
test pollution across files.
- Full suite: 1401 pass.
- Typecheck / lint / build clean.
## Live-smoke
Ran a mock-fetch harness against `postPkgseerGraphql` with
`GITHITS_AGENT=smoke/1.2.3` and `TERM_SESSION_ID=smoke-session-abc`;
confirmed on the wire:
```
x-githits-client-name: githits-cli
x-githits-client-version: 0.1.9
x-githits-agent: smoke/1.2.3
x-githits-session-id: 0ba8a49b8a7d3106
User-Agent: githits-cli/0.1.9
```
jlitola
force-pushed
the
feat/telemetry-headers
branch
from
April 21, 2026 17:38
82387c3 to
b0076c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Emits four
x-githits-*HTTP headers on every API request (REST + GraphQL) plus an MCPclientInfohook. Gives the backend a reliable signal for distinguishing direct-CLI usage from MCP-mode usage, tracking which AI agents / IDEs drive the CLI, and grouping requests by terminal session — all without leaking raw identifiers.Four headers, two transport layers
x-githits-client-namegithits-cli(direct CLI) /githits-cli/mcp(MCP server mode)x-githits-client-versionpackage.json(0.1.9)x-githits-agentname[/version]— fromGITHITS_AGENTenv, MCPclientInfo, or auto-detectx-githits-session-idEmitted on both:
GitHitsService/search, /languages, /feedbacks via the sharedheaders()builder.postPkgseerGraphqlspreads into the fetchheadersalongsideAuthorization/Content-Type.Agent detection (priority order)
setAgentInfo()— explicit runtime override.clientInfovia a lazy provider (when running as MCP server). The MCP SDK sets_clientVersionsynchronously inside_oninitializebefore the initialize response is returned, so every tool call arriving after the handshake sees a populated value. Using a lazy provider — rather than anoninitializednotification callback — avoids the race where the first tool call can slip through before the async notification dispatches.OPENCODE,CLAUDECODE,CURSOR_TRACE_ID,WINDSURF_CONFIG_DIR,ZED_TERM,VSCODE_PID(first match wins).GITHITS_AGENT=name/versionenv — overrides auto-detect but notsetAgentInfo.Session-id detection
Most-specific terminal-session env var wins:
TERM_SESSION_ID,ITERM_SESSION_ID,WEZTERM_PANE,KITTY_PID,ALACRITTY_SOCKET,WT_SESSION,VSCODE_PID,SUPERSET_PANE_ID,SUPERSET_WORKSPACE_ID,STARSHIP_SESSION_KEY,SSH_CONNECTION→ parent PID → process PID → random UUID. Raw value is hashed (SHA-256, truncated to 16 hex chars / 64 bits) before ever touching the wire.Safety invariants
sanitizeHeaderValue— strips C0 / C1 control chars, trims, enforces a 256-byte cap. Blank or oversized values are dropped rather than truncated to a half-value.buildClientHeaders()istry/catch-wrapped around the full body and returns{}on any internal error — API requests never fail because of a header-generation problem.setAgentInfo,setMcpClientVersionProvider) reach subsequent requests.try/catch-wrapped internally — SDK access failures degrade to env detection and never crash the MCP session.x-githits-*first, then hardcodedAuthorization/Content-Type/User-Agent, so no hypothetical collision could drop the hardcoded headers.Testing
buildClientHeaderscomposition.pkgseer-graphql.test.tsandgithits-service.test.tscapture request headers and pin thatx-githits-*values actually land on the wire, not just that the builder emits them.mcp.test.tsassertsstartMcpServerflipsclientModetogithits-cli/mcpand registers the lazy MCP clientInfo provider.afterEachcleanup prevents test-state pollution across files.Live-smoke
Ran a mock-fetch harness against
postPkgseerGraphqlwith:Confirmed on the wire:
Test plan
bun testpasses (1401 / 0)bun run typecheck/bun run build/bun run lintcleanbuildClientHeaders()x-githits-*on the wire, not just builder output🤖 Generated with Claude Code