feat(http): send User-Agent header on swamp-club requests#1477
Conversation
Tag outbound swamp-club traffic with a `User-Agent: swamp-cli/<version>` header so the server can attribute requests by client version. The header is injected at the identity composition root: `loadIdentity()` now sets a `userAgent` field on `ClientIdentity` (built from the CLI `VERSION` constant and exported as `USER_AGENT`), and `mergeIdentityHeaders()` emits it. This covers both swamp-club API clients (`SwampClubClient` and `ExtensionApiClient`) with a single change, with caller-supplied headers still taking precedence. The telemetry sender (`telemetry.swamp-club.com`) builds its requests outside the `ClientIdentity` path, so it gains an optional `userAgent` constructor arg wired from `USER_AGENT` at its construction sites. The update checker (artifacts CDN) and source downloader (GitHub) are left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
None found.
Low
-
src/infrastructure/telemetry/http_telemetry_sender.ts:62— Whenthis.userAgentis set, theUser-Agentheader is included via object spread into aRecord<string, string>and passed tofetch. Deno'sfetchdoes honor customUser-Agentheaders (unlike browsers), but if Deno's behavior ever changes to append or ignore a customUser-Agent(as browsers do per the Fetch spec), this would silently stop working with no indication. ThemergeIdentityHeaderspath usesHeaders.set()which is the canonical approach. Not actionable today — just noting the difference in mechanism between the two code paths. -
src/cli/load_identity.ts:26—USER_AGENTis module-level, so it capturesVERSIONat import time. This is correct for a CLI (process starts, imports, runs, exits), but worth noting: if this module were ever imported in a long-lived process whereVERSIONcould theoretically change (it can't — it's aconst), the value would be stale. Purely theoretical;VERSIONis a compile-time constant.
Verdict
PASS — Clean, well-scoped change. The mergeIdentityHeaders approach correctly injects User-Agent for both SwampClubClient and ExtensionApiClient with a single code path. Caller-header-wins precedence is preserved (identity headers set first, caller headers override via Headers.set). The telemetry sender gets its own parallel path since it bypasses ClientIdentity, and the constructor arg is optional so existing test call sites are unaffected. All production construction sites in mod.ts pass USER_AGENT. Test coverage is thorough — including the caller-override case which validates the precedence contract.
There was a problem hiding this comment.
Code Review
Clean, well-scoped PR. The composition-root injection pattern for User-Agent is the right approach — it covers both swamp-club API clients via mergeIdentityHeaders with a single change, and handles the telemetry sender's separate request path via constructor injection.
Blocking Issues
None.
Suggestions
None — the PR is well-structured:
- DDD alignment:
ClientIdentityis a proper value object; addinguserAgentas an optional field is consistent. The composition root (loadIdentity) assembles identity from infrastructure concerns and passes it down — textbook dependency inversion. - Test coverage: Thorough —
client_identity_test.tscovers set/omit/combined/caller-override cases;load_identity_test.tsverifies the format and thatuserAgentsurvives the failure path;http_telemetry_sender_test.tsverifies the header hits the wire. - Import boundaries: No libswamp boundary violations.
ClientIdentitylives insrc/infrastructure/, so direct CLI imports are correct per project conventions. - Security: User-Agent only reveals CLI version — appropriate for traffic attribution.
- Precedence: Caller headers override identity headers (tested), preserving the existing contract documented in
mergeIdentityHeaders.
Summary
Adds a
User-Agent: swamp-cli/<version>header to outbound requests to swamp-club so the server can attribute traffic by client version.The header is injected at the identity composition root rather than at each call site:
loadIdentity()now sets auserAgentfield onClientIdentity, built from the CLIVERSIONconstant and exported asUSER_AGENT.mergeIdentityHeaders()emits it, so both swamp-club API clients (SwampClubClientandExtensionApiClient) are covered by one change. Caller-supplied headers still take precedence on conflict.telemetry.swamp-club.com/ingest) builds requests outside theClientIdentitypath, so it gains an optionaluserAgentconstructor arg wired fromUSER_AGENTat its construction sites inmod.ts.The version comes from the
VERSIONconstant thatscripts/compile.tsstamps at release-build time, so released binaries report a real version (dev builds report the source default,swamp-cli/<date>.<time>.0-sha.).Deliberately out of scope
artifacts.swamp-club.com) — anonymous static CDN downloads, no identity path.Test Plan
client_identity_test.ts: header set / omitted / combined with other identity headers / caller override.load_identity_test.ts:USER_AGENTformat and that it's always present even on the no-config-dir fallback path.http_telemetry_sender_test.tscase:User-Agentsent when provided.User-Agentoverride onfetch(it does).deno fmt --check,deno lint,deno run test(6517 passed) all green.🤖 Generated with Claude Code