Skip to content

Commit 3cf484d

Browse files
committed
fix(webapp): strict boolean kill switch, bound reload interval, cuid bucket test
1 parent 0b2f08d commit 3cf484d

3 files changed

Lines changed: 25 additions & 3 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,10 @@ const EnvironmentSchema = z
164164
COMPUTE_BACKING_MAP: z.string().default("{}"),
165165
// How often each replica reloads the global flags snapshot from the DB.
166166
// Sets kill/ramp propagation latency.
167-
GLOBAL_FLAGS_RELOAD_INTERVAL_MS: z.coerce.number().int().default(5000),
167+
GLOBAL_FLAGS_RELOAD_INTERVAL_MS: z.coerce.number().int().min(1000).default(5000),
168168
// Max time the first trigger blocks waiting for the initial flags load
169169
// before falling back to defaults (off = container, the safe direction).
170-
GLOBAL_FLAGS_READY_TIMEOUT_MS: z.coerce.number().int().default(5000),
170+
GLOBAL_FLAGS_READY_TIMEOUT_MS: z.coerce.number().int().min(0).default(5000),
171171
WORKER_ENABLED: z.string().default("true"),
172172
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
173173
DISABLE_SSE: z.string().optional(),

apps/webapp/app/v3/featureFlags.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export const FeatureFlagCatalog = {
3030
// globally and per-org (org wins). Defaults to "electric" when unset.
3131
// "shadow" serves Electric but diffs the native path in the background.
3232
[FEATURE_FLAG.realtimeBackend]: z.enum(["electric", "native", "shadow"]),
33-
[FEATURE_FLAG.computeMigrationEnabled]: z.coerce.boolean(),
33+
// Strict z.boolean() (not z.coerce.boolean()): coercion turns the string "false"
34+
// into true, which would silently flip this kill switch / per-org exclude the wrong
35+
// way if written as a string via the admin PAT route. The admin toggle sends a real
36+
// boolean, so this only rejects the dangerous stringified case.
37+
[FEATURE_FLAG.computeMigrationEnabled]: z.boolean(),
3438
[FEATURE_FLAG.computeMigrationFreePercentage]: z.coerce.number().int().min(0).max(100),
3539
[FEATURE_FLAG.computeMigrationPaidPercentage]: z.coerce.number().int().min(0).max(100),
3640
};

apps/webapp/test/computeBucket.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect } from "vitest";
2+
import cuid from "cuid";
23
import { hashBucket } from "~/utils/computeBucket";
34

45
describe("hashBucket", () => {
@@ -25,4 +26,21 @@ describe("hashBucket", () => {
2526
expect(under10).toBeGreaterThan(700);
2627
expect(under10).toBeLessThan(1300);
2728
});
29+
30+
// Org ids are `@default(cuid())` primary keys (e.g. "cjld2cjxh0000qzrmn831i7rn"),
31+
// not the synthetic sequential ids above. cuids share a "c" prefix + timestamp/counter
32+
// structure, so verify the hash still spreads *real-shaped* ids evenly across deciles
33+
// (so a percentage dial maps to ~that fraction of actual orgs, not just of the id space).
34+
it("distributes cuids evenly across all 10 deciles", () => {
35+
const ids = Array.from({ length: 20000 }, () => cuid());
36+
const counts = new Array(10).fill(0);
37+
for (const id of ids) {
38+
counts[Math.floor(hashBucket(id) / 10)]++;
39+
}
40+
// Expected ~2000 per decile; allow a wide band so it isn't flaky.
41+
for (const count of counts) {
42+
expect(count).toBeGreaterThan(1700);
43+
expect(count).toBeLessThan(2300);
44+
}
45+
});
2846
});

0 commit comments

Comments
 (0)