diff --git a/apps/ui/src/compose-boot.test.ts b/apps/ui/src/compose-boot.test.ts index a6f9ef82..baf98af0 100644 --- a/apps/ui/src/compose-boot.test.ts +++ b/apps/ui/src/compose-boot.test.ts @@ -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", diff --git a/apps/ui/src/compose-boot.ts b/apps/ui/src/compose-boot.ts index a1fb7657..28de7aeb 100644 --- a/apps/ui/src/compose-boot.ts +++ b/apps/ui/src/compose-boot.ts @@ -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; diff --git a/apps/ui/src/index.tsx b/apps/ui/src/index.tsx index 8f57761b..26d05aa7 100644 --- a/apps/ui/src/index.tsx +++ b/apps/ui/src/index.tsx @@ -87,36 +87,12 @@ async function main( root: HTMLElement, connection: ResolvedConnection, ): Promise { - // 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));