docs(design): emit the PostHog session id on outbound requests (RIG-2874) - #1040
Open
rigel-mintaka wants to merge 1 commit into
Open
docs(design): emit the PostHog session id on outbound requests (RIG-2874)#1040rigel-mintaka wants to merge 1 commit into
rigel-mintaka wants to merge 1 commit into
Conversation
|
Merging to
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 |
|
Compass engineering docs preview: https://compass-ui-rig-2874-outbound.compass-eng-docs.pages.dev Deployed from Changed pages: |
rigel-mintaka
force-pushed
the
compass-ui/rig-2874-outbound-session-header
branch
2 times, most recently
from
September 9, 2026 06:45
c9f0cd8 to
4a725b2
Compare
…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
force-pushed
the
compass-ui/rig-2874-outbound-session-header
branch
from
September 9, 2026 07:13
4a725b2 to
4737664
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.
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-IDcarry semconvsession.idand aproduct funnel can pivot to a backend trace.
Docs-only: 2 files,
+497on a new record and+2ledger rows. No code.This supersedes a hold you ruled
You held this on 2026-09-05 (RIG-3233) for two reasons. Both are discharged:
69669259, verifiedan ancestor of main).
X-POSTHOG-SESSION-IDundergo/went 0 → 4 files.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 transportread 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/uiwas 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. Butreq.headeris a fetchHeaders, andHeaders.settakes a ByteString.Measured directly in Bun:
sess-é(U+00E9)sess-日本語/ emoji / lone surrogateTypeErrorSo 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+fetchround-trip,égoes on the wire as the UTF-8 pairc3 a9(never the Latin-1e9) and arrives assess-é— valid UTF-8 atevery hop, so the server accepts it and stamps a
session.idthat joins tonothing. 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.setplus a readback observes myown 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, soTextEncoderand the well-formedness check both disappear, and it additionallyrejects 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
traceIdgetter becomesa 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 callsclient.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 ananonymous 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
NewSessionIDInterceptoris installed on one of three doors —network_door.go:300,:308only. The socket and dev doors installNewTraceResponseInterceptor(4 hits inserve.go) but not the session reader,though the dev door's CORS allows the header. Consequence: a dev-mode smoke
test shows no
session.idand looks exactly like a broken UI half. Verifyagainst 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-IDonly.X-POSTHOG-DISTINCT-IDis 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
TracingHeadersextension is rejected for the same reason — it monkey-patchesglobal fetch/XHR and sends the distinct-id header.
Gates
bun tools/design-ledger-gate/index.ts—OK — 310 ledger row(s), 131 record header(s) validmoon run root:markdownlint— 209 files, 0 errorsBased on
main5f7f25f0. Note: ledger rows are DL-346/347, but main's max isDL-343 — PR #984 carries DL-344/345, so if #984 merges first these renumber.
RIG-2874