From 1034189ceb58a63d654368181f28123e7f8fac13 Mon Sep 17 00:00:00 2001 From: xfodev Date: Sat, 25 Jul 2026 09:01:37 -0700 Subject: [PATCH] fix(selfhost): consume the injected central PostHog key as a POSTHOG_API_KEY fallback The hosted control-plane injects a fleet-wide LOOPOVER_CENTRAL_POSTHOG_KEY into each tenant container, but initPostHog() only ever read POSTHOG_API_KEY -- so the injected key was never consumed and hosted tenant containers reported to nothing. Resolve the key as POSTHOG_API_KEY (operator override, highest precedence, unchanged) falling back to LOOPOVER_CENTRAL_POSTHOG_KEY when POSTHOG_API_KEY is unset; when neither is set initPostHog stays a complete no-op (returns false), unchanged. Document the fallback in the module's self-host env-var header block. Consumer-side fix only; the control-plane injection side is already correct and untouched. Adds regression tests: the central key activates the client when POSTHOG_API_KEY is unset, POSTHOG_API_KEY still wins when both are set, and neither-set stays a no-op. Closes #8626 --- src/selfhost/posthog.ts | 17 ++++++++++++----- test/unit/selfhost-posthog.test.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/selfhost/posthog.ts b/src/selfhost/posthog.ts index 54558c812d..6af1ca1828 100644 --- a/src/selfhost/posthog.ts +++ b/src/selfhost/posthog.ts @@ -14,10 +14,12 @@ // // Env-var decision (#8287's own deliverable): POSTHOG_API_KEY/POSTHOG_HOST are the SAME vars #6235's MCP // telemetry (src/mcp/telemetry.ts) already reads off the typed Cloudflare Env -- one project key activates -// both surfaces, the MCP tool-call allowlist (#6228) is untouched. Everything else here (POSTHOG_MIN_SEVERITY, -// POSTHOG_REPO_MIN_SEVERITY, POSTHOG_ENVIRONMENT, POSTHOG_SERVER_NAME, POSTHOG_RELEASE) is self-host-only, -// read off real process.env, never added to src/env.d.ts's typed Env, matching that file's precedent for -// self-host-exclusive config. +// both surfaces, the MCP tool-call allowlist (#6228) is untouched. POSTHOG_API_KEY additionally falls back to +// LOOPOVER_CENTRAL_POSTHOG_KEY when unset (#8626) -- the fleet-wide key the hosted control-plane injects into a +// tenant container; operator-set POSTHOG_API_KEY keeps precedence, this is only a hosted-image fallback source. +// Everything else here (POSTHOG_MIN_SEVERITY, POSTHOG_REPO_MIN_SEVERITY, POSTHOG_ENVIRONMENT, POSTHOG_SERVER_NAME, +// POSTHOG_RELEASE, and LOOPOVER_CENTRAL_POSTHOG_KEY) is self-host-only, read off real process.env, never added to +// src/env.d.ts's typed Env, matching that file's precedent for self-host-exclusive config. import { randomUUID } from "node:crypto"; import { hostname } from "node:os"; import { @@ -133,7 +135,12 @@ function operationalProperties(context: Record | undefined): Re * the SAME var #6235's MCP telemetry reads (env-var decision, #8287). `env` is real process.env, matching * initSentry's identical NodeJS.ProcessEnv shape. */ export async function initPostHog(env: NodeJS.ProcessEnv): Promise { - const apiKey = processEnvString(env, "POSTHOG_API_KEY"); + // #8626: POSTHOG_API_KEY is the operator's own explicit config (highest precedence, unchanged); when it is + // unset, fall back to LOOPOVER_CENTRAL_POSTHOG_KEY -- the fleet-wide key the hosted control-plane injects into + // a tenant container (control-plane/src/container-driver.ts) so a hosted image reports to the loopover-owned + // project without the operator setting anything. A hosted-injected fallback, never a silent override of an + // operator's explicit POSTHOG_API_KEY. When neither is set, initPostHog stays a complete no-op (returns false). + const apiKey = processEnvString(env, "POSTHOG_API_KEY") ?? processEnvString(env, "LOOPOVER_CENTRAL_POSTHOG_KEY"); if (!apiKey) return false; await loadNodeHasher(); const { PostHog } = await import("posthog-node"); diff --git a/test/unit/selfhost-posthog.test.ts b/test/unit/selfhost-posthog.test.ts index 4072d3cbef..501919926b 100644 --- a/test/unit/selfhost-posthog.test.ts +++ b/test/unit/selfhost-posthog.test.ts @@ -77,6 +77,25 @@ describe("initPostHog", () => { expect(options.enableExceptionAutocapture).toBe(true); expect(options.before_send).toBe(scrubPostHogEvent); }); + + it("falls back to LOOPOVER_CENTRAL_POSTHOG_KEY when POSTHOG_API_KEY is unset (#8626)", async () => { + const enabled = await initPostHog({ LOOPOVER_CENTRAL_POSTHOG_KEY: "phc_central" } as unknown as NodeJS.ProcessEnv); + expect(enabled).toBe(true); + expect(mocks.PostHog).toHaveBeenCalledWith("phc_central", expect.objectContaining({ host: "https://us.i.posthog.com" })); + }); + + it("keeps POSTHOG_API_KEY's precedence when both it and LOOPOVER_CENTRAL_POSTHOG_KEY are set (#8626)", async () => { + const enabled = await initPostHog({ POSTHOG_API_KEY: "phc_operator", LOOPOVER_CENTRAL_POSTHOG_KEY: "phc_central" } as unknown as NodeJS.ProcessEnv); + expect(enabled).toBe(true); + expect(mocks.PostHog).toHaveBeenCalledWith("phc_operator", expect.anything()); + expect(mocks.PostHog).not.toHaveBeenCalledWith("phc_central", expect.anything()); + }); + + it("stays a no-op when neither POSTHOG_API_KEY nor LOOPOVER_CENTRAL_POSTHOG_KEY is set (#8626)", async () => { + const enabled = await initPostHog({} as unknown as NodeJS.ProcessEnv); + expect(enabled).toBe(false); + expect(mocks.PostHog).not.toHaveBeenCalled(); + }); }); describe("resolvePostHogRelease", () => {