Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Readme

Feature Flags

This val demonstrates a very simple feature flag implementation. There is a const in the file called FLAGS which stores the feature flags defined, fork this val and modify this value to setup your own flags.

Defining flags

The example FLAGS value is:

const FLAGS: Record<string, Flag> = { "flag1": Flag.rollout(0.5), "flag2": Flag.enabled().block("user_123"), "flag3": Flag.disabled().allow("hello@samwho.dev"), };

This demonstrates:

  • flag1 -- a flag that will be enabled for 50% of users (more on how users are defined later)
  • flag2 -- a flag that is enabled for everyone except user_123
  • flag3 -- a flag that is disabled for everyone except hello@samwho.dev

Endpoints

There are two endpoints:

  • / -- the root endpoint fetches all flags for the given user.
  • /:id -- fetches just one flag value, given by :id, for the given user.

Specifying users

By default, the user is determined by IP address. If you want to be more specific, you can pass in the ?userId query parameter to any endpoint and that will be used instead.

How it works

This val works by hashing the userId and using the resulting value to determine whether a flag should be enabled or disabled. In a 50% rollout, for example, the numeric hash of the userId is taken and divided by the maximum hash value. If the result is less than the rollout percentage, the flag is enabled. This allows for completely stateless feature flags, no database required.

To prevent the same users getting features first all of the time, the flag name is prepended to the userId before hashing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { createHash } from "node:crypto";
import { Context, Hono } from "npm:hono@3";
import { cors } from "npm:hono@3/cors";
const HASH_LENGTH = 32;
const HASH_MAX = 2 ** HASH_LENGTH;
class Flag {
allowlist: Set<string> = new Set();
blocklist: Set<string> = new Set();
static rollout(percent: number): Flag {
return new Flag(percent);
}
static enabled(): Flag {
return new Flag(1);
}
static disabled(): Flag {
return new Flag(0);
}
private constructor(private readonly rollout: number) {
if (rollout < 0 || rollout > 1) {
throw new Error("Rollout must be between 0 and 1");
}
}
allow(userId: string | string[]): Flag {
if (!Array.isArray(userId)) {
userId = [userId];
}
for (const id of userId) {
this.allowlist.add(id);
}
return this;
}
block(userId: string | string[]): Flag {
if (!Array.isArray(userId)) {
userId = [userId];
}
for (const id of userId) {
this.blocklist.add(id);
}
return this;
}
isEnabled(userId: string): boolean {
if (this.allowlist.has(userId)) {
return true;
}
if (this.blocklist.has(userId)) {
return false;
}
const hash = createHash("sha256").update(userId).digest("hex");
const decimalHash = parseInt(hash.substring(0, HASH_LENGTH / 4), 16);
return decimalHash / HASH_MAX < this.rollout;
}
}
const FLAGS: Record<string, Flag> = {
"flag1": Flag.rollout(0.5),
"flag2": Flag.enabled().block("user_123"),
"flag3": Flag.disabled().allow("hello@samwho.dev"),
};
function getUserId(c: Context): string {
return c.req.query("userId")
|| c.req.header("x-forwarded-for")
|| c.req.header("x-real-ip");
}
const app = new Hono();
app.use(
"/*",
cors({
origin: "*",
allowHeaders: ["Content-Type"],
allowMethods: ["GET", "OPTIONS"],
}),
);
app.get("/:id", async (c) => {
const userId = getUserId(c);
if (!userId) {
return c.json({ error: "User ID must be provided" }, 400);
}
const flagId = c.req.param("id");
const flag = FLAGS[flagId];
if (!flag) {
return c.json({ error: `flag "${flagId}" not found` }, 404);
}
return c.json({ enabled: flag.isEnabled(`${flagId}:${userId}`) });
});
samwho-featureflags.web.val.run
August 9, 2024