Skip to content

Commit a6cb656

Browse files
Added feature flags via KV
1 parent d48e8bb commit a6cb656

5 files changed

Lines changed: 69 additions & 10 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export default function getUserAgentGroups(userAgent: string | null) {
2+
if(!userAgent)
3+
return null;
4+
5+
const match = userAgent.match(/(?<client>[\w]+)\-(?<major>[0-9v]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)/);
6+
7+
if(!match?.groups)
8+
return null;
9+
10+
return {
11+
client: match.groups.client,
12+
version: {
13+
major: match.groups.major,
14+
minor: match.groups.minor,
15+
patch: match.groups.patch,
16+
17+
toString: () => {
18+
return `${match.groups?.major}.${match.groups?.minor}.${match.groups?.patch}`;
19+
}
20+
}
21+
};
22+
};

src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ declare type Env = {
44
DATABASE: D1Database;
55
BUCKET: R2Bucket;
66
ACTIVITY_DURABLE_OBJECT: DurableObjectNamespace;
7+
FEATURE_FLAGS: KVNamespace;
78

89
ENVIRONMENT: "production" | "staging" | "dev";
910
CLOUDFLARE_ACCOUNT_ID: string;

src/index.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { createActivitySummary } from "./controllers/activities/summary/createAc
77
import { updatePersonalBestActivitySummary } from "./controllers/activities/summary/updatePersonalBestActivitySummary";
88
import { getActivitySummaryById } from "./controllers/activities/summary/getActivitySummaryById";
99
import { getActivitiesWithoutSummary } from "./controllers/activities/getActivitiesWithoutSummary";
10+
import getUserAgentGroups from "./controllers/getUserAgentGroups";
11+
import { FeatureFlags, VersionFeatureFlags } from "./models/FeatureFlags";
1012

1113
const router = createRouter();
1214

13-
async function getRequest(request: any, env: any, context: any) {
14-
const response: Response = await router.handle(request, env, context);
15+
async function getRequest(request: Request, env: Env, context: EventContext<Env, string, null>, featureFlags: VersionFeatureFlags) {
16+
const response: Response = await router.handle(request, env, context, featureFlags);
1517

1618
if(!response) {
1719
return new Response(undefined, {
@@ -23,8 +25,6 @@ async function getRequest(request: any, env: any, context: any) {
2325
return response;
2426
}
2527

26-
const acceptedAgents = [ "RideTrackerService", "RideTrackerApp" ];
27-
2828
export default {
2929
async scheduled(controller: ScheduledController, env: Env, context: EventContext<Env, string, null>) {
3030
switch(controller.cron) {
@@ -51,25 +51,36 @@ export default {
5151

5252
async fetch(request: Request, env: Env, context: EventContext<Env, string, null>) {
5353
try {
54-
const userAgent = request.headers.get("User-Agent");
54+
const userAgent = getUserAgentGroups(request.headers.get("User-Agent"));
5555

56-
if(!userAgent || !userAgent.match(/\w+\-([0-9]+)\.([0-9]+)\.([0-9]+)/)) {
56+
if(!userAgent) {
5757
return new Response(undefined, {
5858
status: 400,
5959
statusText: "Bad Request"
6060
});
6161
}
6262

63-
if(!acceptedAgents.includes(userAgent.substring(0, userAgent.indexOf('-')))) {
63+
const featureFlags = await env.FEATURE_FLAGS.get<FeatureFlags | null>(`${userAgent.client}`, "json");
64+
65+
if(!featureFlags) {
6466
context.waitUntil(triggerAlarm(env, "User Agent Alarm", `An unrecognized user agent was detected.\n \n\`\`\`\n${userAgent}\n\`\`\`\n${request.method} ${request.url}\nRemote Address: || ${request.headers.get("CF-Connecting-IP")} ||`));
6567

6668
return new Response(undefined, {
67-
status: 403,
68-
statusText: "Forbidden"
69+
status: 400,
70+
statusText: "Bad Request"
71+
});
72+
}
73+
74+
const versionFeatureFlags = featureFlags.versions[userAgent.version.toString()];
75+
76+
if(versionFeatureFlags.status === "UNSUPPORTED") {
77+
return new Response(undefined, {
78+
status: 410,
79+
statusText: "Gone"
6980
});
7081
}
7182

72-
const response = await getRequest(request, env, context);
83+
const response = await getRequest(request, env, context, versionFeatureFlags);
7384

7485
if(response.status < 200 || response.status > 299) {
7586
context.waitUntil(new Promise<void>(async (resolve) => {

src/models/FeatureFlags.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type VersionFeatureFlags = {
2+
uses: string;
3+
4+
status: "SUPPORTED" | "UNSUPPORTED" | "DEPRECATED";
5+
6+
supersededBy?: string;
7+
};
8+
9+
export type FeatureFlags = {
10+
versions: {
11+
[key: string]: VersionFeatureFlags;
12+
};
13+
};

wrangler.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ bindings = [
2626
{ name = "ACTIVITY_DURABLE_OBJECT", class_name = "ActivityDurableObject" }
2727
]
2828

29+
[[ kv_namespaces ]]
30+
binding = "FEATURE_FLAGS"
31+
id = "e4051aac3f97408eb005e85eb81bdc97"
32+
2933
[triggers]
3034
crons = [ "0 * * * *" ]
3135

@@ -46,6 +50,10 @@ bindings = [
4650
{ name = "ACTIVITY_DURABLE_OBJECT", class_name = "ActivityDurableObject" }
4751
]
4852

53+
[[ env.production.kv_namespaces ]]
54+
binding = "FEATURE_FLAGS"
55+
id = "e9e05d97db9c4e0f8fa1e3784dc4d0ad"
56+
4957
[env.production.triggers]
5058
crons = [ "0 * * * *" ]
5159

@@ -66,5 +74,9 @@ bindings = [
6674
{ name = "ACTIVITY_DURABLE_OBJECT", class_name = "ActivityDurableObject" }
6775
]
6876

77+
[[ env.staging.kv_namespaces ]]
78+
binding = "FEATURE_FLAGS"
79+
id = "225cf6788c7447079e5f8dc7b9f1be18"
80+
6981
[env.staging.triggers]
7082
crons = [ "0 * * * *" ]

0 commit comments

Comments
 (0)