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", () => {