@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,103 @@
1
+ import type { AbTest, AbTestAssignment, IBlobStore } from '../types';
2
+ /**
3
+ * Cached test data with pre-computed weights and lookups for fast middleware execution.
4
+ */
5
+ export interface CachedAbTest extends AbTest {
6
+ /** Pre-computed weights map: variantId/control -> weight */
7
+ readyWeights: Record<string, number>;
8
+ /** Pre-computed lookup: variantId/control -> slug */
9
+ variantLookup: Record<string, string>;
10
+ /** Pre-computed lookup: variantId/control -> variant_label */
11
+ variantLabelLookup: Record<string, string | undefined>;
12
+ /** Pre-computed lookup: variantId/control -> reporting event name */
13
+ reportingEventLookup: Record<string, string | undefined>;
14
+ /** Pre-computed lookup: variantId/control -> url_params */
15
+ urlParamsLookup: Record<string, Record<string, string> | undefined>;
16
+ /** Pre-computed lookup: variantId/control -> reporting_params */
17
+ reportingParamsLookup: Record<string, Record<string, string> | undefined>;
18
+ }
19
+ /**
20
+ * In-memory cache structure for A/B tests.
21
+ */
22
+ export interface TestsCache {
23
+ /** Timestamp when the cache was last refreshed */
24
+ timestamp: number;
25
+ /** Tests indexed by normalized control path */
26
+ testsByPath: Map<string, CachedAbTest[]>;
27
+ }
28
+ /**
29
+ * Configuration for the A/B test middleware.
30
+ */
31
+ export interface AbTestMiddlewareConfig {
32
+ /**
33
+ * Factory function that returns the blob store instance.
34
+ * Called when the cache needs to be refreshed.
35
+ */
36
+ getStore: () => IBlobStore<AbTest> | Promise<IBlobStore<AbTest>>;
37
+ /**
38
+ * Cache time-to-live in milliseconds.
39
+ * @default 60000 (60 seconds)
40
+ */
41
+ cacheTtlMs?: number;
42
+ /**
43
+ * Name of the cookie used to store A/B test assignments.
44
+ * @default "ab-test-info"
45
+ */
46
+ cookieName?: string;
47
+ /**
48
+ * Cookie max age in seconds.
49
+ * @default 2592000 (30 days)
50
+ */
51
+ cookieMaxAge?: number;
52
+ /**
53
+ * Optional function to determine if middleware should process this request.
54
+ * Return false to skip A/B testing for this request.
55
+ * @default Always returns true
56
+ */
57
+ shouldProcess?: (pathname: string) => boolean;
58
+ /**
59
+ * Optional test data for development mode.
60
+ * When provided and in development, this data is used instead of fetching from store.
61
+ */
62
+ devTestData?: AbTest[];
63
+ /**
64
+ * Optional callback for validation failures.
65
+ * Called when a cookie assignment fails validation against current config.
66
+ */
67
+ onValidationFailure?: (testId: string, assignment: AbTestAssignment) => void;
68
+ /**
69
+ * Path prefix to prepend when rewriting to a variant.
70
+ * e.g. "/page-test" → rewrites to "/page-test/slug" instead of "/slug".
71
+ * Useful to keep variant URLs on an internal route excluded from robots.txt.
72
+ * @default "" (no prefix — rewrite directly to slug)
73
+ */
74
+ variantPathPrefix?: string;
75
+ /**
76
+ * Remove cookie entries for tests no longer in the active config.
77
+ * @default true
78
+ */
79
+ pruneStaleAssignments?: boolean;
80
+ /**
81
+ * When injecting url_params, skip keys already present on the request URL.
82
+ * @default true
83
+ */
84
+ respectExistingUrlParams?: boolean;
85
+ }
86
+ /**
87
+ * Result of processing an A/B test request.
88
+ */
89
+ export interface AbTestResult {
90
+ /** Whether a test was matched and processed */
91
+ matched: boolean;
92
+ /** The test that was matched (if any) */
93
+ test?: CachedAbTest;
94
+ /** The assignment for this user (if any) */
95
+ assignment?: AbTestAssignment;
96
+ /** The URL to rewrite to (if variant assigned) */
97
+ rewriteUrl?: string;
98
+ /** Updated cookie value to set */
99
+ cookieValue?: string;
100
+ /** Whether stale cookie keys were pruned */
101
+ prunedStale?: boolean;
102
+ }
103
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,MAAM;IAC1C,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,8DAA8D;IAC9D,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,qEAAqE;IACrE,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,2DAA2D;IAC3D,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACpE,iEAAiE;IACjE,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;CAC3E;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;CAC1C;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,EAAE,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IAEjE;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAE9C;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAE7E;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAEhC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,OAAO,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/middleware/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Apply url_params to a URL, optionally respecting existing query keys.
3
+ *
4
+ * @returns true if any param was set
5
+ */
6
+ export declare function applyUrlParams(url: URL, urlParams: Record<string, string> | undefined, respectExisting: boolean): boolean;
7
+ //# sourceMappingURL=urlParams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlParams.d.ts","sourceRoot":"","sources":["../../src/middleware/urlParams.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC7C,eAAe,EAAE,OAAO,GACvB,OAAO,CAeT"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Apply url_params to a URL, optionally respecting existing query keys.
3
+ *
4
+ * @returns true if any param was set
5
+ */
6
+ export function applyUrlParams(url, urlParams, respectExisting) {
7
+ if (!urlParams) {
8
+ return false;
9
+ }
10
+ let changed = false;
11
+ for (const [key, value] of Object.entries(urlParams)) {
12
+ if (respectExisting && url.searchParams.has(key)) {
13
+ continue;
14
+ }
15
+ url.searchParams.set(key, value);
16
+ changed = true;
17
+ }
18
+ return changed;
19
+ }
20
+ //# sourceMappingURL=urlParams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlParams.js","sourceRoot":"","sources":["../../src/middleware/urlParams.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAQ,EACR,SAA6C,EAC7C,eAAwB;IAExB,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,IAAI,eAAe,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjD,SAAS;QACX,CAAC;QACD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Build the variant_id string for a bucket assignment.
3
+ *
4
+ * @param isControl - Whether this is the control bucket
5
+ * @param variantLabel - CMS variant_label for the bucket
6
+ * @param fallbackSlug - Fallback label when variant_label is absent
7
+ */
8
+ export declare function buildVariantId(isControl: boolean, variantLabel: string | undefined, fallbackSlug: string): string;
9
+ //# sourceMappingURL=variantId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variantId.d.ts","sourceRoot":"","sources":["../../src/middleware/variantId.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,GACnB,MAAM,CAGR"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Build the variant_id string for a bucket assignment.
3
+ *
4
+ * @param isControl - Whether this is the control bucket
5
+ * @param variantLabel - CMS variant_label for the bucket
6
+ * @param fallbackSlug - Fallback label when variant_label is absent
7
+ */
8
+ export function buildVariantId(isControl, variantLabel, fallbackSlug) {
9
+ const label = variantLabel ?? fallbackSlug;
10
+ return `${isControl ? 'control' : 'variant'}:${label}`;
11
+ }
12
+ //# sourceMappingURL=variantId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variantId.js","sourceRoot":"","sources":["../../src/middleware/variantId.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,SAAkB,EAClB,YAAgC,EAChC,YAAoB;IAEpB,MAAM,KAAK,GAAG,YAAY,IAAI,YAAY,CAAC;IAC3C,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAAI,KAAK,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { AbTestAssignment } from '../types';
2
+ /**
3
+ * Snake_case experiment_impression payload for analytics adapters.
4
+ */
5
+ export declare function buildExperimentImpression(testId: string, assignment: AbTestAssignment): Record<string, string>;
6
+ //# sourceMappingURL=buildExperimentImpression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildExperimentImpression.d.ts","sourceRoot":"","sources":["../../src/reporting/buildExperimentImpression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,gBAAgB,GAC3B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBxB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Snake_case experiment_impression payload for analytics adapters.
3
+ */
4
+ export function buildExperimentImpression(testId, assignment) {
5
+ const payload = {
6
+ experiment_id: testId,
7
+ experiment_name: assignment.experiment_name,
8
+ variant_id: assignment.variant_id,
9
+ variant_slug: assignment.variant_slug,
10
+ original_path: assignment.original_path,
11
+ };
12
+ if (assignment.reporting_event) {
13
+ payload.reporting_event = assignment.reporting_event;
14
+ }
15
+ if (assignment.reporting_params) {
16
+ for (const [key, value] of Object.entries(assignment.reporting_params)) {
17
+ payload[key] = value;
18
+ }
19
+ }
20
+ return payload;
21
+ }
22
+ //# sourceMappingURL=buildExperimentImpression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildExperimentImpression.js","sourceRoot":"","sources":["../../src/reporting/buildExperimentImpression.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAc,EACd,UAA4B;IAE5B,MAAM,OAAO,GAA2B;QACtC,aAAa,EAAE,MAAM;QACrB,eAAe,EAAE,UAAU,CAAC,eAAe;QAC3C,UAAU,EAAE,UAAU,CAAC,UAAU;QACjC,YAAY,EAAE,UAAU,CAAC,YAAY;QACrC,aAAa,EAAE,UAAU,CAAC,aAAa;KACxC,CAAC;IAEF,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;QAC/B,OAAO,CAAC,eAAe,GAAG,UAAU,CAAC,eAAe,CAAC;IACvD,CAAC;IAED,IAAI,UAAU,CAAC,gBAAgB,EAAE,CAAC;QAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACvE,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * A/B Testing Types
3
+ *
4
+ * Core type definitions for server-side A/B testing.
5
+ */
6
+ /**
7
+ * Represents a single variant in an A/B test.
8
+ */
9
+ export interface AbTestVariant {
10
+ /** Contentful entry ID of the PageVariant */
11
+ id: string;
12
+ /** URL slug of the variant page */
13
+ slug: string;
14
+ }
15
+ /**
16
+ * Configuration for a single variant in an A/B test.
17
+ * Index 0 is the control, subsequent indices correspond to variants.
18
+ */
19
+ export interface AbTestVariantConfig {
20
+ /** Traffic weight for this variant (0-1, all weights should sum to 1) */
21
+ weight: number;
22
+ /** CMS label for this bucket; drives variant_id suffix */
23
+ variant_label?: string;
24
+ /** Optional per-bucket analytics event name */
25
+ reporting_event?: string;
26
+ /** Query params to inject when injectUrlParams is enabled on the test */
27
+ url_params?: Record<string, string>;
28
+ /** Arbitrary key-value pairs merged into experiment_impression payloads */
29
+ reporting_params?: Record<string, string>;
30
+ }
31
+ /**
32
+ * A/B test configuration stored in the blob store.
33
+ * This is the runtime representation of a PageTest entry.
34
+ */
35
+ export interface AbTest {
36
+ /** Contentful entry ID of the PageTest */
37
+ id: string;
38
+ /** Internal CMS label for the test */
39
+ cmsLabel: string;
40
+ /** URL path slug of the control page (e.g., "pricing" or "lp/special-offer") */
41
+ controlSlug: string;
42
+ /** Optional URL query parameters to match (e.g., "utm_source=google") */
43
+ searchParameters?: string;
44
+ /** Analytics experiment name (defaults to cmsLabel) */
45
+ trackingLabel?: string;
46
+ /** Whether the test is currently active */
47
+ enabled: boolean;
48
+ /** When true, inject url_params from the assigned bucket into the request URL */
49
+ injectUrlParams?: boolean;
50
+ /** Array of variant configurations with weights. Index 0 = control, 1+ = variants */
51
+ configuration: AbTestVariantConfig[];
52
+ /** Array of variants to test against the control */
53
+ variants: AbTestVariant[];
54
+ }
55
+ /**
56
+ * Assignment data stored in the A/B test cookie.
57
+ * Each test has its own assignment keyed by test ID.
58
+ */
59
+ export interface AbTestAssignment {
60
+ /** Analytics experiment name for this test */
61
+ experiment_name: string;
62
+ /** Bucket identifier, e.g. "control:normal-cta" or "variant:home-kids" */
63
+ variant_id: string;
64
+ /** Slug of the assigned variant, or "control" */
65
+ variant_slug: string;
66
+ /** Original URL path where the test was assigned */
67
+ original_path: string;
68
+ /** Optional per-bucket analytics event name */
69
+ reporting_event?: string;
70
+ /**
71
+ * When true, middleware and AbTestUtmScript may apply `url_params` to the URL.
72
+ * When false or absent (legacy cookies), do not inject into the address bar.
73
+ */
74
+ injectUrlParams?: boolean;
75
+ /** URL params to inject when injectUrlParams is enabled */
76
+ url_params?: Record<string, string>;
77
+ /** Arbitrary key-value pairs for analytics adapters */
78
+ reporting_params?: Record<string, string>;
79
+ }
80
+ /**
81
+ * Cookie structure for A/B test assignments.
82
+ * Key is the test ID, value is the assignment data.
83
+ */
84
+ export type AbTestCookie = Record<string, AbTestAssignment>;
85
+ /**
86
+ * Generic blob store interface for A/B test configuration storage.
87
+ *
88
+ * Projects must implement this interface for their hosting platform:
89
+ * - Vercel: Use Vercel KV
90
+ * - Netlify: Use Netlify Blobs
91
+ * - Local development: Use file system
92
+ *
93
+ * @example Vercel KV implementation
94
+ * ```typescript
95
+ * import { kv } from '@vercel/kv';
96
+ * import type { IBlobStore, AbTest } from '@se-studio/ab-testing';
97
+ *
98
+ * export function getAbTestStore(): IBlobStore<AbTest> {
99
+ * return {
100
+ * async get(key) { return kv.get(`ab-test:${key}`); },
101
+ * async set(key, value) { await kv.set(`ab-test:${key}`, value); },
102
+ * async bulkWrite(entries) {
103
+ * const pipeline = kv.pipeline();
104
+ * for (const [key, value] of entries) {
105
+ * pipeline.set(`ab-test:${key}`, value);
106
+ * }
107
+ * await pipeline.exec();
108
+ * },
109
+ * async size() { return (await kv.keys('ab-test:*')).length; },
110
+ * async values() { return kv.mget(...(await kv.keys('ab-test:*'))); },
111
+ * };
112
+ * }
113
+ * ```
114
+ */
115
+ export interface IBlobStore<T> {
116
+ /**
117
+ * Get a value by key
118
+ * @param key - The key to look up
119
+ * @returns The value, or undefined if not found
120
+ */
121
+ get(key: string): Promise<T | undefined>;
122
+ /**
123
+ * Set a value by key
124
+ * @param key - The key to store under
125
+ * @param value - The value to store
126
+ */
127
+ set(key: string, value: T): Promise<void>;
128
+ /**
129
+ * Replace all entries in the store with new entries
130
+ * @param entries - Array of [key, value] tuples to store
131
+ */
132
+ bulkWrite(entries: [string, T][]): Promise<void>;
133
+ /**
134
+ * Get the number of entries in the store
135
+ * @returns The count of stored entries
136
+ */
137
+ size(): Promise<number>;
138
+ /**
139
+ * Get all values in the store
140
+ * @returns Array of all stored values
141
+ */
142
+ values(): Promise<T[]>;
143
+ }
144
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,iFAAiF;IACjF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,qFAAqF;IACrF,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,oDAAoD;IACpD,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,eAAe,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAEzC;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C;;;OAGG;IACH,SAAS,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjD;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAExB;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * A/B Testing Types
3
+ *
4
+ * Core type definitions for server-side A/B testing.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -0,0 +1,12 @@
1
+ import type { CachedAbTest } from './middleware/types';
2
+ /**
3
+ * Given a testsByPath map and a variant slug, returns the canonical
4
+ * (user-facing) path that owns the variant.
5
+ *
6
+ * Useful in page-test route handlers to set the correct canonical URL
7
+ * in metadata rather than exposing the internal variant path.
8
+ *
9
+ * @returns The canonical path (e.g. '/services/'), or undefined if not found.
10
+ */
11
+ export declare function findCanonicalPath(testsByPath: Record<string, CachedAbTest[]>, variantSlug: string): string | undefined;
12
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAC3C,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CASpB"}
package/dist/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Given a testsByPath map and a variant slug, returns the canonical
3
+ * (user-facing) path that owns the variant.
4
+ *
5
+ * Useful in page-test route handlers to set the correct canonical URL
6
+ * in metadata rather than exposing the internal variant path.
7
+ *
8
+ * @returns The canonical path (e.g. '/services/'), or undefined if not found.
9
+ */
10
+ export function findCanonicalPath(testsByPath, variantSlug) {
11
+ for (const [controlPath, tests] of Object.entries(testsByPath)) {
12
+ for (const test of tests) {
13
+ if (test.variants.some((v) => v.slug === variantSlug)) {
14
+ return controlPath.endsWith('/') ? controlPath : `${controlPath}/`;
15
+ }
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,WAA2C,EAC3C,WAAmB;IAEnB,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,EAAE,CAAC;gBACtD,OAAO,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,109 @@
1
+ import type { AbTest, AbTestVariantConfig, IBlobStore } from '../types';
2
+ /** Legacy CMS configuration shape accepted during migration. */
3
+ interface LegacyVariantConfig {
4
+ weight: number;
5
+ variant_label?: string;
6
+ reporting_event?: string;
7
+ hubspot_event_name?: string;
8
+ utm_content?: string;
9
+ url_params?: Record<string, string>;
10
+ reporting_params?: Record<string, string>;
11
+ }
12
+ /**
13
+ * Raw PageTest data from Contentful (before transformation).
14
+ * This interface represents the expected shape of data from your Contentful fetch.
15
+ */
16
+ export interface RawPageTest {
17
+ /** Contentful system metadata */
18
+ sys: {
19
+ id: string;
20
+ };
21
+ /** CMS label for the test */
22
+ cmsLabel?: string | null;
23
+ /** Whether the test is enabled */
24
+ enabled?: boolean | null;
25
+ /** Optional tracking label override */
26
+ trackingLabel?: string | null;
27
+ /** Optional search parameters to match */
28
+ searchParameters?: string | null;
29
+ /** When true, inject url_params from the assigned bucket */
30
+ injectUrlParams?: boolean | null;
31
+ /** Variant configuration JSON */
32
+ configuration?: LegacyVariantConfig[] | null;
33
+ /** Control page reference */
34
+ control?: {
35
+ sys?: {
36
+ id: string;
37
+ };
38
+ slug?: string | null;
39
+ } | null;
40
+ /** Variants collection */
41
+ variantsCollection?: {
42
+ items?: Array<{
43
+ sys: {
44
+ id: string;
45
+ };
46
+ slug?: string | null;
47
+ } | null> | null;
48
+ } | null;
49
+ }
50
+ /**
51
+ * Configuration for the webhook handler.
52
+ */
53
+ export interface WebhookHandlerConfig {
54
+ /**
55
+ * Function to fetch all active PageTest entries from Contentful.
56
+ * This should return the raw data from your GraphQL or REST API call.
57
+ */
58
+ fetchPageTests: () => Promise<RawPageTest[]>;
59
+ /**
60
+ * Function to get the blob store instance.
61
+ */
62
+ getStore: () => IBlobStore<AbTest> | Promise<IBlobStore<AbTest>>;
63
+ /**
64
+ * Optional secret for webhook authentication.
65
+ * If provided, the webhook will validate the x-contentful-webhook-secret header.
66
+ */
67
+ webhookSecret?: string;
68
+ /**
69
+ * Optional callback when a test is skipped (e.g., no control slug).
70
+ */
71
+ onSkippedTest?: (testId: string, reason: string) => void;
72
+ /**
73
+ * Optional revalidation function to call after updating the store.
74
+ * Useful for clearing Next.js cache tags.
75
+ */
76
+ revalidate?: () => void | Promise<void>;
77
+ }
78
+ /**
79
+ * Result of processing the webhook.
80
+ */
81
+ export interface WebhookResult {
82
+ /** Whether the webhook was processed successfully */
83
+ success: boolean;
84
+ /** Number of tests stored */
85
+ count: number;
86
+ /** Optional error message */
87
+ error?: string;
88
+ }
89
+ /**
90
+ * Normalize a single bucket config, accepting legacy field names.
91
+ */
92
+ export declare function normalizeVariantConfig(raw: LegacyVariantConfig): AbTestVariantConfig;
93
+ /**
94
+ * Transform raw Contentful PageTest data into AbTest format.
95
+ */
96
+ export declare function transformPageTest(raw: RawPageTest, onSkipped?: (testId: string, reason: string) => void): AbTest | null;
97
+ /**
98
+ * Process all PageTest entries and store them in the blob store.
99
+ *
100
+ * @param config - Webhook handler configuration
101
+ * @returns Result with success status and count
102
+ */
103
+ export declare function processAbTestWebhook(config: WebhookHandlerConfig): Promise<WebhookResult>;
104
+ /**
105
+ * Create a Next.js API route handler for the A/B test webhook.
106
+ */
107
+ export declare function createWebhookHandler(config: WebhookHandlerConfig): (request: Request) => Promise<Response>;
108
+ export {};
109
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"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"}