From 845605920fd16e618e0db3ba820d5b28fa17fa7a Mon Sep 17 00:00:00 2001 From: Dmytro Hryshyn Date: Mon, 23 Oct 2023 19:45:42 +0300 Subject: [PATCH] chore: implement a/b test middleware --- apps/web/abTest/middlewareFactory.ts | 45 ++++++++++++++++++++++++++++ apps/web/abTest/utils.ts | 9 ++++++ packages/lib/constants.ts | 1 + turbo.json | 4 ++- 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 apps/web/abTest/middlewareFactory.ts create mode 100644 apps/web/abTest/utils.ts diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts new file mode 100644 index 0000000000..f311fe7be7 --- /dev/null +++ b/apps/web/abTest/middlewareFactory.ts @@ -0,0 +1,45 @@ +import { getBucket } from "abTest/utils"; +import type { NextFetchEvent, NextMiddleware, NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import z from "zod"; + +const ROUTE_MAP = new Map([ + ["/event-types", Boolean(process.env.APP_ROUTER_EVENT_TYPES_ENABLED)] as const, +]); + +const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override"; +const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled"; + +const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]).default("legacy"); + +export const abTestMiddlewareFactory = + (next: NextMiddleware): NextMiddleware => + async (req: NextRequest, event: NextFetchEvent) => { + const { pathname } = req.nextUrl; + + const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME); + + const enabled = ROUTE_MAP.has(pathname) ? (ROUTE_MAP.get(pathname) ?? false) || override : false; + + if (pathname.includes("future") || !enabled) { + return next(req, event); + } + + const safeParsedBucket = override + ? { success: true as const, data: "future" as const } + : bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value); + + if (!safeParsedBucket.success) { + // cookie does not exist or it has incorrect value + + const res = NextResponse.next(); + res.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, getBucket(), { expires: 1000 * 60 * 30 }); // 30 min in ms + return res; + } + + const bucketUrlPrefix = safeParsedBucket.data === "future" ? "future" : ""; + + const url = req.nextUrl.clone(); + url.pathname = `${bucketUrlPrefix}${pathname}/`; + return NextResponse.rewrite(url); + }; diff --git a/apps/web/abTest/utils.ts b/apps/web/abTest/utils.ts new file mode 100644 index 0000000000..ed40c9fca9 --- /dev/null +++ b/apps/web/abTest/utils.ts @@ -0,0 +1,9 @@ +import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants"; + +const cryptoRandom = () => { + return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff; +}; + +export const getBucket = () => { + return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy"; +}; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 59ac3a80ad..c696839346 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -106,3 +106,4 @@ export const APP_CREDENTIAL_SHARING_ENABLED = export const DEFAULT_LIGHT_BRAND_COLOR = "#292929"; export const DEFAULT_DARK_BRAND_COLOR = "#fafafa"; +export const AB_TEST_BUCKET_PROBABILITY = Number(process.env.AB_TEST_BUCKET_PROBABILITY) ?? 10; diff --git a/turbo.json b/turbo.json index 849d70f2fe..7ec9087d85 100644 --- a/turbo.json +++ b/turbo.json @@ -324,6 +324,8 @@ "ZOHOCRM_CLIENT_ID", "ZOHOCRM_CLIENT_SECRET", "ZOOM_CLIENT_ID", - "ZOOM_CLIENT_SECRET" + "ZOOM_CLIENT_SECRET", + "AB_TEST_BUCKET_PROBABILITY", + "APP_ROUTER_EVENT_TYPES_ENABLED" ] }