Skip to content

docs(design): emit the PostHog session id on outbound requests (RIG-2874) - #1040

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

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

Conversation

@rigel-mintaka

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

Copy link
Copy Markdown
Contributor

Design record for the UI half of the J1 correlation-key seam (RIG-2874 slice 2):
put the PostHog session id on outbound Connect requests, so the backend spans
that already read X-POSTHOG-SESSION-ID carry semconv session.id and a
product funnel can pivot to a backend trace.

Docs-only: 2 files, +497 on a new record and +2 ledger rows. No code.

This supersedes a hold you ruled

You held this on 2026-09-05 (RIG-3233) for two reasons. Both are discharged:

  1. No inbound consumer — compass-server's half merged (69669259, verified
    an ancestor of main). X-POSTHOG-SESSION-ID under go/ went 0 → 4 files.
  2. Boot ordering would need a lazy getter over a not-yet-existing analytics
    handle
    — dissolved by your own question. I claimed the boot order was
    "forced"; it is not. createAnalytics(config, deps?) takes only a config
    (analytics.ts:127-130), so building analytics first lets the transport
    read a real object. No mutable slot, no forward let.

I had filed RIG-3233 myself and then nearly rebuilt the thing it forbade,
because a gap and a decision look identical in the source tree — the only hit
in apps/ui was my own comment recording your ruling.

What the review caught that I would have shipped

The record went through a red-team pass; two blocking findings, both real.

The wire seam is a WebIDL ByteString, not a UTF-8 string. The draft
validated ≤200 UTF-8 bytes + well-formedness, mirroring the server. But
req.header is a fetch Headers, and Headers.set takes a ByteString.
Measured directly in Bun:

Value Result
ASCII UUID OK
sess-é (U+00E9) OK — arrives mangled but valid UTF-8, so the server accepts a wrong id
sess-日本語 / emoji / lone surrogate throws TypeError

So a well-formed non-ASCII id passes both draft checks and then throws
inside the interceptor, failing the entire RPC. An analytics nicety that can
kill every request is strictly worse than the silent server-side drop the
validation existed to prevent.

The Latin-1 row is a correction the review forced: I had recorded that value
as dropped by the server as invalid UTF-8. It is not. Measured with a real
Bun.serve + fetch round-trip, é goes on the wire as the UTF-8 pair
c3 a9 (never the Latin-1 e9) and arrives as sess-é — valid UTF-8 at
every hop, so the server accepts it and stamps a session.id that joins to
nothing. A silently wrong correlation key is harder to notice than a missing
one, so my false claim understated the hazard while arguing for the right fix.
The error was instrument placement: Headers.set plus a readback observes my
own object, never the wire.

Fixed by rejecting anything non-ASCII — /^[\x21-\x7E]+$/ plus a 200-char cap
— which is both simpler and stronger. Verified by execution: everything the
guard accepts is header.set-safe with .length === utf8 bytes, so
TextEncoder and the well-formedness check both disappear, and it additionally
rejects CRLF injection and whitespace-only values that the UTF-8 approach
accepted (a whitespace id would have emitted the empty header the record
forbids).

An inverted reference direction. The draft said the traceId getter becomes
a backward reference after the reorder. It becomes a forward one — and that
is why its laziness is load-bearing rather than tidy. As written it contradicted
the record's own TDZ-based rejection of the alternative.

One trade, stated rather than buried

PostHogAnalytics's constructor calls client.init(...) (analytics.ts:66-73),
which is network-bearing. Moving it earlier means that on the WhoAmI-failure
early return (index.tsx:96-97), an analytics-enabled deployment now emits an
anonymous PostHog session where it previously emitted nothing. Nothing is
captured, and it is unavoidable while the transport needs the getter at
construction — but it is a real behaviour change, so the record says so.

Scope limit worth your eye

NewSessionIDInterceptor is installed on one of three doors
network_door.go:300,:308 only. The socket and dev doors install
NewTraceResponseInterceptor (4 hits in serve.go) but not the session reader,
though the dev door's CORS allows the header. Consequence: a dev-mode smoke
test shows no session.id and looks exactly like a broken UI half.
Verify
against the network door. Whether the other doors should read it is
compass-server's call — filed as Open Question 5, not assumed.

Contract

X-POSTHOG-SESSION-ID only. X-POSTHOG-DISTINCT-ID is permanently excluded:
it identifies a person, and backend spans land in Grafana/Tempo, the plane J1
keeps identity out of. Settled with compass-server. posthog-js's own
TracingHeaders extension is rejected for the same reason — it monkey-patches
global fetch/XHR and sends the distinct-id header.

Gates

  • bun tools/design-ledger-gate/index.tsOK — 310 ledger row(s), 131 record header(s) valid
  • moon run root:markdownlint — 209 files, 0 errors

Based on main 5f7f25f0. Note: ledger rows are DL-346/347, but main's max is
DL-343 — PR #984 carries DL-344/345, so if #984 merges first these renumber.

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

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

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

Deployed from compass-ui/rig-2874-outbound-session-header at 4737664.

Changed pages:

@rigel-mintaka
rigel-mintaka force-pushed the compass-ui/rig-2874-outbound-session-header branch 2 times, most recently from c9f0cd8 to 4a725b2 Compare September 9, 2026 06:45
…874)

Design record for the UI half of the J1 correlation-key seam: a
sessionIdInterceptor in @compass/client puts the PostHog session id on
outbound Connect requests, so the backend spans that already read
X-POSTHOG-SESSION-ID carry semconv session.id.

Supersedes the 2026-09-05 hold (RIG-3233). Both of its reasons are
discharged: the inbound consumer merged (#996), and the boot-ordering
objection is deleted by constructing analytics before the live clients
rather than worked around with a mutable slot.

The sender-side guard is printable ASCII plus a 200-char cap, which is
stricter than the two limits the server enforces. Headers.set takes a
WebIDL ByteString, so a well-formed id above U+00FF throws inside the
interceptor and would fail the whole RPC, and U+0080-U+00FF rides as
invalid-UTF-8 Latin-1 that the server silently drops.

X-POSTHOG-DISTINCT-ID stays excluded: it identifies a person, and
backend spans land in the trace store J1 keeps identity out of.

Ledger: DL-346, DL-347.

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-ui/rig-2874-outbound-session-header branch from 4a725b2 to 4737664 Compare September 9, 2026 07:13
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