Skip to content

Commit 57a3d5d

Browse files
committed
feat: add new v2 config route
1 parent 7535d2a commit 57a3d5d

File tree

30 files changed

+1016
-196
lines changed

30 files changed

+1016
-196
lines changed

apps/web/abby.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export default defineConfig(
66
projectId: process.env.NEXT_PUBLIC_ABBY_PROJECT_ID!,
77
currentEnvironment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
88
apiUrl: process.env.NEXT_PUBLIC_ABBY_API_URL,
9-
__experimentalCdnUrl: process.env.NEXT_PUBLIC_ABBY_CDN_URL,
109
debug: process.env.NEXT_PUBLIC_ABBY_DEBUG === "true",
1110
},
1211
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE `ApiRequest` MODIFY `apiVersion` ENUM('V0', 'V1', 'V2') NOT NULL DEFAULT 'V0';

apps/web/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ enum ApiRequestType {
293293
enum ApiVersion {
294294
V0
295295
V1
296+
V2
296297
}
297298

298299
model ApiRequest {

apps/web/src/api/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { logger } from "hono/logger";
66
import { makeHealthRoute } from "./routes/health";
77
import { makeLegacyProjectDataRoute } from "./routes/legacy_project_data";
88
import { makeEventRoute } from "./routes/v1_event";
9+
import { makeV2ProjectDataRoute } from "./routes/v2_project_data";
910

1011
export const app = new Hono()
1112
.basePath("/api")
@@ -19,4 +20,6 @@ export const app = new Hono()
1920
// v1 routes
2021
.route("/v1/config", makeConfigRoute())
2122
.route("/v1/data", makeProjectDataRoute())
22-
.route("/v1/track", makeEventRoute());
23+
.route("/v1/track", makeEventRoute())
24+
// v2 routes
25+
.route("/v2/data", makeV2ProjectDataRoute());

apps/web/src/api/routes/v1_project_data.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ async function getAbbyResponseWithCache({
2121
c: Context;
2222
}) {
2323
startTime(c, "readCache");
24-
const cachedConfig = ConfigCache.getConfig({ environment, projectId });
24+
const cachedConfig = ConfigCache.getConfig({
25+
environment,
26+
projectId,
27+
apiVersion: "v1",
28+
});
2529
endTime(c, "readCache");
2630

2731
c.header(X_ABBY_CACHE_HEADER, cachedConfig !== undefined ? "HIT" : "MISS");
@@ -72,7 +76,12 @@ async function getAbbyResponseWithCache({
7276
}),
7377
} satisfies AbbyDataResponse;
7478

75-
ConfigCache.setConfig({ environment, projectId, value: response });
79+
ConfigCache.setConfig({
80+
environment,
81+
projectId,
82+
value: response,
83+
apiVersion: "v1",
84+
});
7685
return response;
7786
}
7887

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { zValidator } from "@hono/zod-validator";
2+
import {
3+
ABBY_WINDOW_KEY,
4+
type AbbyData,
5+
hashStringToInt32,
6+
serializeAbbyData,
7+
} from "@tryabby/core";
8+
import { type Context, Hono } from "hono";
9+
import { cors } from "hono/cors";
10+
import { endTime, startTime, timing } from "hono/timing";
11+
import { transformFlagValue } from "lib/flags";
12+
import { ConfigCache } from "server/common/config-cache";
13+
import { prisma } from "server/db/client";
14+
import { afterDataRequestQueue } from "server/queue/queues";
15+
import { z } from "zod";
16+
17+
export const X_ABBY_CACHE_HEADER = "X-Abby-Cache";
18+
19+
async function getAbbyResponseWithCache({
20+
environment,
21+
projectId,
22+
c,
23+
}: {
24+
environment: string;
25+
projectId: string;
26+
c: Context;
27+
}) {
28+
startTime(c, "readCache");
29+
const cachedConfig = ConfigCache.getConfig({
30+
environment,
31+
projectId,
32+
apiVersion: "v2",
33+
});
34+
endTime(c, "readCache");
35+
36+
c.header(X_ABBY_CACHE_HEADER, cachedConfig !== undefined ? "HIT" : "MISS");
37+
if (cachedConfig) {
38+
return serializeAbbyData(cachedConfig as AbbyData);
39+
}
40+
41+
startTime(c, "db");
42+
const [dbTests, dbFlags] = await Promise.all([
43+
prisma.test.findMany({
44+
where: {
45+
projectId,
46+
},
47+
include: { options: { select: { chance: true } } },
48+
}),
49+
prisma.featureFlagValue.findMany({
50+
where: {
51+
environment: {
52+
name: environment,
53+
projectId,
54+
},
55+
},
56+
include: { flag: { select: { name: true, type: true } } },
57+
}),
58+
]);
59+
endTime(c, "db");
60+
61+
const flags = dbFlags.filter(({ flag }) => flag.type === "BOOLEAN");
62+
63+
const remoteConfigs = dbFlags.filter(({ flag }) => flag.type !== "BOOLEAN");
64+
65+
const response = {
66+
tests: dbTests.map((test) => ({
67+
name: hashStringToInt32(test.name).toString(),
68+
weights: test.options.map((o) => o.chance.toNumber()),
69+
})),
70+
flags: flags.map((flagValue) => {
71+
return {
72+
name: hashStringToInt32(flagValue.flag.name).toString(),
73+
value: transformFlagValue(flagValue.value, flagValue.flag.type),
74+
};
75+
}),
76+
remoteConfig: remoteConfigs.map((flagValue) => {
77+
return {
78+
name: hashStringToInt32(flagValue.flag.name).toString(),
79+
value: transformFlagValue(flagValue.value, flagValue.flag.type),
80+
};
81+
}),
82+
} satisfies AbbyData;
83+
84+
ConfigCache.setConfig({
85+
environment,
86+
projectId,
87+
value: response,
88+
apiVersion: "v2",
89+
});
90+
return serializeAbbyData(response);
91+
}
92+
93+
export function makeV2ProjectDataRoute() {
94+
const app = new Hono()
95+
.get(
96+
"/:projectId",
97+
cors({
98+
origin: "*",
99+
maxAge: 86400,
100+
}),
101+
zValidator(
102+
"query",
103+
z.object({
104+
environment: z.string(),
105+
})
106+
),
107+
timing(),
108+
async (c) => {
109+
const projectId = c.req.param("projectId");
110+
const { environment } = c.req.valid("query");
111+
112+
const now = performance.now();
113+
114+
try {
115+
startTime(c, "getAbbyResponseWithCache");
116+
const response = await getAbbyResponseWithCache({
117+
projectId,
118+
environment,
119+
c,
120+
});
121+
endTime(c, "getAbbyResponseWithCache");
122+
123+
const duration = performance.now() - now;
124+
125+
afterDataRequestQueue.add("after-data-request", {
126+
apiVersion: "V2",
127+
functionDuration: duration,
128+
projectId,
129+
});
130+
131+
return c.json(response);
132+
} catch (e) {
133+
console.error(e);
134+
return c.json({ error: "Internal server error" }, { status: 500 });
135+
}
136+
}
137+
)
138+
.get(
139+
"/:projectId/script.js",
140+
cors({
141+
origin: "*",
142+
maxAge: 86400,
143+
}),
144+
zValidator(
145+
"query",
146+
z.object({
147+
environment: z.string(),
148+
})
149+
),
150+
timing(),
151+
async (c) => {
152+
const projectId = c.req.param("projectId");
153+
const { environment } = c.req.valid("query");
154+
155+
const now = performance.now();
156+
157+
try {
158+
startTime(c, "getAbbyResponseWithCache");
159+
const response = await getAbbyResponseWithCache({
160+
projectId,
161+
environment,
162+
c,
163+
});
164+
endTime(c, "getAbbyResponseWithCache");
165+
166+
const jsContent = `window.${ABBY_WINDOW_KEY} = ${JSON.stringify(
167+
response
168+
)}`;
169+
170+
const duration = performance.now() - now;
171+
172+
afterDataRequestQueue.add("after-data-request", {
173+
apiVersion: "V2",
174+
functionDuration: duration,
175+
projectId,
176+
});
177+
178+
return c.text(jsContent, {
179+
headers: {
180+
"Content-Type": "application/javascript",
181+
},
182+
});
183+
} catch (e) {
184+
console.error(e);
185+
return c.json({ error: "Internal server error" }, { status: 500 });
186+
}
187+
}
188+
);
189+
return app;
190+
}

apps/web/src/components/AddFeatureFlagModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { RadioSelect } from "./RadioSelect";
1313

1414
import { Toggle } from "./Toggle";
1515

16+
import { SAFE_NAME_REGEX } from "@tryabby/core";
1617
import { useTracking } from "lib/tracking";
1718
import { Input } from "./ui/input";
1819

@@ -168,7 +169,6 @@ export const AddFeatureFlagModal = ({
168169
projectId,
169170
isRemoteConfig,
170171
}: Props) => {
171-
const _inputRef = useRef<HTMLInputElement>(null);
172172
const ctx = trpc.useContext();
173173
const stateRef = useRef<FlagFormValues>();
174174
const trackEvent = useTracking();
@@ -202,6 +202,11 @@ export const AddFeatureFlagModal = ({
202202
if (!stateRef.current?.value) {
203203
errors.value = "Value is required";
204204
}
205+
206+
if (SAFE_NAME_REGEX.test(trimmedName) === false) {
207+
errors.name =
208+
"Invalid name. Only letters, numbers, and underscores are allowed.";
209+
}
205210
if (Object.keys(errors).length > 0) {
206211
setErrors(errors);
207212
return;

apps/web/src/components/FlagPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { Input } from "components/ui/input";
1616
import Fuse from "fuse.js";
1717
import { useProjectId } from "lib/hooks/useProjectId";
1818
import { EditIcon, FileEditIcon, Search, TrashIcon } from "lucide-react";
19-
import { useMemo, useState } from "react";
19+
import { useEffect, useMemo, useState } from "react";
2020
import { toast } from "react-hot-toast";
2121
import { AiOutlinePlus } from "react-icons/ai";
2222
import { BiInfoCircle } from "react-icons/bi";
@@ -188,6 +188,10 @@ export const FeatureFlagPageContent = ({
188188
setFlags(results.map((result) => result.item));
189189
};
190190

191+
useEffect(() => {
192+
setFlags(data.flags);
193+
}, [data.flags]);
194+
191195
const activeFlag = data.flags.find((flag) => flag.id === activeFlagInfo?.id);
192196

193197
if (data.environments.length === 0)

apps/web/src/lib/abby.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const {
1313
getABTestValue,
1414
withAbbyApiHandler,
1515
getABResetFunction,
16+
useRemoteConfig,
1617
} = createAbby(abbyConfig);
1718

1819
export const AbbyDevtools = withDevtools(abbyDevtools, {});

apps/web/src/pages/_app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import { trpc } from "../utils/trpc";
88

99
import { TooltipProvider } from "components/Tooltip";
1010
import { env } from "env/client.mjs";
11-
import { AbbyDevtools, AbbyProvider, withAbby } from "lib/abby";
1211
import type { NextPage } from "next";
1312
import { useRouter } from "next/router";
1413
import type { ReactElement, ReactNode } from "react";
1514
import "@fontsource/martian-mono/600.css";
1615

1716
import "../styles/shadcn.css";
1817
import "@code-hike/mdx/dist/index.css";
18+
import { AbbyDevtools, AbbyProvider, withAbby } from "lib/abby";
1919
import PlausibleProvider from "next-plausible";
2020

2121
export type NextPageWithLayout<P = unknown, IP = P> = NextPage<P, IP> & {

0 commit comments

Comments
 (0)