From 892cc2bc075e93d91f186c7abb718cb4214077f9 Mon Sep 17 00:00:00 2001 From: "Pablo Fernandez Guerra (PFE)" <148432447+pfe-nazaries@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:26:33 +0200 Subject: [PATCH] refactor(ui): rename reserved billing flag to CLOUD_BILLING_ENABLED (#11920) Co-authored-by: Pablo F.G --- ui/lib/env.test.ts | 27 +++++++++++++++++++ ui/lib/env.ts | 15 ++++++++++- ui/lib/get-runtime-config.client.test.ts | 6 ++--- ui/lib/get-runtime-config.client.ts | 2 +- ui/lib/runtime-config.shared.ts | 4 +-- ui/lib/runtime-config.ts | 8 ++++-- .../runtime-config/runtime-config-page.ts | 2 +- ui/types/env.d.ts | 2 +- 8 files changed, 55 insertions(+), 11 deletions(-) diff --git a/ui/lib/env.test.ts b/ui/lib/env.test.ts index 3c696a8baf..637ace023d 100644 --- a/ui/lib/env.test.ts +++ b/ui/lib/env.test.ts @@ -62,6 +62,7 @@ describe("lib/env gated integration validation", () => { "POSTHOG_KEY", "UI_POSTHOG_HOST", "POSTHOG_HOST", + "CLOUD_BILLING_ENABLED", ] as const; beforeEach(() => { @@ -141,4 +142,30 @@ describe("lib/env gated integration validation", () => { await expect(import("@/lib/env")).rejects.toThrow("POSTHOG_HOST"); }); + + it("throws when CLOUD_BILLING_ENABLED is metronome but PostHog is not enabled", async () => { + // metronome billing routes per tenant via the BILLING_SYSTEM_METRONOME + // PostHog flag, so PostHog must be enabled. + vi.stubEnv("CLOUD_BILLING_ENABLED", "metronome"); + + await expect(import("@/lib/env")).rejects.toThrow( + "PostHog is required for per-tenant billing routing", + ); + }); + + it("resolves when CLOUD_BILLING_ENABLED is metronome and PostHog is enabled", async () => { + vi.stubEnv("CLOUD_BILLING_ENABLED", "metronome"); + vi.stubEnv("UI_POSTHOG_ENABLE", "true"); + vi.stubEnv("UI_POSTHOG_KEY", "phc_key"); + vi.stubEnv("UI_POSTHOG_HOST", "https://eu.i.posthog.com"); + + await expect(import("@/lib/env")).resolves.toBeDefined(); + }); + + it("resolves when CLOUD_BILLING_ENABLED is legacy without PostHog", async () => { + // legacy billing forces V1 and never touches PostHog. + vi.stubEnv("CLOUD_BILLING_ENABLED", "legacy"); + + await expect(import("@/lib/env")).resolves.toBeDefined(); + }); }); diff --git a/ui/lib/env.ts b/ui/lib/env.ts index e32828ebf2..1dbc6234ef 100644 --- a/ui/lib/env.ts +++ b/ui/lib/env.ts @@ -2,7 +2,7 @@ import { assertGatedIntegrations, warnGatedIntegrationsMisconfig, } from "@/lib/integrations"; -import { readEnv } from "@/lib/runtime-env"; +import { readBoolEnv, readEnv } from "@/lib/runtime-env"; // Boot-time required-env assertion so a misconfigured container fails fast // with a clear message. A key with a deprecated legacy name is satisfied by @@ -23,6 +23,19 @@ for (const { key, legacy } of REQUIRED) { } assertGatedIntegrations(); + +// `metronome` billing evaluates the BILLING_SYSTEM_METRONOME PostHog flag per +// tenant, so PostHog must be enabled or every tenant would be misrouted to the +// wrong billing system. Fail fast instead of degrading silently. +if ( + readEnv("CLOUD_BILLING_ENABLED") === "metronome" && + !readBoolEnv("UI_POSTHOG_ENABLE") +) { + throw new Error( + 'CLOUD_BILLING_ENABLED is "metronome" but UI_POSTHOG_ENABLE is not "true"; PostHog is required for per-tenant billing routing.', + ); +} + warnGatedIntegrationsMisconfig(); export {}; diff --git a/ui/lib/get-runtime-config.client.test.ts b/ui/lib/get-runtime-config.client.test.ts index d4c91cf7e3..d46b633d38 100644 --- a/ui/lib/get-runtime-config.client.test.ts +++ b/ui/lib/get-runtime-config.client.test.ts @@ -98,7 +98,7 @@ describe("getRuntimeConfigClient", () => { [ "apiBaseUrl", "apiDocsUrl", - "billingCloudEnable", + "cloudBillingEnabled", "googleTagManagerId", "posthogHost", "posthogKey", @@ -108,9 +108,9 @@ describe("getRuntimeConfigClient", () => { ].sort(), ); expect(config.apiBaseUrl).toBe("https://api.example.com/api/v1"); - // billingCloudEnable is a boolean flag, so it defaults to false (not null) + // cloudBillingEnabled is a boolean flag, so it defaults to false (not null) // when absent from the island. - expect(config.billingCloudEnable).toBe(false); + expect(config.cloudBillingEnabled).toBe(false); expect( (config as unknown as Record).notAllowlisted, ).toBeUndefined(); diff --git a/ui/lib/get-runtime-config.client.ts b/ui/lib/get-runtime-config.client.ts index 0ae7d94de8..9aa4ea15c8 100644 --- a/ui/lib/get-runtime-config.client.ts +++ b/ui/lib/get-runtime-config.client.ts @@ -20,7 +20,7 @@ const pickConfig = ( posthogKey: parsed.posthogKey ?? null, posthogHost: parsed.posthogHost ?? null, reoDevClientId: parsed.reoDevClientId ?? null, - billingCloudEnable: parsed.billingCloudEnable ?? false, + cloudBillingEnabled: parsed.cloudBillingEnabled ?? false, }); // Reads the island once (memoized); all-null during SSR or if it's diff --git a/ui/lib/runtime-config.shared.ts b/ui/lib/runtime-config.shared.ts index 22f53002a7..e4fbbeceed 100644 --- a/ui/lib/runtime-config.shared.ts +++ b/ui/lib/runtime-config.shared.ts @@ -9,7 +9,7 @@ export interface RuntimePublicConfig { posthogKey: string | null; // reserved posthogHost: string | null; // reserved reoDevClientId: string | null; // reserved - billingCloudEnable: boolean; + cloudBillingEnabled: boolean; } export const RUNTIME_CONFIG_SCRIPT_ID = "__PROWLER_RUNTIME_CONFIG__"; @@ -24,5 +24,5 @@ export const EMPTY_RUNTIME_PUBLIC_CONFIG: RuntimePublicConfig = { posthogKey: null, posthogHost: null, reoDevClientId: null, - billingCloudEnable: false, + cloudBillingEnabled: false, }; diff --git a/ui/lib/runtime-config.ts b/ui/lib/runtime-config.ts index 9c149355e5..eda71bc114 100644 --- a/ui/lib/runtime-config.ts +++ b/ui/lib/runtime-config.ts @@ -4,7 +4,7 @@ import { connection } from "next/server"; import { readGatedEnv } from "@/lib/integrations"; import type { RuntimePublicConfig } from "@/lib/runtime-config.shared"; -import { readBoolEnv, readEnv } from "@/lib/runtime-env"; +import { readEnv } from "@/lib/runtime-env"; // `connection()` forces a per-request runtime read (never build-snapshotted); // only this allowlist reaches the client. Each migrated key falls back to its @@ -44,6 +44,10 @@ export async function getRuntimePublicConfig(): Promise { "POSTHOG_HOST", ), reoDevClientId: readEnv("REO_DEV_CLIENT_ID"), - billingCloudEnable: readBoolEnv("BILLING_CLOUD_ENABLE"), + // Install-level selector "legacy" | "metronome" | "false"; the client only + // needs on/off, so expose a derived boolean (the raw selector is read + // server-side for V1/V2 routing). Default (unset) is off. + cloudBillingEnabled: + (readEnv("CLOUD_BILLING_ENABLED") ?? "false") !== "false", }; } diff --git a/ui/tests/runtime-config/runtime-config-page.ts b/ui/tests/runtime-config/runtime-config-page.ts index 41a415ed9a..dcdf3e546c 100644 --- a/ui/tests/runtime-config/runtime-config-page.ts +++ b/ui/tests/runtime-config/runtime-config-page.ts @@ -19,7 +19,7 @@ export const RUNTIME_CONFIG_KEYS = [ "posthogKey", "posthogHost", "reoDevClientId", - "billingCloudEnable", + "cloudBillingEnabled", ] as const satisfies ReadonlyArray; /** diff --git a/ui/types/env.d.ts b/ui/types/env.d.ts index 301a59802d..b07ed75286 100644 --- a/ui/types/env.d.ts +++ b/ui/types/env.d.ts @@ -28,7 +28,7 @@ declare global { NEXT_PUBLIC_SENTRY_ENVIRONMENT?: string; UI_SENTRY_ENVIRONMENT?: string; - BILLING_CLOUD_ENABLE?: "true" | "false"; + CLOUD_BILLING_ENABLED?: "legacy" | "metronome" | "false"; // Build-time public config NEXT_PUBLIC_IS_CLOUD_ENV?: "true" | "false";