Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions apps/ui/src/compose-boot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,9 @@ import { composeBoot } from "./compose-boot";
import type { LiveClients } from "./live/client";
import type { ResolvedConnection } from "./live/provider";

// composeBoot must build analytics BEFORE the clients: the analytics `traceId`
// getter is a forward reference to `clients`, safe only because it fires at
// capture time — after the clients bind. A wrong order still typechecks and
// renders; it only fails at the network door as a missing X-POSTHOG-SESSION-ID
// header, so the order is untestable in production. These fakes record the
// observable consequences of the order and the two lazy getters.
// A wrong order still typechecks and renders; it fails only at the network door
// as a missing X-POSTHOG-SESSION-ID header. These fakes record the order and
// both lazy getters, which is the only place that defect is observable.

const connection: ResolvedConnection = {
baseUrl: "https://compass.example:8443",
Expand Down
9 changes: 3 additions & 6 deletions apps/ui/src/compose-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ import {
import { createLiveClients, type LiveClients } from "./live/client";
import type { ResolvedConnection } from "./live/provider";

// The analytics+clients construction pair, lifted out of `index.tsx main()`
// behind injectable factories so the boot ORDER is testable. Production only
// fails a wrong order at the network door (a missing X-POSTHOG-SESSION-ID
// header), so recording fakes substituted here are the sole way to pin it — and
// this module is importable without the App/mount render graph that index.tsx
// drags in. The order and the lazy forward reference are load-bearing.
// A wrong boot order fails only at the network door (a missing
// X-POSTHOG-SESSION-ID header), so injectable factories are the sole way to
// pin it — and unlike index.tsx, this module imports without the render graph.
export interface ComposeBootDeps {
connection: ResolvedConnection;
createAnalytics?: typeof createAnalytics;
Expand Down
36 changes: 6 additions & 30 deletions apps/ui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,36 +87,12 @@ async function main(
root: HTMLElement,
connection: ResolvedConnection,
): Promise<void> {
// Product analytics, OFF by default: analyticsConfigFromEnv returns undefined
// unless a PostHog project key is configured, and createAnalytics then hands
// back a no-op that never touches posthog — an unconfigured deployment emits
// zero analytics.
//
// Built FIRST, before the clients, because correlation now runs in both
// directions and the outbound half needs a real analytics object to read
// from. Both directions are lazy getters, and they point opposite ways:
//
// inbound `clients.traceId` → analytics: the transport records each
// reply's server trace id into that slot, and analytics reads it
// at capture time. `clients` is a forward reference from inside
// this getter, which is safe because the getter only runs once
// an event is captured — long after the next statement binds it.
// outbound `analytics.sessionId()` → the transport: every request asks
// for the current PostHog session id and sends it as
// X-POSTHOG-SESSION-ID, so backend spans carry the same session
// the frontend recorded.
//
// The inbound half is best-effort by construction, on two counts. The slot
// holds the LAST reply's trace id, so an event fired before any call has
// returned carries nothing, and one fired between calls carries the previous
// call's trace rather than its own. And the server sets `traceresponse` only
// on UNARY replies, and only when an OTel provider is installed — an
// unconfigured deployment (empty exporter endpoint ⇒ no span ⇒ no header)
// stamps nothing at all.
//
// The outbound half is best-effort too: the getter returns undefined until a
// PostHog session exists, and the interceptor then sends no header and
// self-heals on the next request. Only the TLS network door reads the header.
// Product analytics, OFF by default: an unconfigured deployment (no PostHog
// project key) gets a no-op that never touches posthog. Correlation runs both
// ways and is best-effort in both: the inbound trace id is the LAST reply's
// (and absent entirely without an OTel provider), and the outbound session id
// is absent until a PostHog session exists. See composeBoot for why the
// construction order is load-bearing.
const { analytics, clients } = composeBoot({ connection });

const callerId = await bootCaller(root, () => resolveCaller(clients.compass));
Expand Down
Loading