@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.
Files changed (50) hide show
  1. package/README.md +393 -0
  2. package/dist/hooks/index.d.ts +8 -0
  3. package/dist/hooks/index.d.ts.map +1 -0
  4. package/dist/hooks/index.js +7 -0
  5. package/dist/hooks/index.js.map +1 -0
  6. package/dist/hooks/useAbTestAssignments.d.ts +55 -0
  7. package/dist/hooks/useAbTestAssignments.d.ts.map +1 -0
  8. package/dist/hooks/useAbTestAssignments.js +87 -0
  9. package/dist/hooks/useAbTestAssignments.js.map +1 -0
  10. package/dist/index.d.ts +15 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +14 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/middleware/assignment.d.ts +36 -0
  15. package/dist/middleware/assignment.d.ts.map +1 -0
  16. package/dist/middleware/assignment.js +91 -0
  17. package/dist/middleware/assignment.js.map +1 -0
  18. package/dist/middleware/cache.d.ts +26 -0
  19. package/dist/middleware/cache.d.ts.map +1 -0
  20. package/dist/middleware/cache.js +128 -0
  21. package/dist/middleware/cache.js.map +1 -0
  22. package/dist/middleware/cookies.d.ts +44 -0
  23. package/dist/middleware/cookies.d.ts.map +1 -0
  24. package/dist/middleware/cookies.js +66 -0
  25. package/dist/middleware/cookies.js.map +1 -0
  26. package/dist/middleware/handler.d.ts +45 -0
  27. package/dist/middleware/handler.d.ts.map +1 -0
  28. package/dist/middleware/handler.js +189 -0
  29. package/dist/middleware/handler.js.map +1 -0
  30. package/dist/middleware/index.d.ts +11 -0
  31. package/dist/middleware/index.d.ts.map +1 -0
  32. package/dist/middleware/index.js +10 -0
  33. package/dist/middleware/index.js.map +1 -0
  34. package/dist/middleware/types.d.ts +78 -0
  35. package/dist/middleware/types.d.ts.map +1 -0
  36. package/dist/middleware/types.js +2 -0
  37. package/dist/middleware/types.js.map +1 -0
  38. package/dist/types.d.ts +125 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +7 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/webhook/handler.d.ts +116 -0
  43. package/dist/webhook/handler.d.ts.map +1 -0
  44. package/dist/webhook/handler.js +123 -0
  45. package/dist/webhook/handler.js.map +1 -0
  46. package/dist/webhook/index.d.ts +8 -0
  47. package/dist/webhook/index.d.ts.map +1 -0
  48. package/dist/webhook/index.js +7 -0
  49. package/dist/webhook/index.js.map +1 -0
  50. package/package.json +74 -0
package/README.md ADDED
@@ -0,0 +1,393 @@
1
+ # @se-studio/ab-testing
2
+
3
+ Server-side A/B testing framework for Next.js applications with Contentful CMS.
4
+
5
+ ## Features
6
+
7
+ - **Server-side rendering**: No flicker or layout shift - variants are rendered on the server
8
+ - **Edge-optimized**: Middleware runs at the edge for minimal latency
9
+ - **CMS-driven**: Test configuration managed entirely in Contentful
10
+ - **Provider-agnostic**: Bring your own blob storage (Vercel KV, Netlify Blobs, etc.)
11
+ - **Flexible analytics**: Use the `useAbTestAssignments` hook to integrate with any analytics platform
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
17
+ │ Contentful │────▶│ Webhook Handler │────▶│ Blob Store │
18
+ │ PageTest │ │ (API Route) │ │ (Project- │
19
+ │ Entries │ │ │ │ specific) │
20
+ └─────────────────┘ └──────────────────┘ └────────┬────────┘
21
+
22
+
23
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
24
+ │ User Request │────▶│ Middleware │────▶│ Variant Page │
25
+ │ /pricing │ │ (Edge) │ │ (SSR) │
26
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
27
+
28
+
29
+ ┌──────────────────┐
30
+ │ Cookie Set │
31
+ │ ab-test-info │
32
+ └────────┬─────────┘
33
+
34
+
35
+ ┌──────────────────┐
36
+ │ useAbTestAssign- │
37
+ │ ments Hook │
38
+ │ → Your Analytics │
39
+ └──────────────────┘
40
+ ```
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pnpm add @se-studio/ab-testing
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ### 1. Implement Your Blob Store
51
+
52
+ The package requires you to implement the `IBlobStore<AbTest>` interface for your hosting platform.
53
+
54
+ **Vercel KV Example:**
55
+
56
+ ```typescript
57
+ // src/server/abTestStore.ts
58
+ import { kv } from '@vercel/kv';
59
+ import type { IBlobStore, AbTest } from '@se-studio/ab-testing';
60
+
61
+ const STORE_KEY = 'ab-tests';
62
+
63
+ export function getAbTestStore(): IBlobStore<AbTest> {
64
+ return {
65
+ async get(key) {
66
+ const data = await kv.hget<AbTest>(STORE_KEY, key);
67
+ return data ?? undefined;
68
+ },
69
+ async set(key, value) {
70
+ await kv.hset(STORE_KEY, { [key]: value });
71
+ },
72
+ async bulkWrite(entries) {
73
+ await kv.del(STORE_KEY);
74
+ if (entries.length > 0) {
75
+ const data = Object.fromEntries(entries);
76
+ await kv.hset(STORE_KEY, data);
77
+ }
78
+ },
79
+ async size() {
80
+ return kv.hlen(STORE_KEY);
81
+ },
82
+ async values() {
83
+ const data = await kv.hgetall<Record<string, AbTest>>(STORE_KEY);
84
+ return data ? Object.values(data) : [];
85
+ },
86
+ };
87
+ }
88
+ ```
89
+
90
+ **Netlify Blobs Example:**
91
+
92
+ ```typescript
93
+ // src/server/abTestStore.ts
94
+ import { getStore } from '@netlify/blobs';
95
+ import type { IBlobStore, AbTest } from '@se-studio/ab-testing';
96
+
97
+ export function getAbTestStore(): IBlobStore<AbTest> {
98
+ const store = getStore('ab-testing');
99
+ const BLOB_NAME = 'config';
100
+
101
+ return {
102
+ async get(key) {
103
+ const data = await store.get(BLOB_NAME, { type: 'json' });
104
+ return data?.[key];
105
+ },
106
+ async set(key, value) {
107
+ const data = (await store.get(BLOB_NAME, { type: 'json' })) ?? {};
108
+ data[key] = value;
109
+ await store.setJSON(BLOB_NAME, data);
110
+ },
111
+ async bulkWrite(entries) {
112
+ const data = Object.fromEntries(entries);
113
+ await store.setJSON(BLOB_NAME, data);
114
+ },
115
+ async size() {
116
+ const data = await store.get(BLOB_NAME, { type: 'json' });
117
+ return data ? Object.keys(data).length : 0;
118
+ },
119
+ async values() {
120
+ const data = await store.get(BLOB_NAME, { type: 'json' });
121
+ return data ? Object.values(data) : [];
122
+ },
123
+ };
124
+ }
125
+ ```
126
+
127
+ ### 2. Set Up the Middleware
128
+
129
+ ```typescript
130
+ // src/middleware.ts
131
+ import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';
132
+ import { getAbTestStore } from './server/abTestStore';
133
+ import { NextResponse } from 'next/server';
134
+ import type { NextRequest } from 'next/server';
135
+
136
+ const abTestHandler = createAbTestMiddleware({
137
+ getStore: getAbTestStore,
138
+ cacheTtlMs: 60000, // 60 second cache
139
+ });
140
+
141
+ export async function middleware(request: NextRequest) {
142
+ // Skip static assets, API routes, etc.
143
+ if (request.nextUrl.pathname.startsWith('/_next') ||
144
+ request.nextUrl.pathname.startsWith('/api')) {
145
+ return NextResponse.next();
146
+ }
147
+
148
+ // Process A/B tests
149
+ const abResponse = await abTestHandler(request);
150
+ if (abResponse) return abResponse;
151
+
152
+ return NextResponse.next();
153
+ }
154
+
155
+ export const config = {
156
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
157
+ };
158
+ ```
159
+
160
+ ### 3. Create the Webhook Handler
161
+
162
+ ```typescript
163
+ // src/app/api/webhooks/ab-test/route.ts
164
+ import { createWebhookHandler } from '@se-studio/ab-testing/webhook';
165
+ import { getAbTestStore } from '@/server/abTestStore';
166
+ import { revalidateTag } from 'next/cache';
167
+
168
+ // Your Contentful fetch function
169
+ async function fetchPageTests() {
170
+ // Fetch all PageTest entries from Contentful
171
+ // Return array of RawPageTest objects
172
+ }
173
+
174
+ export const dynamic = 'force-dynamic';
175
+
176
+ export const POST = createWebhookHandler({
177
+ fetchPageTests,
178
+ getStore: getAbTestStore,
179
+ webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET,
180
+ revalidate: () => revalidateTag('PageTests'),
181
+ });
182
+ ```
183
+
184
+ ### 4. Create Your Analytics Reporter (Project-Specific)
185
+
186
+ The package provides a `useAbTestAssignments` hook that returns test assignments for the current page. You create your own reporter component with your project's specific analytics integrations.
187
+
188
+ ```tsx
189
+ // src/components/AbTestReporter.tsx (project-specific)
190
+ 'use client';
191
+
192
+ import { useEffect } from 'react';
193
+ import { useAbTestAssignments } from '@se-studio/ab-testing';
194
+ import { sendEvent } from '@/lib/analytics';
195
+
196
+ export function AbTestReporter() {
197
+ const assignments = useAbTestAssignments();
198
+
199
+ useEffect(() => {
200
+ for (const assignment of assignments) {
201
+ // Send to GTM/GA4
202
+ sendEvent('experiment_impression', {
203
+ experiment_id: assignment.testId,
204
+ experiment_name: assignment.test_label,
205
+ variant_id: assignment.test_path,
206
+ });
207
+ }
208
+ }, [assignments]);
209
+
210
+ return null;
211
+ }
212
+ ```
213
+
214
+ **With HubSpot Integration (HSD-style):**
215
+
216
+ ```tsx
217
+ // src/components/AbTestReporter.tsx
218
+ 'use client';
219
+
220
+ import { useEffect } from 'react';
221
+ import { useAbTestAssignments } from '@se-studio/ab-testing';
222
+ import { sendEvent } from '@/lib/analytics';
223
+ import { sendHubspotCustomEvent } from '@/lib/hubspotCustomEvents';
224
+
225
+ export function AbTestReporter() {
226
+ const assignments = useAbTestAssignments();
227
+
228
+ useEffect(() => {
229
+ for (const assignment of assignments) {
230
+ // Send to GTM/GA4
231
+ sendEvent('experiment_impression', {
232
+ experiment_id: assignment.testId,
233
+ experiment_name: assignment.test_label,
234
+ variant_id: assignment.test_path,
235
+ });
236
+
237
+ // Send to HubSpot (if configured)
238
+ if (assignment.hubspot_event) {
239
+ sendHubspotCustomEvent(assignment.hubspot_event, {
240
+ experiment_name: assignment.test_label,
241
+ experiment_id: assignment.testId,
242
+ });
243
+ }
244
+ }
245
+ }, [assignments]);
246
+
247
+ return null;
248
+ }
249
+ ```
250
+
251
+ **Add to your layout:**
252
+
253
+ ```tsx
254
+ // src/app/layout.tsx
255
+ import { AbTestReporter } from '@/components/AbTestReporter';
256
+
257
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
258
+ return (
259
+ <html>
260
+ <body>
261
+ {children}
262
+ <AbTestReporter />
263
+ </body>
264
+ </html>
265
+ );
266
+ }
267
+ ```
268
+
269
+ ## Contentful Content Model
270
+
271
+ ### PageTest
272
+
273
+ | Field | Type | Description |
274
+ |-------|------|-------------|
275
+ | cmsLabel | Symbol | Internal label for the test |
276
+ | control | Reference (Page/PageVariant) | The control page to test against |
277
+ | enabled | Boolean | Whether the test is active |
278
+ | trackingLabel | Symbol (optional) | Override for analytics label |
279
+ | searchParameters | Symbol (optional) | URL params to match (e.g., "utm_source=google") |
280
+ | configuration | JSON | Array of `{weight, hubspot_event_name?}` |
281
+ | variants | References (PageVariant[]) | Variants to test |
282
+
283
+ ### PageVariant
284
+
285
+ Existing content type that references an original page and defines component swaps.
286
+
287
+ ## API Reference
288
+
289
+ ### Types
290
+
291
+ ```typescript
292
+ interface AbTest {
293
+ id: string;
294
+ cmsLabel: string;
295
+ controlSlug: string;
296
+ searchParameters?: string;
297
+ trackingLabel?: string;
298
+ enabled: boolean;
299
+ configuration: AbTestVariantConfig[];
300
+ variants: AbTestVariant[];
301
+ }
302
+
303
+ interface IBlobStore<T> {
304
+ get(key: string): Promise<T | undefined>;
305
+ set(key: string, value: T): Promise<void>;
306
+ bulkWrite(entries: [string, T][]): Promise<void>;
307
+ size(): Promise<number>;
308
+ values(): Promise<T[]>;
309
+ }
310
+
311
+ interface ActiveAbTestAssignment {
312
+ testId: string;
313
+ test_label: string;
314
+ test_path: string;
315
+ hubspot_event?: string;
316
+ original_path?: string;
317
+ }
318
+ ```
319
+
320
+ ### Middleware
321
+
322
+ ```typescript
323
+ import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';
324
+
325
+ const handler = createAbTestMiddleware({
326
+ getStore: () => store, // Required: blob store factory
327
+ cacheTtlMs: 60000, // Optional: cache TTL (default: 60s)
328
+ cookieName: 'ab-test-info', // Optional: cookie name
329
+ cookieMaxAge: 2592000, // Optional: cookie max age (default: 30 days)
330
+ shouldProcess: (path) => true, // Optional: filter requests
331
+ devTestData: [], // Optional: test data for development
332
+ });
333
+ ```
334
+
335
+ ### Webhook
336
+
337
+ ```typescript
338
+ import { createWebhookHandler } from '@se-studio/ab-testing/webhook';
339
+
340
+ export const POST = createWebhookHandler({
341
+ fetchPageTests: () => Promise<RawPageTest[]>, // Required
342
+ getStore: () => store, // Required
343
+ webhookSecret: 'secret', // Optional
344
+ revalidate: () => void, // Optional
345
+ onSkippedTest: (id, reason) => void, // Optional
346
+ });
347
+ ```
348
+
349
+ ### Hook
350
+
351
+ ```typescript
352
+ import { useAbTestAssignments } from '@se-studio/ab-testing';
353
+
354
+ // Returns array of assignments for the current page
355
+ const assignments = useAbTestAssignments({
356
+ cookieName: 'ab-test-info', // Optional: custom cookie name
357
+ });
358
+
359
+ // Each assignment contains:
360
+ // - testId: string
361
+ // - test_label: string
362
+ // - test_path: string (variant slug or "control")
363
+ // - hubspot_event?: string
364
+ // - original_path?: string
365
+ ```
366
+
367
+ ## Analytics Integration
368
+
369
+ ### Google Tag Manager (GTM)
370
+
371
+ Push events to `window.dataLayer`:
372
+
373
+ ```typescript
374
+ window.dataLayer.push({
375
+ event: 'experiment_impression',
376
+ experiment_id: assignment.testId,
377
+ experiment_name: assignment.test_label,
378
+ variant_id: assignment.test_path,
379
+ });
380
+ ```
381
+
382
+ ### Google Analytics 4 (GA4)
383
+
384
+ Register custom dimensions in GA4:
385
+ 1. Go to Admin > Data display > Custom definitions
386
+ 2. Create event-scoped dimensions for:
387
+ - `experiment_id`
388
+ - `experiment_name`
389
+ - `variant_id`
390
+
391
+ ## License
392
+
393
+ MIT
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A/B Testing Hooks
3
+ *
4
+ * React hooks for client-side A/B test reporting.
5
+ */
6
+ export type { ActiveAbTestAssignment, UseAbTestAssignmentsOptions } from './useAbTestAssignments';
7
+ export { useAbTestAssignments } from './useAbTestAssignments';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAClG,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A/B Testing Hooks
3
+ *
4
+ * React hooks for client-side A/B test reporting.
5
+ */
6
+ export { useAbTestAssignments } from './useAbTestAssignments';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { AbTestAssignment } from '../types';
2
+ /**
3
+ * An A/B test assignment with its test ID included.
4
+ */
5
+ export interface ActiveAbTestAssignment extends AbTestAssignment {
6
+ /** Contentful entry ID of the PageTest */
7
+ testId: string;
8
+ }
9
+ /**
10
+ * Options for the useAbTestAssignments hook.
11
+ */
12
+ export interface UseAbTestAssignmentsOptions {
13
+ /**
14
+ * Cookie name to read assignments from.
15
+ * @default "ab-test-info"
16
+ */
17
+ cookieName?: string;
18
+ }
19
+ /**
20
+ * Hook that returns A/B test assignments for the current page.
21
+ *
22
+ * This hook reads the A/B test cookie and returns only the assignments
23
+ * that match the current page path. Projects can use this to build
24
+ * their own analytics reporting with project-specific integrations.
25
+ *
26
+ * @param options - Hook configuration options
27
+ * @returns Array of active test assignments for the current page
28
+ *
29
+ * @example Basic usage
30
+ * ```tsx
31
+ * 'use client';
32
+ *
33
+ * import { useEffect } from 'react';
34
+ * import { useAbTestAssignments } from '@se-studio/ab-testing';
35
+ *
36
+ * export function AbTestReporter() {
37
+ * const assignments = useAbTestAssignments();
38
+ *
39
+ * useEffect(() => {
40
+ * for (const assignment of assignments) {
41
+ * // Send to your analytics
42
+ * sendEvent('experiment_impression', {
43
+ * experiment_id: assignment.testId,
44
+ * experiment_name: assignment.test_label,
45
+ * variant_id: assignment.test_path,
46
+ * });
47
+ * }
48
+ * }, [assignments]);
49
+ *
50
+ * return null;
51
+ * }
52
+ * ```
53
+ */
54
+ export declare function useAbTestAssignments(options?: UseAbTestAssignmentsOptions): ActiveAbTestAssignment[];
55
+ //# sourceMappingURL=useAbTestAssignments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAbTestAssignments.d.ts","sourceRoot":"","sources":["../../src/hooks/useAbTestAssignments.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,CAAC,EAAE,2BAA2B,GACpC,sBAAsB,EAAE,CA8C1B"}
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { DEFAULT_COOKIE_NAME, readCookieFromDocument } from '../middleware/cookies';
4
+ /**
5
+ * Normalize a path for comparison (remove trailing slash except for root).
6
+ */
7
+ function normalizePath(path) {
8
+ if (path.endsWith('/') && path.length > 1) {
9
+ return path.slice(0, -1);
10
+ }
11
+ return path;
12
+ }
13
+ /**
14
+ * Hook that returns A/B test assignments for the current page.
15
+ *
16
+ * This hook reads the A/B test cookie and returns only the assignments
17
+ * that match the current page path. Projects can use this to build
18
+ * their own analytics reporting with project-specific integrations.
19
+ *
20
+ * @param options - Hook configuration options
21
+ * @returns Array of active test assignments for the current page
22
+ *
23
+ * @example Basic usage
24
+ * ```tsx
25
+ * 'use client';
26
+ *
27
+ * import { useEffect } from 'react';
28
+ * import { useAbTestAssignments } from '@se-studio/ab-testing';
29
+ *
30
+ * export function AbTestReporter() {
31
+ * const assignments = useAbTestAssignments();
32
+ *
33
+ * useEffect(() => {
34
+ * for (const assignment of assignments) {
35
+ * // Send to your analytics
36
+ * sendEvent('experiment_impression', {
37
+ * experiment_id: assignment.testId,
38
+ * experiment_name: assignment.test_label,
39
+ * variant_id: assignment.test_path,
40
+ * });
41
+ * }
42
+ * }, [assignments]);
43
+ *
44
+ * return null;
45
+ * }
46
+ * ```
47
+ */
48
+ export function useAbTestAssignments(options) {
49
+ const cookieName = options?.cookieName ?? DEFAULT_COOKIE_NAME;
50
+ const [assignments, setAssignments] = useState([]);
51
+ useEffect(() => {
52
+ try {
53
+ const allAssignments = readCookieFromDocument(document.cookie, cookieName);
54
+ if (Object.keys(allAssignments).length === 0) {
55
+ setAssignments([]);
56
+ return;
57
+ }
58
+ // Get and normalize current browser path
59
+ const currentPath = window.location.pathname;
60
+ const normalizedCurrentPath = normalizePath(currentPath);
61
+ // Filter to only assignments for the current page
62
+ const activeAssignments = [];
63
+ for (const [testId, assignment] of Object.entries(allAssignments)) {
64
+ // Skip if original_path is not present
65
+ if (!assignment.original_path) {
66
+ continue;
67
+ }
68
+ // Skip if not on the original path where the test was assigned
69
+ if (assignment.original_path !== normalizedCurrentPath) {
70
+ continue;
71
+ }
72
+ activeAssignments.push({
73
+ ...assignment,
74
+ testId,
75
+ });
76
+ }
77
+ setAssignments(activeAssignments);
78
+ }
79
+ catch (e) {
80
+ // biome-ignore lint/suspicious/noConsole: Error logging
81
+ console.error('[useAbTestAssignments] Error reading A/B test cookie', e);
82
+ setAssignments([]);
83
+ }
84
+ }, [cookieName]);
85
+ return assignments;
86
+ }
87
+ //# sourceMappingURL=useAbTestAssignments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAbTestAssignments.js","sourceRoot":"","sources":["../../src/hooks/useAbTestAssignments.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAsBpF;;GAEG;AACH,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAqC;IAErC,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,mBAAmB,CAAC;IAC9D,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAA2B,EAAE,CAAC,CAAC;IAE7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC;YACH,MAAM,cAAc,GAAG,sBAAsB,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YAE3E,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC7C,cAAc,CAAC,EAAE,CAAC,CAAC;gBACnB,OAAO;YACT,CAAC;YAED,yCAAyC;YACzC,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC7C,MAAM,qBAAqB,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;YAEzD,kDAAkD;YAClD,MAAM,iBAAiB,GAA6B,EAAE,CAAC;YAEvD,KAAK,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;gBAClE,uCAAuC;gBACvC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,CAAC;oBAC9B,SAAS;gBACX,CAAC;gBAED,+DAA+D;gBAC/D,IAAI,UAAU,CAAC,aAAa,KAAK,qBAAqB,EAAE,CAAC;oBACvD,SAAS;gBACX,CAAC;gBAED,iBAAiB,CAAC,IAAI,CAAC;oBACrB,GAAG,UAAU;oBACb,MAAM;iBACP,CAAC,CAAC;YACL,CAAC;YAED,cAAc,CAAC,iBAAiB,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,wDAAwD;YACxD,OAAO,CAAC,KAAK,CAAC,sDAAsD,EAAE,CAAC,CAAC,CAAC;YACzE,cAAc,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAEjB,OAAO,WAAW,CAAC;AACrB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @se-studio/ab-testing
3
+ *
4
+ * Server-side A/B testing framework for Next.js applications with Contentful CMS.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ export type { ActiveAbTestAssignment, UseAbTestAssignmentsOptions } from './hooks';
9
+ export { useAbTestAssignments } from './hooks';
10
+ export type { AbTestMiddlewareConfig, AbTestResult, CachedAbTest, TestsCache, } from './middleware';
11
+ export { clearTestCache, createAbTestMiddleware, createAssignment, DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, getCachedTests, getTestCacheState, isValidAssignment, normalizePath, parseCookie, processAbTestRequest, readCookieFromDocument, selectVariant, serializeCookie, setAssignment, } from './middleware';
12
+ export type { AbTest, AbTestAssignment, AbTestCookie, AbTestVariant, AbTestVariantConfig, IBlobStore, } from './types';
13
+ export type { RawPageTest, WebhookHandlerConfig, WebhookResult, } from './webhook';
14
+ export { createWebhookHandler, processAbTestWebhook, transformPageTest, } from './webhook';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAEnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,YAAY,EACV,sBAAsB,EACtB,YAAY,EACZ,YAAY,EACZ,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,oBAAoB,EACpB,sBAAsB,EACtB,aAAa,EACb,eAAe,EACf,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,MAAM,EACN,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @se-studio/ab-testing
3
+ *
4
+ * Server-side A/B testing framework for Next.js applications with Contentful CMS.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ // Re-export hooks
9
+ export { useAbTestAssignments } from './hooks';
10
+ // Re-export middleware utilities
11
+ export { clearTestCache, createAbTestMiddleware, createAssignment, DEFAULT_COOKIE_MAX_AGE, DEFAULT_COOKIE_NAME, getAssignment, getCachedTests, getTestCacheState, isValidAssignment, normalizePath, parseCookie, processAbTestRequest, readCookieFromDocument, selectVariant, serializeCookie, setAssignment, } from './middleware';
12
+ // Re-export webhook utilities
13
+ export { createWebhookHandler, processAbTestWebhook, transformPageTest, } from './webhook';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,kBAAkB;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAO/C,iCAAiC;AACjC,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACX,oBAAoB,EACpB,sBAAsB,EACtB,aAAa,EACb,eAAe,EACf,aAAa,GACd,MAAM,cAAc,CAAC;AAetB,8BAA8B;AAC9B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { AbTestAssignment } from '../types';
2
+ import type { CachedAbTest } from './types';
3
+ /**
4
+ * Perform weighted random selection to assign a variant.
5
+ *
6
+ * @param test - The cached test with pre-computed weights
7
+ * @returns Object with selectedKey (variant ID or "control") and variant slug
8
+ */
9
+ export declare function selectVariant(test: CachedAbTest): {
10
+ selectedKey: string;
11
+ variantSlug: string;
12
+ };
13
+ /**
14
+ * Create an assignment object for a newly selected variant.
15
+ *
16
+ * @param test - The cached test
17
+ * @param selectedKey - The selected variant ID or "control"
18
+ * @param variantSlug - The slug of the selected variant
19
+ * @param originalPath - The original URL path where the test was assigned
20
+ * @returns Assignment object to store in cookie
21
+ */
22
+ export declare function createAssignment(test: CachedAbTest, selectedKey: string, variantSlug: string, originalPath: string): AbTestAssignment;
23
+ /**
24
+ * Validate an existing cookie assignment against current CMS config.
25
+ * Checks that:
26
+ * - original_path is present
27
+ * - test_label matches current config
28
+ * - test_path (variant) exists and has weight > 0
29
+ * - hubspot_event matches current config
30
+ *
31
+ * @param test - The cached test with current config
32
+ * @param assignment - The assignment from the cookie
33
+ * @returns true if assignment is still valid, false if it should be re-assigned
34
+ */
35
+ export declare function isValidAssignment(test: CachedAbTest, assignment: AbTestAssignment): boolean;
36
+ //# sourceMappingURL=assignment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignment.d.ts","sourceRoot":"","sources":["../../src/middleware/assignment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAe9F;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,YAAY,EAClB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,gBAAgB,CAOlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CA6C3F"}