From 48e7b616b849ef2398a06d4f615acd93a7be0dc7 Mon Sep 17 00:00:00 2001 From: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:06:18 +0200 Subject: [PATCH] chore: [app dir bootstrapping 10] adds ab tests middleware (#12045) ## What does this PR do? - Adds AB tests middleware that redirects users to the app-dir pages with probability that is defined by env var Fixes # (issue) ## Requirement/Documentation This PR requires new env variables: `APP_ROUTER_EVENT_TYPES_ENABLED` - boolean that defines if app dir event-types page redirect is enabled `AB_TEST_BUCKET_PROBABILITY` - number [0, 100] that defines the percentage of users getting redirected to the app dir pages ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] Chore (refactoring code, technical debt, workflow improvements) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ## How should this be tested? Does not requires testing, implements middleware as a dead code ## Mandatory Tasks - [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected. --- apps/web/abTest/middlewareFactory.ts | 50 ++++++++++++++++++++++++++++ apps/web/abTest/utils.ts | 9 +++++ packages/lib/constants.ts | 1 + turbo.json | 2 ++ 4 files changed, 62 insertions(+) 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..bb2115e47a --- /dev/null +++ b/apps/web/abTest/middlewareFactory.ts @@ -0,0 +1,50 @@ +import { getBucket } from "abTest/utils"; +import type { NextMiddleware, NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import z from "zod"; + +const ROUTES: [RegExp, boolean][] = [ + [/^\/event-types$/, Boolean(process.env.APP_ROUTER_EVENT_TYPES_ENABLED)], +]; + +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: (req: NextRequest) => Promise>): NextMiddleware => + async (req: NextRequest) => { + const response = await next(req); + + const { pathname } = req.nextUrl; + + const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME); + + const route = ROUTES.find(([regExp]) => regExp.test(pathname)) ?? null; + + const enabled = route !== null ? route[1] || override : false; + + if (pathname.includes("future") || !enabled) { + return response; + } + + 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(response); + 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, response); + }; 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..cc5fb76143 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 7f1282d0c4..2b171f6723 100644 --- a/turbo.json +++ b/turbo.json @@ -194,9 +194,11 @@ }, "globalDependencies": ["yarn.lock"], "globalEnv": [ + "AB_TEST_BUCKET_PROBABILITY", "ALLOWED_HOSTNAMES", "ANALYZE", "API_KEY_PREFIX", + "APP_ROUTER_EVENT_TYPES_ENABLED", "APP_USER_NAME", "BASECAMP3_CLIENT_ID", "BASECAMP3_CLIENT_SECRET",