Skip to content

feat(ui): emit the PostHog session id on outbound requests (RIG-2874) - #1055

Open
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-ui/rig-2874-outbound-impl
Open

feat(ui): emit the PostHog session id on outbound requests (RIG-2874)#1055
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-ui/rig-2874-outbound-impl

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Implements RIG-2874's outbound half: the UI stamps X-POSTHOG-SESSION-ID on
outbound gRPC-Web requests, so a PostHog session correlates with the server-side
traces it produced. The server already reads this header (#996, squash
69669259); this closes the loop.

Executes the design record frozen in #1040 — tasks T1-T5. Stacked on that
bookmark rather than on main, so it did not wait on the merge queue.

What ships

  • packages/compass-clientsessionIdInterceptor plus
    isSendableSessionId: a printable-ASCII guard (/^[\x21-\x7E]+$/) capped at
    200 characters. The guard runs before header.set, not after, because
    Headers.set is a WebIDL ByteString: a well-formed value above U+00FF
    throws inside the interceptor and would kill the RPC rather than merely
    lose a correlation key.
  • callInterceptors — the early return on a missing traceSink
    (if (!traceSink) return bearer;) is replaced by an accumulating list. Under
    the old shape an appended interceptor was silently skipped whenever no trace
    sink was configured.
  • Analytics.sessionId() — re-read per request, never memoized: PostHog
    rotates the session id on idle and at max length, so a cached value goes
    stale.
  • Boot reordercreateAnalytics now runs before createLiveClients,
    which deletes the ordering problem instead of working around it (no forward
    let, no undefined window to guard).

The four per-client factories deliberately do not grow a sessionId
option; the shipped path is createLiveClientscreateCompassWebTransport.
That omission is documented at the call site with the audit that justifies it.

Guard exactness

Verified by exhaustive measurement, not sampling. For all 94 characters in
[0x21,0x7E]: Headers.set never throws, and .length equals the UTF-8 byte
length — so the client's UTF-16 count and the server's len() coincide on
exactly the domain where the cap is applied, and && short-circuiting means a
non-ASCII value never reaches .length at all.

Guard-accepted is a strict subset of server-accepted
(TrimSpace != "" && len <= 200 && utf8.ValidString, interceptor.go:96-98),
so there is no false reject. It is deliberately narrower than Headers.set,
which accepts space, tab, and DEL.

Verification

Gate Result
packages/compass-client suite 71 pass / 0 fail
analytics / client suites 16 pass / 6 pass
bun test packages 754 pass / 0 fail
bun test apps/ui/src 536 pass / 242 fail
Baseline, clean main 530 pass / 242 fail
tsc --noEmit (both projects) rc=0
moon run root:lint rc=0 (39 pre-existing devenv-cli warnings)
moon run root:markdownlint rc=0
design-ledger-gate OK — 310 rows, 131 headers valid

The 242 apps/ui failures are pre-existing: measured on a throwaway commit
off clean main before attributing anything. This change is +6 passing, zero
new failures.

Mutation-tested, seven mutants, all caught

Green tests are not evidence a suite can fail, so each guarantee was checked by
breaking it:

Mutant Result
Drop the id !== undefined check CAUGHT
Widen guard to admit SPACE (0x20) CAUGHT
Length cap <=< CAUGHT
Neutralize the ASCII class entirely CAUGHT (10 fail)
Corrupt the wire header name CAUGHT
Swap interceptor composition order CAUGHT
Memoize sessionId() CAUGHT

Two of these survived the first implementation and were found by the review
agent. The undefined one was not cosmetic: a bare
SENDABLE.test(undefined) coerces to the string "undefined" and passes,
so evaluation reaches .length on undefined and throws — meaning the
analytics-off path, which is the deployment default, would have failed every
request. It went unnoticed because the case sat on the capturing seam, whose
rejects.toThrow() gate is satisfied by the interceptor's own crash. Both cases
moved to the direct seam, which asserts the awaited result is a sentinel and
therefore cannot pass if anything threw.

Review

One review pass: 0 high, 3 medium, 4 low — all addressed. The mediums were the
two surviving mutants plus unpinned composition order. The lows were stale
comments: three separate places still gave the old boot order as the
rationale for the trace-id sink, one of them claiming the slot avoided
"reordering boot" — which this change does. All three now state the surviving
reason (the trace id arrives on a reply, so there is nothing to hand over at
construction time whatever order boot runs in).

Open items for review

  1. T4's acceptance is manual. The boot reorder has no automated coverage —
    the record says so explicitly. It needs a boot through the TLS network
    door
    ; the dev door produces a false negative indistinguishable from a
    broken UI half, and the visual smoke passes whether the reorder is right,
    wrong, or absent.
  2. A record defect worth a follow-up. The record assigns the
    getter returns undefined case to the capture seam
    (design.md:582). That is unsafe for this implementation, since the guard
    can throw on undefined. The code is correct; only the prescribed test
    placement was wrong. The record's own rule — never route a would-throw value
    onto the capture seam — is right and now applied.
  3. One genuine choice left unmade: whether to export a composeBoot(deps)
    seam purely so the boot order becomes testable. The record marks this as
    Matt's call, so it is raised rather than decided.

RIG-2874

@linear-code

linear-code Bot commented Sep 9, 2026

Copy link
Copy Markdown

RIG-2874

@trunk-io

trunk-io Bot commented Sep 9, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@rigel-mintaka rigel-mintaka changed the title docs(design): emit the PostHog session id on outbound requests (RIG-2874) feat(ui): emit the PostHog session id on outbound requests (RIG-2874) Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-ui-rig-2874-outbound-9mv2.compass-eng-docs.pages.dev

Deployed from compass-ui/rig-2874-outbound-impl at 9a2137b.

@rigel-mintaka
rigel-mintaka force-pushed the compass-ui/rig-2874-outbound-impl branch from 53b42c4 to 9a2137b Compare September 9, 2026 22:04
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.

1 participant