Skip to content

Commit e2b15fe

Browse files
committed
feat(webapp): add deterministic org hashBucket for rollout
1 parent 5b632e7 commit e2b15fe

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, it, expect } from "vitest";
2+
import { hashBucket } from "./computeBucket";
3+
4+
describe("hashBucket", () => {
5+
it("returns a stable value in [0, 100) for the same id", () => {
6+
const a = hashBucket("org_abc");
7+
const b = hashBucket("org_abc");
8+
expect(a).toBe(b);
9+
expect(a).toBeGreaterThanOrEqual(0);
10+
expect(a).toBeLessThan(100);
11+
});
12+
13+
it("is nested: the set enrolled at 1% is a subset of the set at 5%", () => {
14+
const ids = Array.from({ length: 5000 }, (_, i) => `org_${i}`);
15+
const at1 = new Set(ids.filter((id) => hashBucket(id) < 1));
16+
const at5 = ids.filter((id) => hashBucket(id) < 5);
17+
for (const id of at1) {
18+
expect(at5).toContain(id);
19+
}
20+
});
21+
22+
it("distributes roughly uniformly", () => {
23+
const ids = Array.from({ length: 10000 }, (_, i) => `org_${i}`);
24+
const under10 = ids.filter((id) => hashBucket(id) < 10).length;
25+
expect(under10).toBeGreaterThan(700);
26+
expect(under10).toBeLessThan(1300);
27+
});
28+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Deterministic 0-99 bucket for an org id, stable across processes and deploys.
3+
* FNV-1a (non-crypto): we only need determinism + uniform spread, not collision
4+
* resistance. Used for nested percentage rollout: `hashBucket(orgId) < percentage`.
5+
* Ramping the percentage down keeps a strict subset (the low buckets), so an org
6+
* never flaps in and out as the dial moves.
7+
*/
8+
export function hashBucket(orgId: string): number {
9+
let hash = 0x811c9dc5; // FNV offset basis
10+
for (let i = 0; i < orgId.length; i++) {
11+
hash ^= orgId.charCodeAt(i);
12+
hash = Math.imul(hash, 0x01000193) >>> 0;
13+
}
14+
return hash % 100;
15+
}

0 commit comments

Comments
 (0)