@se-studio/ab-testing 0.0.0-next16-20260822103555

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +1125 -0
  2. package/README.md +412 -0
  3. package/dist/codegen/cli.d.ts +3 -0
  4. package/dist/codegen/cli.d.ts.map +1 -0
  5. package/dist/codegen/cli.js +121 -0
  6. package/dist/codegen/cli.js.map +1 -0
  7. package/dist/codegen/generator.d.ts +42 -0
  8. package/dist/codegen/generator.d.ts.map +1 -0
  9. package/dist/codegen/generator.js +56 -0
  10. package/dist/codegen/generator.js.map +1 -0
  11. package/dist/codegen/index.d.ts +9 -0
  12. package/dist/codegen/index.d.ts.map +1 -0
  13. package/dist/codegen/index.js +8 -0
  14. package/dist/codegen/index.js.map +1 -0
  15. package/dist/components/AbTestReporter.d.ts +2 -0
  16. package/dist/components/AbTestReporter.d.ts.map +1 -0
  17. package/dist/components/AbTestReporter.js +19 -0
  18. package/dist/components/AbTestReporter.js.map +1 -0
  19. package/dist/components/AbTestUtmScript.d.ts +17 -0
  20. package/dist/components/AbTestUtmScript.d.ts.map +1 -0
  21. package/dist/components/AbTestUtmScript.js +43 -0
  22. package/dist/components/AbTestUtmScript.js.map +1 -0
  23. package/dist/components/index.d.ts +3 -0
  24. package/dist/components/index.d.ts.map +1 -0
  25. package/dist/components/index.js +3 -0
  26. package/dist/components/index.js.map +1 -0
  27. package/dist/hooks/index.d.ts +8 -0
  28. package/dist/hooks/index.d.ts.map +1 -0
  29. package/dist/hooks/index.js +7 -0
  30. package/dist/hooks/index.js.map +1 -0
  31. package/dist/hooks/useAbTestAssignments.d.ts +56 -0
  32. package/dist/hooks/useAbTestAssignments.d.ts.map +1 -0
  33. package/dist/hooks/useAbTestAssignments.js +87 -0
  34. package/dist/hooks/useAbTestAssignments.js.map +1 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +17 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/middleware/assignment.d.ts +31 -0
  40. package/dist/middleware/assignment.d.ts.map +1 -0
  41. package/dist/middleware/assignment.js +125 -0
  42. package/dist/middleware/assignment.js.map +1 -0
  43. package/dist/middleware/cache.d.ts +30 -0
  44. package/dist/middleware/cache.d.ts.map +1 -0
  45. package/dist/middleware/cache.js +140 -0
  46. package/dist/middleware/cache.js.map +1 -0
  47. package/dist/middleware/cookies.d.ts +44 -0
  48. package/dist/middleware/cookies.d.ts.map +1 -0
  49. package/dist/middleware/cookies.js +66 -0
  50. package/dist/middleware/cookies.js.map +1 -0
  51. package/dist/middleware/handler.d.ts +53 -0
  52. package/dist/middleware/handler.d.ts.map +1 -0
  53. package/dist/middleware/handler.js +201 -0
  54. package/dist/middleware/handler.js.map +1 -0
  55. package/dist/middleware/index.d.ts +15 -0
  56. package/dist/middleware/index.d.ts.map +1 -0
  57. package/dist/middleware/index.js +13 -0
  58. package/dist/middleware/index.js.map +1 -0
  59. package/dist/middleware/prune.d.ts +13 -0
  60. package/dist/middleware/prune.d.ts.map +1 -0
  61. package/dist/middleware/prune.js +29 -0
  62. package/dist/middleware/prune.js.map +1 -0
  63. package/dist/middleware/types.d.ts +103 -0
  64. package/dist/middleware/types.d.ts.map +1 -0
  65. package/dist/middleware/types.js +2 -0
  66. package/dist/middleware/types.js.map +1 -0
  67. package/dist/middleware/urlParams.d.ts +7 -0
  68. package/dist/middleware/urlParams.d.ts.map +1 -0
  69. package/dist/middleware/urlParams.js +20 -0
  70. package/dist/middleware/urlParams.js.map +1 -0
  71. package/dist/middleware/variantId.d.ts +9 -0
  72. package/dist/middleware/variantId.d.ts.map +1 -0
  73. package/dist/middleware/variantId.js +12 -0
  74. package/dist/middleware/variantId.js.map +1 -0
  75. package/dist/reporting/buildExperimentImpression.d.ts +6 -0
  76. package/dist/reporting/buildExperimentImpression.d.ts.map +1 -0
  77. package/dist/reporting/buildExperimentImpression.js +22 -0
  78. package/dist/reporting/buildExperimentImpression.js.map +1 -0
  79. package/dist/types.d.ts +144 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +7 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/utils.d.ts +12 -0
  84. package/dist/utils.d.ts.map +1 -0
  85. package/dist/utils.js +20 -0
  86. package/dist/utils.js.map +1 -0
  87. package/dist/webhook/handler.d.ts +109 -0
  88. package/dist/webhook/handler.d.ts.map +1 -0
  89. package/dist/webhook/handler.js +128 -0
  90. package/dist/webhook/handler.js.map +1 -0
  91. package/dist/webhook/index.d.ts +8 -0
  92. package/dist/webhook/index.d.ts.map +1 -0
  93. package/dist/webhook/index.js +7 -0
  94. package/dist/webhook/index.js.map +1 -0
  95. package/docs/llms.md +106 -0
  96. package/package.json +91 -0
@@ -0,0 +1,140 @@
1
+ /** Global cache instance */
2
+ let globalTestsCache = null;
3
+ /**
4
+ * Pre-compute weights and lookups for a test.
5
+ */
6
+ export function computeCachedTest(test) {
7
+ const readyWeights = {};
8
+ const variantLabelLookup = {};
9
+ const reportingEventLookup = {};
10
+ const urlParamsLookup = {};
11
+ const reportingParamsLookup = {};
12
+ const config = test.configuration;
13
+ if (config && config.length > 0) {
14
+ // Control bucket (index 0)
15
+ readyWeights['control'] = config[0]?.weight ?? 0;
16
+ variantLabelLookup['control'] = config[0]?.variant_label;
17
+ reportingEventLookup['control'] = config[0]?.reporting_event;
18
+ urlParamsLookup['control'] = config[0]?.url_params;
19
+ reportingParamsLookup['control'] = config[0]?.reporting_params;
20
+ // Variant buckets (index 1+)
21
+ for (let i = 0; i < test.variants.length; i++) {
22
+ const variantConfig = config[i + 1];
23
+ const variant = test.variants[i];
24
+ if (variant) {
25
+ readyWeights[variant.id] = variantConfig?.weight ?? 0;
26
+ variantLabelLookup[variant.id] = variantConfig?.variant_label;
27
+ reportingEventLookup[variant.id] = variantConfig?.reporting_event;
28
+ urlParamsLookup[variant.id] = variantConfig?.url_params;
29
+ reportingParamsLookup[variant.id] = variantConfig?.reporting_params;
30
+ }
31
+ }
32
+ }
33
+ else {
34
+ // Fallback: equal weights if no configuration
35
+ const totalOptions = test.variants.length + 1;
36
+ const equalWeight = 1 / totalOptions;
37
+ readyWeights['control'] = equalWeight;
38
+ for (const variant of test.variants) {
39
+ readyWeights[variant.id] = equalWeight;
40
+ }
41
+ }
42
+ // Pre-compute variant lookup (ID -> Slug)
43
+ const variantLookup = { control: 'control' };
44
+ for (const variant of test.variants) {
45
+ variantLookup[variant.id] = variant.slug;
46
+ }
47
+ return {
48
+ ...test,
49
+ readyWeights,
50
+ variantLookup,
51
+ variantLabelLookup,
52
+ reportingEventLookup,
53
+ urlParamsLookup,
54
+ reportingParamsLookup,
55
+ };
56
+ }
57
+ /**
58
+ * Normalize a URL path for consistent matching.
59
+ * - Removes trailing slashes (except for root)
60
+ * - Ensures leading slash
61
+ */
62
+ export function normalizePath(path) {
63
+ // Handle "index" as root
64
+ if (path === 'index') {
65
+ return '/';
66
+ }
67
+ // Ensure leading slash
68
+ let normalized = path.startsWith('/') ? path : `/${path}`;
69
+ // Remove trailing slash (except for root)
70
+ if (normalized.endsWith('/') && normalized.length > 1) {
71
+ normalized = normalized.slice(0, -1);
72
+ }
73
+ return normalized;
74
+ }
75
+ /**
76
+ * Build cached tests map from raw tests.
77
+ */
78
+ function buildTestsByPath(tests) {
79
+ const testsByPath = new Map();
80
+ for (const test of tests) {
81
+ if (!test.enabled)
82
+ continue;
83
+ const normalizedControlPath = normalizePath(test.controlSlug);
84
+ const cachedTest = computeCachedTest(test);
85
+ const existing = testsByPath.get(normalizedControlPath) || [];
86
+ existing.push(cachedTest);
87
+ testsByPath.set(normalizedControlPath, existing);
88
+ }
89
+ return testsByPath;
90
+ }
91
+ /**
92
+ * Get cached tests, refreshing from store if needed.
93
+ *
94
+ * @param store - The blob store to fetch tests from
95
+ * @param cacheTtlMs - Cache time-to-live in milliseconds
96
+ * @param devTestData - Optional test data for development mode
97
+ * @returns Map of tests indexed by normalized control path
98
+ */
99
+ export async function getCachedTests(store, cacheTtlMs, devTestData) {
100
+ const now = Date.now();
101
+ // Return cached data if still valid
102
+ if (globalTestsCache && now - globalTestsCache.timestamp < cacheTtlMs) {
103
+ return globalTestsCache.testsByPath;
104
+ }
105
+ try {
106
+ let tests;
107
+ // Use dev test data if provided
108
+ if (devTestData && process.env.NODE_ENV === 'development') {
109
+ tests = devTestData;
110
+ }
111
+ else {
112
+ tests = await store.values();
113
+ }
114
+ const testsByPath = buildTestsByPath(tests);
115
+ globalTestsCache = {
116
+ timestamp: now,
117
+ testsByPath,
118
+ };
119
+ return testsByPath;
120
+ }
121
+ catch (error) {
122
+ // biome-ignore lint/suspicious/noConsole: Error logging in middleware
123
+ console.error('Error fetching A/B tests:', error);
124
+ // Return stale cache if available, otherwise empty map
125
+ return globalTestsCache?.testsByPath || new Map();
126
+ }
127
+ }
128
+ /**
129
+ * Clear the test cache. Useful for testing or forcing a refresh.
130
+ */
131
+ export function clearTestCache() {
132
+ globalTestsCache = null;
133
+ }
134
+ /**
135
+ * Get the current cache state. Useful for debugging.
136
+ */
137
+ export function getTestCacheState() {
138
+ return globalTestsCache;
139
+ }
140
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { AbTestAssignment, AbTestCookie } from '../types';
2
+ /** Default cookie name for A/B test assignments */
3
+ export declare const DEFAULT_COOKIE_NAME = "ab-test-info";
4
+ /** Default cookie max age: 30 days in seconds */
5
+ export declare const DEFAULT_COOKIE_MAX_AGE: number;
6
+ /**
7
+ * Parse the A/B test cookie value into an assignment map.
8
+ *
9
+ * @param cookieValue - The raw cookie value string
10
+ * @returns Parsed assignment map, or empty object if invalid
11
+ */
12
+ export declare function parseCookie(cookieValue: string | undefined): AbTestCookie;
13
+ /**
14
+ * Serialize an assignment map to a cookie value string.
15
+ *
16
+ * @param assignments - The assignment map to serialize
17
+ * @returns Serialized cookie value
18
+ */
19
+ export declare function serializeCookie(assignments: AbTestCookie): string;
20
+ /**
21
+ * Get a specific test assignment from the cookie.
22
+ *
23
+ * @param cookie - Parsed cookie object
24
+ * @param testId - The test ID to look up
25
+ * @returns The assignment for this test, or undefined
26
+ */
27
+ export declare function getAssignment(cookie: AbTestCookie, testId: string): AbTestAssignment | undefined;
28
+ /**
29
+ * Set a test assignment in the cookie object.
30
+ *
31
+ * @param cookie - Parsed cookie object (will be mutated)
32
+ * @param testId - The test ID
33
+ * @param assignment - The assignment to store
34
+ */
35
+ export declare function setAssignment(cookie: AbTestCookie, testId: string, assignment: AbTestAssignment): void;
36
+ /**
37
+ * Read an A/B test cookie from a document.cookie string (client-side).
38
+ *
39
+ * @param documentCookie - The document.cookie string
40
+ * @param cookieName - The cookie name to look for
41
+ * @returns Parsed assignment map, or empty object if not found
42
+ */
43
+ export declare function readCookieFromDocument(documentCookie: string, cookieName?: string): AbTestCookie;
44
+ //# sourceMappingURL=cookies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/middleware/cookies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE/D,mDAAmD;AACnD,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAElD,iDAAiD;AACjD,eAAO,MAAM,sBAAsB,QAAoB,CAAC;AAExD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAWzE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,YAAY,GAAG,MAAM,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEhG;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CAEN;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,MAAM,EACtB,UAAU,GAAE,MAA4B,GACvC,YAAY,CAMd"}
@@ -0,0 +1,66 @@
1
+ /** Default cookie name for A/B test assignments */
2
+ export const DEFAULT_COOKIE_NAME = 'ab-test-info';
3
+ /** Default cookie max age: 30 days in seconds */
4
+ export const DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
5
+ /**
6
+ * Parse the A/B test cookie value into an assignment map.
7
+ *
8
+ * @param cookieValue - The raw cookie value string
9
+ * @returns Parsed assignment map, or empty object if invalid
10
+ */
11
+ export function parseCookie(cookieValue) {
12
+ if (!cookieValue) {
13
+ return {};
14
+ }
15
+ try {
16
+ const decoded = decodeURIComponent(cookieValue);
17
+ return JSON.parse(decoded);
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /**
24
+ * Serialize an assignment map to a cookie value string.
25
+ *
26
+ * @param assignments - The assignment map to serialize
27
+ * @returns Serialized cookie value
28
+ */
29
+ export function serializeCookie(assignments) {
30
+ return JSON.stringify(assignments);
31
+ }
32
+ /**
33
+ * Get a specific test assignment from the cookie.
34
+ *
35
+ * @param cookie - Parsed cookie object
36
+ * @param testId - The test ID to look up
37
+ * @returns The assignment for this test, or undefined
38
+ */
39
+ export function getAssignment(cookie, testId) {
40
+ return cookie[testId];
41
+ }
42
+ /**
43
+ * Set a test assignment in the cookie object.
44
+ *
45
+ * @param cookie - Parsed cookie object (will be mutated)
46
+ * @param testId - The test ID
47
+ * @param assignment - The assignment to store
48
+ */
49
+ export function setAssignment(cookie, testId, assignment) {
50
+ cookie[testId] = assignment;
51
+ }
52
+ /**
53
+ * Read an A/B test cookie from a document.cookie string (client-side).
54
+ *
55
+ * @param documentCookie - The document.cookie string
56
+ * @param cookieName - The cookie name to look for
57
+ * @returns Parsed assignment map, or empty object if not found
58
+ */
59
+ export function readCookieFromDocument(documentCookie, cookieName = DEFAULT_COOKIE_NAME) {
60
+ const match = documentCookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
61
+ if (!match?.[2]) {
62
+ return {};
63
+ }
64
+ return parseCookie(match[2]);
65
+ }
66
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/middleware/cookies.ts"],"names":[],"mappings":"AAEA,mDAAmD;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD,iDAAiD;AACjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAExD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,WAA+B;IACzD,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiB,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,WAAyB;IACvD,OAAO,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAoB,EAAE,MAAc;IAChE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAoB,EACpB,MAAc,EACd,UAA4B;IAE5B,MAAM,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC;AAC9B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,cAAsB,EACtB,aAAqB,mBAAmB;IAExC,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,UAAU,UAAU,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,53 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { NextResponse } from 'next/server';
3
+ import type { AbTestAssignment } from '../types';
4
+ import type { AbTestMiddlewareConfig, AbTestResult, CachedAbTest } from './types';
5
+ /**
6
+ * Create an A/B test middleware handler.
7
+ *
8
+ * This factory function returns a middleware handler that can be called
9
+ * from your Next.js middleware to process A/B test assignments.
10
+ *
11
+ * @param config - Middleware configuration
12
+ * @returns Async function that processes requests and returns NextResponse or null
13
+ */
14
+ export declare function createAbTestMiddleware(config: AbTestMiddlewareConfig): (request: NextRequest) => Promise<NextResponse | null>;
15
+ /**
16
+ * Low-level function to process an A/B test request.
17
+ * Use this if you need more control over the response handling.
18
+ */
19
+ export declare function processAbTestRequest(pathname: string, searchParams: URLSearchParams, cookieValue: string | undefined, store: ReturnType<AbTestMiddlewareConfig['getStore']>, cacheTtlMs: number, devTestData?: AbTestMiddlewareConfig['devTestData'], options?: Pick<AbTestMiddlewareConfig, 'pruneStaleAssignments' | 'respectExistingUrlParams' | 'onValidationFailure'>): Promise<AbTestResult>;
20
+ /**
21
+ * Options for the static A/B test middleware.
22
+ */
23
+ export interface StaticAbTestMiddlewareOptions {
24
+ /** Name of the cookie used to store A/B test assignments. @default "ab-test-info" */
25
+ cookieName?: string;
26
+ /** Cookie max age in seconds. @default 2592000 (30 days) */
27
+ cookieMaxAge?: number;
28
+ /**
29
+ * Path prefix to prepend when rewriting to a variant.
30
+ * e.g. "/page-test" → rewrites to "/page-test/slug" instead of "/slug".
31
+ * @default "" (no prefix)
32
+ */
33
+ variantPathPrefix?: string;
34
+ /** Optional function to skip processing for certain pathnames. */
35
+ shouldProcess?: (pathname: string) => boolean;
36
+ /** Optional callback when a cookie assignment fails validation. */
37
+ onValidationFailure?: (testId: string, assignment: AbTestAssignment) => void;
38
+ /**
39
+ * Remove cookie entries for tests no longer in the active config.
40
+ * @default true
41
+ */
42
+ pruneStaleAssignments?: boolean;
43
+ /**
44
+ * When injecting url_params, skip keys already present on the request URL.
45
+ * @default true
46
+ */
47
+ respectExistingUrlParams?: boolean;
48
+ }
49
+ /**
50
+ * Create a synchronous A/B test middleware from a pre-generated static testsByPath map.
51
+ */
52
+ export declare function createStaticAbTestMiddleware(staticTestsByPath: Record<string, CachedAbTest[]>, options?: StaticAbTestMiddlewareOptions): (request: NextRequest) => NextResponse | null;
53
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/middleware/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,UAAU,CAAC;AAY/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAkFlF;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,IAc5B,SAAS,WAAW,KAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAgE3F;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,eAAe,EAC7B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,KAAK,EAAE,UAAU,CAAC,sBAAsB,CAAC,UAAU,CAAC,CAAC,EACrD,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,sBAAsB,CAAC,aAAa,CAAC,EACnD,OAAO,GAAE,IAAI,CACX,sBAAsB,EACtB,uBAAuB,GAAG,0BAA0B,GAAG,qBAAqB,CACxE,GACL,OAAO,CAAC,YAAY,CAAC,CA2CvB;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,mEAAmE;IACnE,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC7E;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAC1C,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EACjD,OAAO,GAAE,6BAAkC,IAeJ,SAAS,WAAW,KAAG,YAAY,GAAG,IAAI,CAiDlF"}
@@ -0,0 +1,201 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { createAssignment, isValidAssignment, selectVariant } from './assignment';
3
+ import { getCachedTests, normalizePath } from './cache';
4
+ import { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, parseCookie, serializeCookie, setAssignment, } from './cookies';
5
+ import { getActiveTestIds, pruneStaleAssignments } from './prune';
6
+ import { applyUrlParams } from './urlParams';
7
+ function searchParamsMatch(test, searchParams) {
8
+ if (!test.searchParameters) {
9
+ return true;
10
+ }
11
+ const testSearchParams = new URLSearchParams(test.searchParameters);
12
+ for (const [key, value] of testSearchParams.entries()) {
13
+ if (searchParams.get(key) !== value) {
14
+ return false;
15
+ }
16
+ }
17
+ return true;
18
+ }
19
+ function resolveAssignment(test, cookie, normalizedPathname, onValidationFailure) {
20
+ let assignmentData = getAssignment(cookie, test.id);
21
+ if (assignmentData && !isValidAssignment(test, assignmentData)) {
22
+ onValidationFailure?.(test.id, assignmentData);
23
+ assignmentData = undefined;
24
+ }
25
+ if (!assignmentData) {
26
+ const { selectedKey, variantSlug } = selectVariant(test);
27
+ assignmentData = createAssignment(test, selectedKey, variantSlug, normalizedPathname);
28
+ setAssignment(cookie, test.id, assignmentData);
29
+ }
30
+ return assignmentData;
31
+ }
32
+ function buildRewriteUrl(assignment, variantPathPrefix) {
33
+ const variantSlug = assignment.variant_slug;
34
+ if (!variantSlug || variantSlug === 'control') {
35
+ return undefined;
36
+ }
37
+ const cleanSlug = variantSlug.startsWith('/') ? variantSlug.slice(1) : variantSlug;
38
+ return `${variantPathPrefix}/${cleanSlug}`;
39
+ }
40
+ function applyTestUrlParams(url, test, assignment, respectExistingUrlParams) {
41
+ if (!test.injectUrlParams) {
42
+ return false;
43
+ }
44
+ return applyUrlParams(url, assignment.url_params, respectExistingUrlParams);
45
+ }
46
+ function setCookieHeader(response, cookie, cookieName, cookieMaxAge) {
47
+ const cookieValue = serializeCookie(cookie);
48
+ if (cookieValue) {
49
+ response.cookies.set(cookieName, cookieValue, {
50
+ path: '/',
51
+ maxAge: cookieMaxAge,
52
+ sameSite: 'lax',
53
+ });
54
+ }
55
+ }
56
+ /**
57
+ * Create an A/B test middleware handler.
58
+ *
59
+ * This factory function returns a middleware handler that can be called
60
+ * from your Next.js middleware to process A/B test assignments.
61
+ *
62
+ * @param config - Middleware configuration
63
+ * @returns Async function that processes requests and returns NextResponse or null
64
+ */
65
+ export function createAbTestMiddleware(config) {
66
+ const { getStore, cacheTtlMs = 60000, cookieName = DEFAULT_COOKIE_NAME, cookieMaxAge = DEFAULT_COOKIE_MAX_AGE, shouldProcess, devTestData, onValidationFailure, variantPathPrefix = '', pruneStaleAssignments: pruneStale = true, respectExistingUrlParams = true, } = config;
67
+ return async function abTestMiddleware(request) {
68
+ const pathname = request.nextUrl.pathname;
69
+ if (shouldProcess && !shouldProcess(pathname)) {
70
+ return null;
71
+ }
72
+ try {
73
+ const normalizedPathname = normalizePath(pathname);
74
+ const store = await Promise.resolve(getStore());
75
+ const testsByPath = await getCachedTests(store, cacheTtlMs, devTestData);
76
+ const activeTestIds = pruneStale ? getActiveTestIds(testsByPath) : null;
77
+ const cookieValue = request.cookies.get(cookieName)?.value;
78
+ const cookie = parseCookie(cookieValue);
79
+ const prunedStale = activeTestIds ? pruneStaleAssignments(cookie, activeTestIds) : false;
80
+ const tests = testsByPath.get(normalizedPathname);
81
+ if (tests && tests.length > 0) {
82
+ for (const test of tests) {
83
+ if (!searchParamsMatch(test, request.nextUrl.searchParams)) {
84
+ continue;
85
+ }
86
+ const assignment = resolveAssignment(test, cookie, normalizedPathname, onValidationFailure);
87
+ const rewriteUrl = buildRewriteUrl(assignment, variantPathPrefix);
88
+ const url = request.nextUrl.clone();
89
+ if (rewriteUrl) {
90
+ url.pathname = rewriteUrl;
91
+ }
92
+ const paramsChanged = applyTestUrlParams(url, test, assignment, respectExistingUrlParams);
93
+ const urlChanged = url.pathname !== request.nextUrl.pathname || url.search !== request.nextUrl.search;
94
+ const response = rewriteUrl || paramsChanged || urlChanged
95
+ ? NextResponse.rewrite(url)
96
+ : NextResponse.next();
97
+ setCookieHeader(response, cookie, cookieName, cookieMaxAge);
98
+ return response;
99
+ }
100
+ }
101
+ if (prunedStale) {
102
+ const response = NextResponse.next();
103
+ setCookieHeader(response, cookie, cookieName, cookieMaxAge);
104
+ return response;
105
+ }
106
+ return null;
107
+ }
108
+ catch (error) {
109
+ // biome-ignore lint/suspicious/noConsole: Error logging in middleware
110
+ console.error('Middleware A/B test error:', error);
111
+ return null;
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Low-level function to process an A/B test request.
117
+ * Use this if you need more control over the response handling.
118
+ */
119
+ export async function processAbTestRequest(pathname, searchParams, cookieValue, store, cacheTtlMs, devTestData, options = {}) {
120
+ const { pruneStaleAssignments: pruneStale = true, onValidationFailure } = options;
121
+ const normalizedPathname = normalizePath(pathname);
122
+ const resolvedStore = await Promise.resolve(store);
123
+ const testsByPath = await getCachedTests(resolvedStore, cacheTtlMs, devTestData);
124
+ const activeTestIds = pruneStale ? getActiveTestIds(testsByPath) : null;
125
+ const cookie = parseCookie(cookieValue);
126
+ const prunedStale = activeTestIds ? pruneStaleAssignments(cookie, activeTestIds) : false;
127
+ const tests = testsByPath.get(normalizedPathname);
128
+ if (!tests || tests.length === 0) {
129
+ return {
130
+ matched: false,
131
+ prunedStale,
132
+ cookieValue: prunedStale ? serializeCookie(cookie) : undefined,
133
+ };
134
+ }
135
+ for (const test of tests) {
136
+ if (!searchParamsMatch(test, searchParams)) {
137
+ continue;
138
+ }
139
+ const assignment = resolveAssignment(test, cookie, normalizedPathname, onValidationFailure);
140
+ const rewriteUrl = buildRewriteUrl(assignment, '');
141
+ return {
142
+ matched: true,
143
+ test,
144
+ assignment,
145
+ rewriteUrl,
146
+ cookieValue: serializeCookie(cookie),
147
+ prunedStale,
148
+ };
149
+ }
150
+ return {
151
+ matched: false,
152
+ prunedStale,
153
+ cookieValue: prunedStale ? serializeCookie(cookie) : undefined,
154
+ };
155
+ }
156
+ /**
157
+ * Create a synchronous A/B test middleware from a pre-generated static testsByPath map.
158
+ */
159
+ export function createStaticAbTestMiddleware(staticTestsByPath, options = {}) {
160
+ const { cookieName = DEFAULT_COOKIE_NAME, cookieMaxAge = DEFAULT_COOKIE_MAX_AGE, variantPathPrefix = '', shouldProcess, onValidationFailure, pruneStaleAssignments: pruneStale = true, respectExistingUrlParams = true, } = options;
161
+ const testsByPathMap = new Map(Object.entries(staticTestsByPath));
162
+ const activeTestIds = pruneStale ? getActiveTestIds(testsByPathMap) : null;
163
+ return function staticAbTestMiddleware(request) {
164
+ const pathname = request.nextUrl.pathname;
165
+ if (shouldProcess && !shouldProcess(pathname)) {
166
+ return null;
167
+ }
168
+ const normalizedPathname = normalizePath(pathname);
169
+ const cookieValue = request.cookies.get(cookieName)?.value;
170
+ const cookie = parseCookie(cookieValue);
171
+ const prunedStale = activeTestIds ? pruneStaleAssignments(cookie, activeTestIds) : false;
172
+ const tests = testsByPathMap.get(normalizedPathname);
173
+ if (tests && tests.length > 0) {
174
+ for (const test of tests) {
175
+ if (!searchParamsMatch(test, request.nextUrl.searchParams)) {
176
+ continue;
177
+ }
178
+ const assignment = resolveAssignment(test, cookie, normalizedPathname, onValidationFailure);
179
+ const rewriteUrl = buildRewriteUrl(assignment, variantPathPrefix);
180
+ const url = request.nextUrl.clone();
181
+ if (rewriteUrl) {
182
+ url.pathname = rewriteUrl;
183
+ }
184
+ const paramsChanged = applyTestUrlParams(url, test, assignment, respectExistingUrlParams);
185
+ const urlChanged = url.pathname !== request.nextUrl.pathname || url.search !== request.nextUrl.search;
186
+ const response = rewriteUrl || paramsChanged || urlChanged
187
+ ? NextResponse.rewrite(url)
188
+ : NextResponse.next();
189
+ setCookieHeader(response, cookie, cookieName, cookieMaxAge);
190
+ return response;
191
+ }
192
+ }
193
+ if (prunedStale) {
194
+ const response = NextResponse.next();
195
+ setCookieHeader(response, cookie, cookieName, cookieMaxAge);
196
+ return response;
197
+ }
198
+ return null;
199
+ };
200
+ }
201
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/middleware/handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A/B Testing Middleware
3
+ *
4
+ * Helpers for implementing server-side A/B testing in Next.js middleware.
5
+ */
6
+ export { createAssignment, isValidAssignment, selectVariant } from './assignment';
7
+ export { clearTestCache, computeCachedTest, getCachedTests, getTestCacheState, normalizePath, } from './cache';
8
+ export { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, parseCookie, readCookieFromDocument, serializeCookie, setAssignment, } from './cookies';
9
+ export type { StaticAbTestMiddlewareOptions } from './handler';
10
+ export { createAbTestMiddleware, createStaticAbTestMiddleware, processAbTestRequest, } from './handler';
11
+ export { getActiveTestIds, pruneStaleAssignments } from './prune';
12
+ export type { AbTestMiddlewareConfig, AbTestResult, CachedAbTest, TestsCache, } from './types';
13
+ export { applyUrlParams } from './urlParams';
14
+ export { buildVariantId } from './variantId';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,YAAY,EACV,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A/B Testing Middleware
3
+ *
4
+ * Helpers for implementing server-side A/B testing in Next.js middleware.
5
+ */
6
+ export { createAssignment, isValidAssignment, selectVariant } from './assignment';
7
+ export { clearTestCache, computeCachedTest, getCachedTests, getTestCacheState, normalizePath, } from './cache';
8
+ export { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, parseCookie, readCookieFromDocument, serializeCookie, setAssignment, } from './cookies';
9
+ export { createAbTestMiddleware, createStaticAbTestMiddleware, processAbTestRequest, } from './handler';
10
+ export { getActiveTestIds, pruneStaleAssignments } from './prune';
11
+ export { applyUrlParams } from './urlParams';
12
+ export { buildVariantId } from './variantId';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,oBAAoB,GACrB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAOlE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { AbTestCookie } from '../types';
2
+ import type { CachedAbTest } from './types';
3
+ /**
4
+ * Collect all active test IDs from a testsByPath map.
5
+ */
6
+ export declare function getActiveTestIds(testsByPath: Record<string, CachedAbTest[]> | Map<string, CachedAbTest[]>): Set<string>;
7
+ /**
8
+ * Remove cookie entries for tests no longer in the active config.
9
+ *
10
+ * @returns true if any keys were removed
11
+ */
12
+ export declare function pruneStaleAssignments(cookie: AbTestCookie, activeTestIds: Set<string>): boolean;
13
+ //# sourceMappingURL=prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../../src/middleware/prune.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,GACxE,GAAG,CAAC,MAAM,CAAC,CAWb;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAW/F"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Collect all active test IDs from a testsByPath map.
3
+ */
4
+ export function getActiveTestIds(testsByPath) {
5
+ const ids = new Set();
6
+ const testLists = testsByPath instanceof Map ? testsByPath.values() : Object.values(testsByPath);
7
+ for (const tests of testLists) {
8
+ for (const test of tests) {
9
+ ids.add(test.id);
10
+ }
11
+ }
12
+ return ids;
13
+ }
14
+ /**
15
+ * Remove cookie entries for tests no longer in the active config.
16
+ *
17
+ * @returns true if any keys were removed
18
+ */
19
+ export function pruneStaleAssignments(cookie, activeTestIds) {
20
+ let pruned = false;
21
+ for (const testId of Object.keys(cookie)) {
22
+ if (!activeTestIds.has(testId)) {
23
+ delete cookie[testId];
24
+ pruned = true;
25
+ }
26
+ }
27
+ return pruned;
28
+ }
29
+ //# sourceMappingURL=prune.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.js","sourceRoot":"","sources":["../../src/middleware/prune.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAyE;IAEzE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,MAAM,SAAS,GAAG,WAAW,YAAY,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAEjG,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAoB,EAAE,aAA0B;IACpF,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/B,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC;YACtB,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}