refactor(ui): rename reserved billing flag to CLOUD_BILLING_ENABLED (#11920)

Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
This commit is contained in:
Pablo Fernandez Guerra (PFE)
2026-07-10 09:26:33 +02:00
committed by GitHub
parent 2a077cae5e
commit 892cc2bc07
8 changed files with 55 additions and 11 deletions
+27
View File
@@ -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();
});
});
+14 -1
View File
@@ -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 {};
+3 -3
View File
@@ -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<string, unknown>).notAllowlisted,
).toBeUndefined();
+1 -1
View File
@@ -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 <head> island once (memoized); all-null during SSR or if it's
+2 -2
View File
@@ -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,
};
+6 -2
View File
@@ -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<RuntimePublicConfig> {
"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",
};
}
@@ -19,7 +19,7 @@ export const RUNTIME_CONFIG_KEYS = [
"posthogKey",
"posthogHost",
"reoDevClientId",
"billingCloudEnable",
"cloudBillingEnabled",
] as const satisfies ReadonlyArray<keyof RuntimePublicConfig>;
/**
+1 -1
View File
@@ -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";