@se-studio/ab-testing 1.0.0
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.
- package/README.md +393 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useAbTestAssignments.d.ts +55 -0
- package/dist/hooks/useAbTestAssignments.d.ts.map +1 -0
- package/dist/hooks/useAbTestAssignments.js +87 -0
- package/dist/hooks/useAbTestAssignments.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/assignment.d.ts +36 -0
- package/dist/middleware/assignment.d.ts.map +1 -0
- package/dist/middleware/assignment.js +91 -0
- package/dist/middleware/assignment.js.map +1 -0
- package/dist/middleware/cache.d.ts +26 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +128 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/cookies.d.ts +44 -0
- package/dist/middleware/cookies.d.ts.map +1 -0
- package/dist/middleware/cookies.js +66 -0
- package/dist/middleware/cookies.js.map +1 -0
- package/dist/middleware/handler.d.ts +45 -0
- package/dist/middleware/handler.d.ts.map +1 -0
- package/dist/middleware/handler.js +189 -0
- package/dist/middleware/handler.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +10 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/types.d.ts +78 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/middleware/types.js +2 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook/handler.d.ts +116 -0
- package/dist/webhook/handler.d.ts.map +1 -0
- package/dist/webhook/handler.js +123 -0
- package/dist/webhook/handler.js.map +1 -0
- package/dist/webhook/index.d.ts +8 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/dist/webhook/index.js +7 -0
- package/dist/webhook/index.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perform weighted random selection to assign a variant.
|
|
3
|
+
*
|
|
4
|
+
* @param test - The cached test with pre-computed weights
|
|
5
|
+
* @returns Object with selectedKey (variant ID or "control") and variant slug
|
|
6
|
+
*/
|
|
7
|
+
export function selectVariant(test) {
|
|
8
|
+
const rand = Math.random();
|
|
9
|
+
let sum = 0;
|
|
10
|
+
let selectedKey = 'control';
|
|
11
|
+
for (const [key, weight] of Object.entries(test.readyWeights)) {
|
|
12
|
+
sum += weight;
|
|
13
|
+
if (rand < sum) {
|
|
14
|
+
selectedKey = key;
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const variantSlug = test.variantLookup[selectedKey] || 'control';
|
|
19
|
+
return { selectedKey, variantSlug };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create an assignment object for a newly selected variant.
|
|
23
|
+
*
|
|
24
|
+
* @param test - The cached test
|
|
25
|
+
* @param selectedKey - The selected variant ID or "control"
|
|
26
|
+
* @param variantSlug - The slug of the selected variant
|
|
27
|
+
* @param originalPath - The original URL path where the test was assigned
|
|
28
|
+
* @returns Assignment object to store in cookie
|
|
29
|
+
*/
|
|
30
|
+
export function createAssignment(test, selectedKey, variantSlug, originalPath) {
|
|
31
|
+
return {
|
|
32
|
+
test_label: test.trackingLabel || test.cmsLabel,
|
|
33
|
+
test_path: variantSlug === 'control' ? 'control' : variantSlug,
|
|
34
|
+
hubspot_event: test.hubspotEventLookup[selectedKey],
|
|
35
|
+
original_path: originalPath,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate an existing cookie assignment against current CMS config.
|
|
40
|
+
* Checks that:
|
|
41
|
+
* - original_path is present
|
|
42
|
+
* - test_label matches current config
|
|
43
|
+
* - test_path (variant) exists and has weight > 0
|
|
44
|
+
* - hubspot_event matches current config
|
|
45
|
+
*
|
|
46
|
+
* @param test - The cached test with current config
|
|
47
|
+
* @param assignment - The assignment from the cookie
|
|
48
|
+
* @returns true if assignment is still valid, false if it should be re-assigned
|
|
49
|
+
*/
|
|
50
|
+
export function isValidAssignment(test, assignment) {
|
|
51
|
+
const { test_label, test_path, hubspot_event, original_path } = assignment;
|
|
52
|
+
// 1. Validate original_path is present (required for path-based reporting)
|
|
53
|
+
if (!original_path) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// 2. Validate test_label matches current config
|
|
57
|
+
const expectedLabel = test.trackingLabel || test.cmsLabel;
|
|
58
|
+
if (test_label !== expectedLabel) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// 3. Validate test_path and get the variant key for hubspot lookup
|
|
62
|
+
let variantKey;
|
|
63
|
+
if (test_path === 'control') {
|
|
64
|
+
if ((test.readyWeights['control'] ?? 0) <= 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
variantKey = 'control';
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// Find variant ID by slug
|
|
71
|
+
for (const [id, slug] of Object.entries(test.variantLookup)) {
|
|
72
|
+
if (slug === test_path && id !== 'control') {
|
|
73
|
+
if ((test.readyWeights[id] ?? 0) <= 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
variantKey = id;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!variantKey) {
|
|
81
|
+
return false; // Slug not found
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 4. Validate hubspot_event matches current config
|
|
85
|
+
const expectedHubspotEvent = test.hubspotEventLookup[variantKey];
|
|
86
|
+
if (hubspot_event !== expectedHubspotEvent) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=assignment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assignment.js","sourceRoot":"","sources":["../../src/middleware/assignment.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,IAAkB;IAC9C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC3B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,WAAW,GAAG,SAAS,CAAC;IAE5B,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9D,GAAG,IAAI,MAAM,CAAC;QACd,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;YACf,WAAW,GAAG,GAAG,CAAC;YAClB,MAAM;QACR,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,SAAS,CAAC;IACjE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC;AACtC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAkB,EAClB,WAAmB,EACnB,WAAmB,EACnB,YAAoB;IAEpB,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,QAAQ;QAC/C,SAAS,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC9D,aAAa,EAAE,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC;QACnD,aAAa,EAAE,YAAY;KAC5B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAkB,EAAE,UAA4B;IAChF,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,UAAU,CAAC;IAE3E,2EAA2E;IAC3E,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gDAAgD;IAChD,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,QAAQ,CAAC;IAC1D,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;QACjC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,mEAAmE;IACnE,IAAI,UAA8B,CAAC;IAEnC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAC;QACf,CAAC;QACD,UAAU,GAAG,SAAS,CAAC;IACzB,CAAC;SAAM,CAAC;QACN,0BAA0B;QAC1B,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;YAC5D,IAAI,IAAI,KAAK,SAAS,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;gBAC3C,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;oBACtC,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,UAAU,GAAG,EAAE,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,CAAC,iBAAiB;QACjC,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,MAAM,oBAAoB,GAAG,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;IACjE,IAAI,aAAa,KAAK,oBAAoB,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AbTest, IBlobStore } from '../types';
|
|
2
|
+
import type { CachedAbTest, TestsCache } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a URL path for consistent matching.
|
|
5
|
+
* - Removes trailing slashes (except for root)
|
|
6
|
+
* - Ensures leading slash
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizePath(path: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Get cached tests, refreshing from store if needed.
|
|
11
|
+
*
|
|
12
|
+
* @param store - The blob store to fetch tests from
|
|
13
|
+
* @param cacheTtlMs - Cache time-to-live in milliseconds
|
|
14
|
+
* @param devTestData - Optional test data for development mode
|
|
15
|
+
* @returns Map of tests indexed by normalized control path
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCachedTests(store: IBlobStore<AbTest>, cacheTtlMs: number, devTestData?: AbTest[]): Promise<Map<string, CachedAbTest[]>>;
|
|
18
|
+
/**
|
|
19
|
+
* Clear the test cache. Useful for testing or forcing a refresh.
|
|
20
|
+
*/
|
|
21
|
+
export declare function clearTestCache(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current cache state. Useful for debugging.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getTestCacheState(): TestsCache | null;
|
|
26
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAmDxD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAelD;AAsBD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,EACzB,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC,CAgCtC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,UAAU,GAAG,IAAI,CAErD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Global cache instance */
|
|
2
|
+
let globalTestsCache = null;
|
|
3
|
+
/**
|
|
4
|
+
* Pre-compute weights and lookups for a test.
|
|
5
|
+
*/
|
|
6
|
+
function computeCachedTest(test) {
|
|
7
|
+
const readyWeights = {};
|
|
8
|
+
const hubspotEventLookup = {};
|
|
9
|
+
const config = test.configuration;
|
|
10
|
+
if (config && config.length > 0) {
|
|
11
|
+
// Control weight (index 0)
|
|
12
|
+
readyWeights['control'] = config[0]?.weight ?? 0;
|
|
13
|
+
hubspotEventLookup['control'] = config[0]?.hubspot_event_name;
|
|
14
|
+
// Variant weights (index 1+)
|
|
15
|
+
for (let i = 0; i < test.variants.length; i++) {
|
|
16
|
+
const variantConfig = config[i + 1];
|
|
17
|
+
const variant = test.variants[i];
|
|
18
|
+
if (variant) {
|
|
19
|
+
readyWeights[variant.id] = variantConfig?.weight ?? 0;
|
|
20
|
+
hubspotEventLookup[variant.id] = variantConfig?.hubspot_event_name;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
// Fallback: equal weights if no configuration
|
|
26
|
+
const totalOptions = test.variants.length + 1;
|
|
27
|
+
const equalWeight = 1 / totalOptions;
|
|
28
|
+
readyWeights['control'] = equalWeight;
|
|
29
|
+
for (const variant of test.variants) {
|
|
30
|
+
readyWeights[variant.id] = equalWeight;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Pre-compute variant lookup (ID -> Slug)
|
|
34
|
+
const variantLookup = { control: 'control' };
|
|
35
|
+
for (const variant of test.variants) {
|
|
36
|
+
variantLookup[variant.id] = variant.slug;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...test,
|
|
40
|
+
readyWeights,
|
|
41
|
+
variantLookup,
|
|
42
|
+
hubspotEventLookup,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Normalize a URL path for consistent matching.
|
|
47
|
+
* - Removes trailing slashes (except for root)
|
|
48
|
+
* - Ensures leading slash
|
|
49
|
+
*/
|
|
50
|
+
export function normalizePath(path) {
|
|
51
|
+
// Handle "index" as root
|
|
52
|
+
if (path === 'index') {
|
|
53
|
+
return '/';
|
|
54
|
+
}
|
|
55
|
+
// Ensure leading slash
|
|
56
|
+
let normalized = path.startsWith('/') ? path : `/${path}`;
|
|
57
|
+
// Remove trailing slash (except for root)
|
|
58
|
+
if (normalized.endsWith('/') && normalized.length > 1) {
|
|
59
|
+
normalized = normalized.slice(0, -1);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build cached tests map from raw tests.
|
|
65
|
+
*/
|
|
66
|
+
function buildTestsByPath(tests) {
|
|
67
|
+
const testsByPath = new Map();
|
|
68
|
+
for (const test of tests) {
|
|
69
|
+
if (!test.enabled)
|
|
70
|
+
continue;
|
|
71
|
+
const normalizedControlPath = normalizePath(test.controlSlug);
|
|
72
|
+
const cachedTest = computeCachedTest(test);
|
|
73
|
+
const existing = testsByPath.get(normalizedControlPath) || [];
|
|
74
|
+
existing.push(cachedTest);
|
|
75
|
+
testsByPath.set(normalizedControlPath, existing);
|
|
76
|
+
}
|
|
77
|
+
return testsByPath;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get cached tests, refreshing from store if needed.
|
|
81
|
+
*
|
|
82
|
+
* @param store - The blob store to fetch tests from
|
|
83
|
+
* @param cacheTtlMs - Cache time-to-live in milliseconds
|
|
84
|
+
* @param devTestData - Optional test data for development mode
|
|
85
|
+
* @returns Map of tests indexed by normalized control path
|
|
86
|
+
*/
|
|
87
|
+
export async function getCachedTests(store, cacheTtlMs, devTestData) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
// Return cached data if still valid
|
|
90
|
+
if (globalTestsCache && now - globalTestsCache.timestamp < cacheTtlMs) {
|
|
91
|
+
return globalTestsCache.testsByPath;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
let tests;
|
|
95
|
+
// Use dev test data if provided
|
|
96
|
+
if (devTestData && process.env.NODE_ENV === 'development') {
|
|
97
|
+
tests = devTestData;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
tests = await store.values();
|
|
101
|
+
}
|
|
102
|
+
const testsByPath = buildTestsByPath(tests);
|
|
103
|
+
globalTestsCache = {
|
|
104
|
+
timestamp: now,
|
|
105
|
+
testsByPath,
|
|
106
|
+
};
|
|
107
|
+
return testsByPath;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// biome-ignore lint/suspicious/noConsole: Error logging in middleware
|
|
111
|
+
console.error('Error fetching A/B tests:', error);
|
|
112
|
+
// Return stale cache if available, otherwise empty map
|
|
113
|
+
return globalTestsCache?.testsByPath || new Map();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Clear the test cache. Useful for testing or forcing a refresh.
|
|
118
|
+
*/
|
|
119
|
+
export function clearTestCache() {
|
|
120
|
+
globalTestsCache = null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get the current cache state. Useful for debugging.
|
|
124
|
+
*/
|
|
125
|
+
export function getTestCacheState() {
|
|
126
|
+
return globalTestsCache;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/middleware/cache.ts"],"names":[],"mappings":"AAGA,4BAA4B;AAC5B,IAAI,gBAAgB,GAAsB,IAAI,CAAC;AAE/C;;GAEG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,YAAY,GAA2B,EAAE,CAAC;IAChD,MAAM,kBAAkB,GAAuC,EAAE,CAAC;IAClE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC;IAElC,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChC,2BAA2B;QAC3B,YAAY,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;QACjD,kBAAkB,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC;QAE9D,6BAA6B;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9C,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACpC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;YACjC,IAAI,OAAO,EAAE,CAAC;gBACZ,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,MAAM,IAAI,CAAC,CAAC;gBACtD,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,kBAAkB,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,8CAA8C;QAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAC9C,MAAM,WAAW,GAAG,CAAC,GAAG,YAAY,CAAC;QACrC,YAAY,CAAC,SAAS,CAAC,GAAG,WAAW,CAAC;QACtC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC;QACzC,CAAC;IACH,CAAC;IAED,0CAA0C;IAC1C,MAAM,aAAa,GAA2B,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACrE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpC,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,CAAC;IAED,OAAO;QACL,GAAG,IAAI;QACP,YAAY;QACZ,aAAa;QACb,kBAAkB;KACnB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,yBAAyB;IACzB,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,uBAAuB;IACvB,IAAI,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;IAE1D,0CAA0C;IAC1C,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB,CAAC,KAAe;IACvC,MAAM,WAAW,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEtD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,SAAS;QAE5B,MAAM,qBAAqB,GAAG,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9D,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAE3C,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAC;QAC9D,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1B,WAAW,CAAC,GAAG,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,KAAyB,EACzB,UAAkB,EAClB,WAAsB;IAEtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,oCAAoC;IACpC,IAAI,gBAAgB,IAAI,GAAG,GAAG,gBAAgB,CAAC,SAAS,GAAG,UAAU,EAAE,CAAC;QACtE,OAAO,gBAAgB,CAAC,WAAW,CAAC;IACtC,CAAC;IAED,IAAI,CAAC;QACH,IAAI,KAAe,CAAC;QAEpB,gCAAgC;QAChC,IAAI,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YAC1D,KAAK,GAAG,WAAW,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,KAAK,GAAG,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;QAC/B,CAAC;QAED,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAE5C,gBAAgB,GAAG;YACjB,SAAS,EAAE,GAAG;YACd,WAAW;SACZ,CAAC;QAEF,OAAO,WAAW,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,sEAAsE;QACtE,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;QAClD,uDAAuD;QACvD,OAAO,gBAAgB,EAAE,WAAW,IAAI,IAAI,GAAG,EAAE,CAAC;IACpD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc;IAC5B,gBAAgB,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,gBAAgB,CAAC;AAC1B,CAAC"}
|
|
@@ -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 || !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,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NextRequest } from 'next/server';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import type { AbTestMiddlewareConfig, AbTestResult } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Create an A/B test middleware handler.
|
|
6
|
+
*
|
|
7
|
+
* This factory function returns a middleware handler that can be called
|
|
8
|
+
* from your Next.js middleware to process A/B test assignments.
|
|
9
|
+
*
|
|
10
|
+
* @param config - Middleware configuration
|
|
11
|
+
* @returns Async function that processes requests and returns NextResponse or null
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // middleware.ts
|
|
16
|
+
* import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';
|
|
17
|
+
* import { getAbTestStore } from './server/abTestStore';
|
|
18
|
+
*
|
|
19
|
+
* const abTestHandler = createAbTestMiddleware({
|
|
20
|
+
* getStore: getAbTestStore,
|
|
21
|
+
* cacheTtlMs: 60000,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* export async function middleware(request: NextRequest) {
|
|
25
|
+
* const abResponse = await abTestHandler(request);
|
|
26
|
+
* if (abResponse) return abResponse;
|
|
27
|
+
* return NextResponse.next();
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function createAbTestMiddleware(config: AbTestMiddlewareConfig): (request: NextRequest) => Promise<NextResponse | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Low-level function to process an A/B test request.
|
|
34
|
+
* Use this if you need more control over the response handling.
|
|
35
|
+
*
|
|
36
|
+
* @param pathname - The request pathname
|
|
37
|
+
* @param searchParams - The request search params
|
|
38
|
+
* @param cookieValue - The current A/B test cookie value
|
|
39
|
+
* @param store - The blob store to fetch tests from
|
|
40
|
+
* @param cacheTtlMs - Cache TTL in milliseconds
|
|
41
|
+
* @param devTestData - Optional dev test data
|
|
42
|
+
* @returns Result object with test matching info and new cookie value
|
|
43
|
+
*/
|
|
44
|
+
export declare function processAbTestRequest(pathname: string, searchParams: URLSearchParams, cookieValue: string | undefined, store: ReturnType<AbTestMiddlewareConfig['getStore']>, cacheTtlMs: number, devTestData?: AbTestMiddlewareConfig['devTestData']): Promise<AbTestResult>;
|
|
45
|
+
//# 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;AAY3C,OAAO,KAAK,EAAE,sBAAsB,EAAE,YAAY,EAAgB,MAAM,SAAS,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,IAmE5B,SAAS,WAAW,KAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA0D3F;AAED;;;;;;;;;;;GAWG;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,GAClD,OAAO,CAAC,YAAY,CAAC,CAwDvB"}
|
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
/**
|
|
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
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // middleware.ts
|
|
17
|
+
* import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';
|
|
18
|
+
* import { getAbTestStore } from './server/abTestStore';
|
|
19
|
+
*
|
|
20
|
+
* const abTestHandler = createAbTestMiddleware({
|
|
21
|
+
* getStore: getAbTestStore,
|
|
22
|
+
* cacheTtlMs: 60000,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* export async function middleware(request: NextRequest) {
|
|
26
|
+
* const abResponse = await abTestHandler(request);
|
|
27
|
+
* if (abResponse) return abResponse;
|
|
28
|
+
* return NextResponse.next();
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createAbTestMiddleware(config) {
|
|
33
|
+
const { getStore, cacheTtlMs = 60000, cookieName = DEFAULT_COOKIE_NAME, cookieMaxAge = DEFAULT_COOKIE_MAX_AGE, shouldProcess, devTestData, onValidationFailure, } = config;
|
|
34
|
+
/**
|
|
35
|
+
* Process a single test against the request.
|
|
36
|
+
*/
|
|
37
|
+
function processTest(test, normalizedPathname, searchParams, cookie) {
|
|
38
|
+
// Check search parameters matching
|
|
39
|
+
if (test.searchParameters) {
|
|
40
|
+
const testSearchParams = new URLSearchParams(test.searchParameters);
|
|
41
|
+
for (const [key, value] of testSearchParams.entries()) {
|
|
42
|
+
if (searchParams.get(key) !== value) {
|
|
43
|
+
return null; // Params don't match
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Get existing assignment from cookie
|
|
48
|
+
let assignmentData = getAssignment(cookie, test.id);
|
|
49
|
+
// Validate existing assignment against current CMS config
|
|
50
|
+
if (assignmentData && !isValidAssignment(test, assignmentData)) {
|
|
51
|
+
onValidationFailure?.(test.id, assignmentData);
|
|
52
|
+
assignmentData = undefined; // Force re-assignment
|
|
53
|
+
}
|
|
54
|
+
// Create new assignment if needed
|
|
55
|
+
if (!assignmentData) {
|
|
56
|
+
const { selectedKey, variantSlug } = selectVariant(test);
|
|
57
|
+
assignmentData = createAssignment(test, selectedKey, variantSlug, normalizedPathname);
|
|
58
|
+
setAssignment(cookie, test.id, assignmentData);
|
|
59
|
+
}
|
|
60
|
+
// Determine if we need to rewrite
|
|
61
|
+
const variantSlug = assignmentData.test_path;
|
|
62
|
+
const rewriteUrl = variantSlug !== 'control' && variantSlug
|
|
63
|
+
? variantSlug.startsWith('/')
|
|
64
|
+
? variantSlug
|
|
65
|
+
: `/${variantSlug}`
|
|
66
|
+
: undefined;
|
|
67
|
+
return {
|
|
68
|
+
matched: true,
|
|
69
|
+
test,
|
|
70
|
+
assignment: assignmentData,
|
|
71
|
+
rewriteUrl,
|
|
72
|
+
cookieValue: serializeCookie(cookie),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Main middleware handler.
|
|
77
|
+
*/
|
|
78
|
+
return async function abTestMiddleware(request) {
|
|
79
|
+
const pathname = request.nextUrl.pathname;
|
|
80
|
+
// Check if we should process this request
|
|
81
|
+
if (shouldProcess && !shouldProcess(pathname)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const normalizedPathname = normalizePath(pathname);
|
|
86
|
+
const store = await Promise.resolve(getStore());
|
|
87
|
+
const testsByPath = await getCachedTests(store, cacheTtlMs, devTestData);
|
|
88
|
+
const tests = testsByPath.get(normalizedPathname);
|
|
89
|
+
if (!tests || tests.length === 0) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Parse existing cookie
|
|
93
|
+
const cookieValue = request.cookies.get(cookieName)?.value;
|
|
94
|
+
const cookie = parseCookie(cookieValue);
|
|
95
|
+
// Process each test for this path
|
|
96
|
+
for (const test of tests) {
|
|
97
|
+
const result = processTest(test, normalizedPathname, request.nextUrl.searchParams, cookie);
|
|
98
|
+
if (result?.matched) {
|
|
99
|
+
// Create response
|
|
100
|
+
let response;
|
|
101
|
+
if (result.rewriteUrl) {
|
|
102
|
+
const url = request.nextUrl.clone();
|
|
103
|
+
url.pathname = result.rewriteUrl;
|
|
104
|
+
response = NextResponse.rewrite(url);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
response = NextResponse.next();
|
|
108
|
+
}
|
|
109
|
+
// Set cookie with assignment
|
|
110
|
+
if (result.cookieValue) {
|
|
111
|
+
response.cookies.set(cookieName, result.cookieValue, {
|
|
112
|
+
path: '/',
|
|
113
|
+
maxAge: cookieMaxAge,
|
|
114
|
+
sameSite: 'lax',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// biome-ignore lint/suspicious/noConsole: Error logging in middleware
|
|
124
|
+
console.error('Middleware A/B test error:', error);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Low-level function to process an A/B test request.
|
|
131
|
+
* Use this if you need more control over the response handling.
|
|
132
|
+
*
|
|
133
|
+
* @param pathname - The request pathname
|
|
134
|
+
* @param searchParams - The request search params
|
|
135
|
+
* @param cookieValue - The current A/B test cookie value
|
|
136
|
+
* @param store - The blob store to fetch tests from
|
|
137
|
+
* @param cacheTtlMs - Cache TTL in milliseconds
|
|
138
|
+
* @param devTestData - Optional dev test data
|
|
139
|
+
* @returns Result object with test matching info and new cookie value
|
|
140
|
+
*/
|
|
141
|
+
export async function processAbTestRequest(pathname, searchParams, cookieValue, store, cacheTtlMs, devTestData) {
|
|
142
|
+
const normalizedPathname = normalizePath(pathname);
|
|
143
|
+
const resolvedStore = await Promise.resolve(store);
|
|
144
|
+
const testsByPath = await getCachedTests(resolvedStore, cacheTtlMs, devTestData);
|
|
145
|
+
const tests = testsByPath.get(normalizedPathname);
|
|
146
|
+
if (!tests || tests.length === 0) {
|
|
147
|
+
return { matched: false };
|
|
148
|
+
}
|
|
149
|
+
const cookie = parseCookie(cookieValue);
|
|
150
|
+
for (const test of tests) {
|
|
151
|
+
// Check search parameters
|
|
152
|
+
if (test.searchParameters) {
|
|
153
|
+
const testSearchParams = new URLSearchParams(test.searchParameters);
|
|
154
|
+
let paramsMatch = true;
|
|
155
|
+
for (const [key, value] of testSearchParams.entries()) {
|
|
156
|
+
if (searchParams.get(key) !== value) {
|
|
157
|
+
paramsMatch = false;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!paramsMatch)
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let assignmentData = getAssignment(cookie, test.id);
|
|
165
|
+
if (assignmentData && !isValidAssignment(test, assignmentData)) {
|
|
166
|
+
assignmentData = undefined;
|
|
167
|
+
}
|
|
168
|
+
if (!assignmentData) {
|
|
169
|
+
const { selectedKey, variantSlug } = selectVariant(test);
|
|
170
|
+
assignmentData = createAssignment(test, selectedKey, variantSlug, normalizedPathname);
|
|
171
|
+
setAssignment(cookie, test.id, assignmentData);
|
|
172
|
+
}
|
|
173
|
+
const variantSlug = assignmentData.test_path;
|
|
174
|
+
const rewriteUrl = variantSlug !== 'control' && variantSlug
|
|
175
|
+
? variantSlug.startsWith('/')
|
|
176
|
+
? variantSlug
|
|
177
|
+
: `/${variantSlug}`
|
|
178
|
+
: undefined;
|
|
179
|
+
return {
|
|
180
|
+
matched: true,
|
|
181
|
+
test,
|
|
182
|
+
assignment: assignmentData,
|
|
183
|
+
rewriteUrl,
|
|
184
|
+
cookieValue: serializeCookie(cookie),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { matched: false };
|
|
188
|
+
}
|
|
189
|
+
//# 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,11 @@
|
|
|
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, getCachedTests, getTestCacheState, normalizePath } from './cache';
|
|
8
|
+
export { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, parseCookie, readCookieFromDocument, serializeCookie, setAssignment, } from './cookies';
|
|
9
|
+
export { createAbTestMiddleware, processAbTestRequest } from './handler';
|
|
10
|
+
export type { AbTestMiddlewareConfig, AbTestResult, CachedAbTest, TestsCache, } from './types';
|
|
11
|
+
//# 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,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC3F,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,sBAAsB,EACtB,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACzE,YAAY,EACV,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,UAAU,GACX,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
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, getCachedTests, getTestCacheState, normalizePath } from './cache';
|
|
8
|
+
export { DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, parseCookie, readCookieFromDocument, serializeCookie, setAssignment, } from './cookies';
|
|
9
|
+
export { createAbTestMiddleware, processAbTestRequest } from './handler';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|