@kiryl.pekarski/payload-plugin-ab 1.2.1 → 1.3.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.
@@ -1,4 +1,4 @@
1
- import { AnalyticsAdapter } from '../../index.js';
1
+ import { A as AnalyticsAdapter } from '../../../types-OJFBnrUD.js';
2
2
 
3
3
  interface GoogleAnalyticsAdapterConfig {
4
4
  /** GA4 Measurement ID, e.g. "G-XXXXXXXXXX" */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/analytics/adapters/googleAnalytics/constants.ts","../../../../src/analytics/adapters/googleAnalytics/utils/canUseGtag.ts","../../../../src/analytics/adapters/googleAnalytics/utils/waitForGtag.ts","../../../../src/analytics/adapters/googleAnalytics/client.ts","../../../../src/analytics/adapters/googleAnalytics/server.ts","../../../../src/analytics/adapters/googleAnalytics/stats.ts","../../../../src/analytics/adapters/googleAnalytics/index.ts"],"sourcesContent":["export const DEFAULT_IMPRESSION_EVENT_NAME = \"ab_impression\";\r\n\r\nexport const DEFAULT_CONVERSION_EVENT_NAME = \"ab_conversion\";\r\n\r\nexport const MEASUREMENT_PROTOCOL_URL = \"https://www.google-analytics.com/mp/collect\";\r\n\r\nexport const DATA_API_BASE = \"https://analyticsdata.googleapis.com/v1beta\";\r\n","export type WindowWithGtag = Window & { gtag: NonNullable<Window[\"gtag\"]> };\r\n\r\nexport const canUseGtag = (window: Window): window is WindowWithGtag => {\r\n return typeof window !== \"undefined\" && typeof window.gtag === \"function\";\r\n};\r\n","import { canUseGtag, type WindowWithGtag } from \"./canUseGtag\";\r\n\r\ntype GtagFn = WindowWithGtag[\"gtag\"];\r\n\r\ninterface WaitForGtagOptions {\r\n interval?: number;\r\n timeout?: number;\r\n}\r\n\r\nexport function waitForGtag(callback: (gtag: GtagFn) => void, options: WaitForGtagOptions = {}) {\r\n const { interval = 50, timeout = 5000 } = options;\r\n\r\n if (canUseGtag(window)) {\r\n callback(window.gtag);\r\n\r\n return;\r\n }\r\n\r\n const start = Date.now();\r\n const id = setInterval(() => {\r\n if (canUseGtag(window)) {\r\n clearInterval(id);\r\n\r\n callback(window.gtag);\r\n } else if (Date.now() - start >= timeout) {\r\n clearInterval(id);\r\n }\r\n }, interval);\r\n}\r\n","import type { TrackConversionArgs, TrackImpressionArgs } from \"../../types\";\r\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\r\nimport { DEFAULT_CONVERSION_EVENT_NAME, DEFAULT_IMPRESSION_EVENT_NAME } from \"./constants\";\r\nimport { canUseGtag } from \"./utils/canUseGtag\";\r\nimport { waitForGtag } from \"./utils/waitForGtag\";\r\n\r\ndeclare global {\r\n interface Window {\r\n gtag?: (command: \"event\", eventName: string, params: Record<string, unknown>) => void;\r\n }\r\n}\r\n\r\nexport function trackImpressionClient(\r\n config: GoogleAnalyticsAdapterConfig,\r\n { experimentId, variantBucket, visitorId, locale, metadata }: TrackImpressionArgs,\r\n) {\r\n waitForGtag((gtag) => {\r\n gtag(\"event\", config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME, {\r\n experiment_id: experimentId,\r\n variant_bucket: variantBucket,\r\n visitor_id: visitorId,\r\n ...(locale !== undefined && { locale }),\r\n ...metadata,\r\n });\r\n });\r\n}\r\n\r\nexport function trackConversionClient(\r\n config: GoogleAnalyticsAdapterConfig,\r\n { experimentId, goalId, variantBucket, visitorId, goalValue, locale, metadata }: TrackConversionArgs,\r\n) {\r\n if (!canUseGtag(window)) {\r\n return;\r\n }\r\n\r\n window.gtag(\"event\", config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME, {\r\n experiment_id: experimentId,\r\n variant_bucket: variantBucket,\r\n visitor_id: visitorId,\r\n goal_id: goalId,\r\n ...(goalValue !== undefined && { value: goalValue }),\r\n ...(locale !== undefined && { locale }),\r\n ...metadata,\r\n });\r\n}\r\n","import type { TrackImpressionArgs } from \"../../types\";\r\nimport { DEFAULT_IMPRESSION_EVENT_NAME, MEASUREMENT_PROTOCOL_URL } from \"./constants\";\r\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\r\n\r\nexport async function trackImpressionServer(\r\n config: GoogleAnalyticsAdapterConfig,\r\n { experimentId, variantBucket, visitorId, locale, metadata }: TrackImpressionArgs,\r\n) {\r\n if (!config.apiSecret) return;\r\n\r\n const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${config.measurementId}&api_secret=${config.apiSecret}`;\r\n\r\n await fetch(url, {\r\n method: \"POST\",\r\n headers: { \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify({\r\n client_id: visitorId,\r\n events: [\r\n {\r\n name: config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME,\r\n params: {\r\n experiment_id: experimentId,\r\n variant_bucket: variantBucket,\r\n visitor_id: visitorId,\r\n engagement_time_msec: 1,\r\n ...(locale !== undefined && { locale }),\r\n ...metadata,\r\n },\r\n },\r\n ],\r\n }),\r\n });\r\n}\r\n","import type { DateRange, ExperimentStats, VariantStats } from \"../../types\";\r\nimport { DATA_API_BASE, DEFAULT_CONVERSION_EVENT_NAME, DEFAULT_IMPRESSION_EVENT_NAME } from \"./constants\";\r\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\r\n\r\ninterface GA4ReportRow {\r\n dimensionValues: Array<{ value: string }>;\r\n metricValues: Array<{ value: string }>;\r\n}\r\n\r\ninterface GA4Report {\r\n rows?: GA4ReportRow[];\r\n}\r\n\r\ninterface GA4BatchResponse {\r\n reports: GA4Report[];\r\n}\r\n\r\nfunction parseReport(report: GA4Report | undefined): Map<string, number> {\r\n const result = new Map<string, number>();\r\n\r\n if (!report) return result;\r\n\r\n for (const row of report.rows ?? []) {\r\n const bucket = row.dimensionValues[0]?.value;\r\n const raw = row.metricValues[0]?.value;\r\n\r\n if (bucket != null && raw != null) {\r\n result.set(bucket, parseInt(raw, 10));\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\nexport async function getExperimentStats(\r\n config: GoogleAnalyticsAdapterConfig,\r\n experimentId: string,\r\n dateRange: DateRange = { startDate: \"30daysAgo\", endDate: \"today\" },\r\n): Promise<ExperimentStats> {\r\n if (!config.propertyId || !config.getAccessToken) {\r\n throw new Error(\r\n \"payload-plugin-ab: getStats() requires propertyId and getAccessToken \"\r\n + \"to be set in GoogleAnalyticsAdapterConfig.\",\r\n );\r\n }\r\n\r\n const accessToken = await config.getAccessToken();\r\n const url = `${DATA_API_BASE}/${config.propertyId}:batchRunReports`;\r\n\r\n const makeReport = (eventName: string) => ({\r\n dimensions: [{ name: \"customEvent:variant_bucket\" }],\r\n metrics: [{ name: \"eventCount\" }],\r\n dimensionFilter: {\r\n andGroup: {\r\n expressions: [\r\n {\r\n filter: {\r\n fieldName: \"eventName\",\r\n stringFilter: { matchType: \"EXACT\", value: eventName },\r\n },\r\n },\r\n {\r\n filter: {\r\n fieldName: \"customEvent:experiment_id\",\r\n stringFilter: { matchType: \"EXACT\", value: experimentId },\r\n },\r\n },\r\n ],\r\n },\r\n },\r\n dateRanges: [dateRange],\r\n });\r\n\r\n const res = await fetch(url, {\r\n method: \"POST\",\r\n headers: {\r\n Authorization: `Bearer ${accessToken}`,\r\n \"Content-Type\": \"application/json\",\r\n },\r\n body: JSON.stringify({\r\n requests: [\r\n makeReport(config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME),\r\n makeReport(config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME),\r\n ],\r\n }),\r\n });\r\n\r\n if (!res.ok) {\r\n const body = await res.text();\r\n throw new Error(`payload-plugin-ab: GA4 Data API responded with ${res.status}: ${body}`);\r\n }\r\n\r\n const data: GA4BatchResponse = await res.json();\r\n\r\n const impressionMap = parseReport(data.reports[0]);\r\n const conversionMap = parseReport(data.reports[1]);\r\n\r\n const allBuckets = new Set([...impressionMap.keys(), ...conversionMap.keys()]);\r\n\r\n const totalImpressions = [...impressionMap.values()].reduce((acc, n) => acc + n, 0);\r\n const totalConversions = [...conversionMap.values()].reduce((acc, n) => acc + n, 0);\r\n\r\n const variants: VariantStats[] = [...allBuckets].map((bucket) => {\r\n const impressions = impressionMap.get(bucket) ?? 0;\r\n const conversions = conversionMap.get(bucket) ?? 0;\r\n return {\r\n bucket,\r\n impressions,\r\n impressionShare: totalImpressions > 0 ? impressions / totalImpressions : 0,\r\n conversions,\r\n conversionRate: impressions > 0 ? conversions / impressions : 0,\r\n };\r\n });\r\n\r\n return {\r\n experimentId,\r\n dateRange,\r\n variants,\r\n totals: {\r\n impressions: totalImpressions,\r\n conversions: totalConversions,\r\n },\r\n };\r\n}\r\n","import type { AnalyticsAdapter } from \"../../types\";\r\nimport { trackConversionClient, trackImpressionClient } from \"./client\";\r\nimport { trackImpressionServer } from \"./server\";\r\nimport { getExperimentStats } from \"./stats\";\r\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\r\n\r\nexport type { GoogleAnalyticsAdapterConfig };\r\n\r\nexport function googleAnalyticsAdapter(config: GoogleAnalyticsAdapterConfig): AnalyticsAdapter {\r\n return {\r\n trackImpression: (args) => trackImpressionClient(config, args),\r\n trackConversion: (args) => trackConversionClient(config, args),\r\n ...(config.apiSecret != null && {\r\n trackImpressionServer: (args) => trackImpressionServer(config, args),\r\n }),\r\n ...(config.propertyId != null\r\n && config.getAccessToken != null && {\r\n getStats: (experimentId, dateRange) => getExperimentStats(config, experimentId, dateRange),\r\n }),\r\n };\r\n}\r\n"],"mappings":";AAAO,IAAM,gCAAgC;AAEtC,IAAM,gCAAgC;AAEtC,IAAM,2BAA2B;AAEjC,IAAM,gBAAgB;;;ACJtB,IAAM,aAAa,CAACA,YAA6C;AACtE,SAAO,OAAOA,YAAW,eAAe,OAAOA,QAAO,SAAS;AACjE;;;ACKO,SAAS,YAAY,UAAkC,UAA8B,CAAC,GAAG;AAC9F,QAAM,EAAE,WAAW,IAAI,UAAU,IAAK,IAAI;AAE1C,MAAI,WAAW,MAAM,GAAG;AACtB,aAAS,OAAO,IAAI;AAEpB;AAAA,EACF;AAEA,QAAM,QAAQ,KAAK,IAAI;AACvB,QAAM,KAAK,YAAY,MAAM;AAC3B,QAAI,WAAW,MAAM,GAAG;AACtB,oBAAc,EAAE;AAEhB,eAAS,OAAO,IAAI;AAAA,IACtB,WAAW,KAAK,IAAI,IAAI,SAAS,SAAS;AACxC,oBAAc,EAAE;AAAA,IAClB;AAAA,EACF,GAAG,QAAQ;AACb;;;AChBO,SAAS,sBACd,QACA,EAAE,cAAc,eAAe,WAAW,QAAQ,SAAS,GAC3D;AACA,cAAY,CAAC,SAAS;AACpB,SAAK,SAAS,OAAO,uBAAuB,+BAA+B;AAAA,MACzE,eAAe;AAAA,MACf,gBAAgB;AAAA,MAChB,YAAY;AAAA,MACZ,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,MACrC,GAAG;AAAA,IACL,CAAC;AAAA,EACH,CAAC;AACH;AAEO,SAAS,sBACd,QACA,EAAE,cAAc,QAAQ,eAAe,WAAW,WAAW,QAAQ,SAAS,GAC9E;AACA,MAAI,CAAC,WAAW,MAAM,GAAG;AACvB;AAAA,EACF;AAEA,SAAO,KAAK,SAAS,OAAO,uBAAuB,+BAA+B;AAAA,IAChF,eAAe;AAAA,IACf,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,GAAI,cAAc,UAAa,EAAE,OAAO,UAAU;AAAA,IAClD,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,IACrC,GAAG;AAAA,EACL,CAAC;AACH;;;ACxCA,eAAsB,sBACpB,QACA,EAAE,cAAc,eAAe,WAAW,QAAQ,SAAS,GAC3D;AACA,MAAI,CAAC,OAAO,UAAW;AAEvB,QAAM,MAAM,GAAG,wBAAwB,mBAAmB,OAAO,aAAa,eAAe,OAAO,SAAS;AAE7G,QAAM,MAAM,KAAK;AAAA,IACf,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU;AAAA,MACnB,WAAW;AAAA,MACX,QAAQ;AAAA,QACN;AAAA,UACE,MAAM,OAAO,uBAAuB;AAAA,UACpC,QAAQ;AAAA,YACN,eAAe;AAAA,YACf,gBAAgB;AAAA,YAChB,YAAY;AAAA,YACZ,sBAAsB;AAAA,YACtB,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,YACrC,GAAG;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;;;ACfA,SAAS,YAAY,QAAoD;AACvE,QAAM,SAAS,oBAAI,IAAoB;AAEvC,MAAI,CAAC,OAAQ,QAAO;AAEpB,aAAW,OAAO,OAAO,QAAQ,CAAC,GAAG;AACnC,UAAM,SAAS,IAAI,gBAAgB,CAAC,GAAG;AACvC,UAAM,MAAM,IAAI,aAAa,CAAC,GAAG;AAEjC,QAAI,UAAU,QAAQ,OAAO,MAAM;AACjC,aAAO,IAAI,QAAQ,SAAS,KAAK,EAAE,CAAC;AAAA,IACtC;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAsB,mBACpB,QACA,cACA,YAAuB,EAAE,WAAW,aAAa,SAAS,QAAQ,GACxC;AAC1B,MAAI,CAAC,OAAO,cAAc,CAAC,OAAO,gBAAgB;AAChD,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,OAAO,eAAe;AAChD,QAAM,MAAM,GAAG,aAAa,IAAI,OAAO,UAAU;AAEjD,QAAM,aAAa,CAAC,eAAuB;AAAA,IACzC,YAAY,CAAC,EAAE,MAAM,6BAA6B,CAAC;AAAA,IACnD,SAAS,CAAC,EAAE,MAAM,aAAa,CAAC;AAAA,IAChC,iBAAiB;AAAA,MACf,UAAU;AAAA,QACR,aAAa;AAAA,UACX;AAAA,YACE,QAAQ;AAAA,cACN,WAAW;AAAA,cACX,cAAc,EAAE,WAAW,SAAS,OAAO,UAAU;AAAA,YACvD;AAAA,UACF;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,cACN,WAAW;AAAA,cACX,cAAc,EAAE,WAAW,SAAS,OAAO,aAAa;AAAA,YAC1D;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,YAAY,CAAC,SAAS;AAAA,EACxB;AAEA,QAAM,MAAM,MAAM,MAAM,KAAK;AAAA,IAC3B,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,eAAe,UAAU,WAAW;AAAA,MACpC,gBAAgB;AAAA,IAClB;AAAA,IACA,MAAM,KAAK,UAAU;AAAA,MACnB,UAAU;AAAA,QACR,WAAW,OAAO,uBAAuB,6BAA6B;AAAA,QACtE,WAAW,OAAO,uBAAuB,6BAA6B;AAAA,MACxE;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAM,IAAI,MAAM,kDAAkD,IAAI,MAAM,KAAK,IAAI,EAAE;AAAA,EACzF;AAEA,QAAM,OAAyB,MAAM,IAAI,KAAK;AAE9C,QAAM,gBAAgB,YAAY,KAAK,QAAQ,CAAC,CAAC;AACjD,QAAM,gBAAgB,YAAY,KAAK,QAAQ,CAAC,CAAC;AAEjD,QAAM,aAAa,oBAAI,IAAI,CAAC,GAAG,cAAc,KAAK,GAAG,GAAG,cAAc,KAAK,CAAC,CAAC;AAE7E,QAAM,mBAAmB,CAAC,GAAG,cAAc,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAClF,QAAM,mBAAmB,CAAC,GAAG,cAAc,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAElF,QAAM,WAA2B,CAAC,GAAG,UAAU,EAAE,IAAI,CAAC,WAAW;AAC/D,UAAM,cAAc,cAAc,IAAI,MAAM,KAAK;AACjD,UAAM,cAAc,cAAc,IAAI,MAAM,KAAK;AACjD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,iBAAiB,mBAAmB,IAAI,cAAc,mBAAmB;AAAA,MACzE;AAAA,MACA,gBAAgB,cAAc,IAAI,cAAc,cAAc;AAAA,IAChE;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,MACN,aAAa;AAAA,MACb,aAAa;AAAA,IACf;AAAA,EACF;AACF;;;ACnHO,SAAS,uBAAuB,QAAwD;AAC7F,SAAO;AAAA,IACL,iBAAiB,CAAC,SAAS,sBAAsB,QAAQ,IAAI;AAAA,IAC7D,iBAAiB,CAAC,SAAS,sBAAsB,QAAQ,IAAI;AAAA,IAC7D,GAAI,OAAO,aAAa,QAAQ;AAAA,MAC9B,uBAAuB,CAAC,SAAS,sBAAsB,QAAQ,IAAI;AAAA,IACrE;AAAA,IACA,GAAI,OAAO,cAAc,QACpB,OAAO,kBAAkB,QAAQ;AAAA,MAClC,UAAU,CAAC,cAAc,cAAc,mBAAmB,QAAQ,cAAc,SAAS;AAAA,IAC3F;AAAA,EACJ;AACF;","names":["window"]}
1
+ {"version":3,"sources":["../../../../src/analytics/adapters/googleAnalytics/constants.ts","../../../../src/analytics/adapters/googleAnalytics/utils/canUseGtag.ts","../../../../src/analytics/adapters/googleAnalytics/utils/waitForGtag.ts","../../../../src/analytics/adapters/googleAnalytics/client.ts","../../../../src/analytics/adapters/googleAnalytics/server.ts","../../../../src/analytics/adapters/googleAnalytics/stats.ts","../../../../src/analytics/adapters/googleAnalytics/index.ts"],"sourcesContent":["export const DEFAULT_IMPRESSION_EVENT_NAME = \"ab_impression\";\n\nexport const DEFAULT_CONVERSION_EVENT_NAME = \"ab_conversion\";\n\nexport const MEASUREMENT_PROTOCOL_URL = \"https://www.google-analytics.com/mp/collect\";\n\nexport const DATA_API_BASE = \"https://analyticsdata.googleapis.com/v1beta\";\n","export type WindowWithGtag = Window & { gtag: NonNullable<Window[\"gtag\"]> };\n\nexport const canUseGtag = (window: Window): window is WindowWithGtag => {\n return typeof window !== \"undefined\" && typeof window.gtag === \"function\";\n};\n","import { canUseGtag, type WindowWithGtag } from \"./canUseGtag\";\n\ntype GtagFn = WindowWithGtag[\"gtag\"];\n\ninterface WaitForGtagOptions {\n interval?: number;\n timeout?: number;\n}\n\nexport function waitForGtag(callback: (gtag: GtagFn) => void, options: WaitForGtagOptions = {}) {\n const { interval = 50, timeout = 5000 } = options;\n\n if (canUseGtag(window)) {\n callback(window.gtag);\n\n return;\n }\n\n const start = Date.now();\n const id = setInterval(() => {\n if (canUseGtag(window)) {\n clearInterval(id);\n\n callback(window.gtag);\n } else if (Date.now() - start >= timeout) {\n clearInterval(id);\n }\n }, interval);\n}\n","import type { TrackConversionArgs, TrackImpressionArgs } from \"../../types\";\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\nimport { DEFAULT_CONVERSION_EVENT_NAME, DEFAULT_IMPRESSION_EVENT_NAME } from \"./constants\";\nimport { canUseGtag } from \"./utils/canUseGtag\";\nimport { waitForGtag } from \"./utils/waitForGtag\";\n\ndeclare global {\n interface Window {\n gtag?: (command: \"event\", eventName: string, params: Record<string, unknown>) => void;\n }\n}\n\nexport function trackImpressionClient(\n config: GoogleAnalyticsAdapterConfig,\n { experimentId, variantBucket, visitorId, locale, metadata }: TrackImpressionArgs,\n) {\n waitForGtag((gtag) => {\n gtag(\"event\", config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME, {\n experiment_id: experimentId,\n variant_bucket: variantBucket,\n visitor_id: visitorId,\n ...(locale !== undefined && { locale }),\n ...metadata,\n });\n });\n}\n\nexport function trackConversionClient(\n config: GoogleAnalyticsAdapterConfig,\n { experimentId, goalId, variantBucket, visitorId, goalValue, locale, metadata }: TrackConversionArgs,\n) {\n if (!canUseGtag(window)) {\n return;\n }\n\n window.gtag(\"event\", config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME, {\n experiment_id: experimentId,\n variant_bucket: variantBucket,\n visitor_id: visitorId,\n goal_id: goalId,\n ...(goalValue !== undefined && { value: goalValue }),\n ...(locale !== undefined && { locale }),\n ...metadata,\n });\n}\n","import type { TrackImpressionArgs } from \"../../types\";\nimport { DEFAULT_IMPRESSION_EVENT_NAME, MEASUREMENT_PROTOCOL_URL } from \"./constants\";\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\n\nexport async function trackImpressionServer(\n config: GoogleAnalyticsAdapterConfig,\n { experimentId, variantBucket, visitorId, locale, metadata }: TrackImpressionArgs,\n) {\n if (!config.apiSecret) return;\n\n const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${config.measurementId}&api_secret=${config.apiSecret}`;\n\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_id: visitorId,\n events: [\n {\n name: config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME,\n params: {\n experiment_id: experimentId,\n variant_bucket: variantBucket,\n visitor_id: visitorId,\n engagement_time_msec: 1,\n ...(locale !== undefined && { locale }),\n ...metadata,\n },\n },\n ],\n }),\n });\n}\n","import type { DateRange, ExperimentStats, VariantStats } from \"../../types\";\nimport { DATA_API_BASE, DEFAULT_CONVERSION_EVENT_NAME, DEFAULT_IMPRESSION_EVENT_NAME } from \"./constants\";\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\n\ninterface GA4ReportRow {\n dimensionValues: Array<{ value: string }>;\n metricValues: Array<{ value: string }>;\n}\n\ninterface GA4Report {\n rows?: GA4ReportRow[];\n}\n\ninterface GA4BatchResponse {\n reports: GA4Report[];\n}\n\nfunction parseReport(report: GA4Report | undefined): Map<string, number> {\n const result = new Map<string, number>();\n\n if (!report) return result;\n\n for (const row of report.rows ?? []) {\n const bucket = row.dimensionValues[0]?.value;\n const raw = row.metricValues[0]?.value;\n\n if (bucket != null && raw != null) {\n result.set(bucket, parseInt(raw, 10));\n }\n }\n\n return result;\n}\n\nexport async function getExperimentStats(\n config: GoogleAnalyticsAdapterConfig,\n experimentId: string,\n dateRange: DateRange = { startDate: \"30daysAgo\", endDate: \"today\" },\n): Promise<ExperimentStats> {\n if (!config.propertyId || !config.getAccessToken) {\n throw new Error(\n \"payload-plugin-ab: getStats() requires propertyId and getAccessToken \"\n + \"to be set in GoogleAnalyticsAdapterConfig.\",\n );\n }\n\n const accessToken = await config.getAccessToken();\n const url = `${DATA_API_BASE}/${config.propertyId}:batchRunReports`;\n\n const makeReport = (eventName: string) => ({\n dimensions: [{ name: \"customEvent:variant_bucket\" }],\n metrics: [{ name: \"eventCount\" }],\n dimensionFilter: {\n andGroup: {\n expressions: [\n {\n filter: {\n fieldName: \"eventName\",\n stringFilter: { matchType: \"EXACT\", value: eventName },\n },\n },\n {\n filter: {\n fieldName: \"customEvent:experiment_id\",\n stringFilter: { matchType: \"EXACT\", value: experimentId },\n },\n },\n ],\n },\n },\n dateRanges: [dateRange],\n });\n\n const res = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n requests: [\n makeReport(config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME),\n makeReport(config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME),\n ],\n }),\n });\n\n if (!res.ok) {\n const body = await res.text();\n throw new Error(`payload-plugin-ab: GA4 Data API responded with ${res.status}: ${body}`);\n }\n\n const data: GA4BatchResponse = await res.json();\n\n const impressionMap = parseReport(data.reports[0]);\n const conversionMap = parseReport(data.reports[1]);\n\n const allBuckets = new Set([...impressionMap.keys(), ...conversionMap.keys()]);\n\n const totalImpressions = [...impressionMap.values()].reduce((acc, n) => acc + n, 0);\n const totalConversions = [...conversionMap.values()].reduce((acc, n) => acc + n, 0);\n\n const variants: VariantStats[] = [...allBuckets].map((bucket) => {\n const impressions = impressionMap.get(bucket) ?? 0;\n const conversions = conversionMap.get(bucket) ?? 0;\n return {\n bucket,\n impressions,\n impressionShare: totalImpressions > 0 ? impressions / totalImpressions : 0,\n conversions,\n conversionRate: impressions > 0 ? conversions / impressions : 0,\n };\n });\n\n return {\n experimentId,\n dateRange,\n variants,\n totals: {\n impressions: totalImpressions,\n conversions: totalConversions,\n },\n };\n}\n","import type { AnalyticsAdapter } from \"../../types\";\nimport { trackConversionClient, trackImpressionClient } from \"./client\";\nimport { trackImpressionServer } from \"./server\";\nimport { getExperimentStats } from \"./stats\";\nimport type { GoogleAnalyticsAdapterConfig } from \"./types\";\n\nexport type { GoogleAnalyticsAdapterConfig };\n\nexport function googleAnalyticsAdapter(config: GoogleAnalyticsAdapterConfig): AnalyticsAdapter {\n return {\n trackImpression: (args) => trackImpressionClient(config, args),\n trackConversion: (args) => trackConversionClient(config, args),\n ...(config.apiSecret != null && {\n trackImpressionServer: (args) => trackImpressionServer(config, args),\n }),\n ...(config.propertyId != null\n && config.getAccessToken != null && {\n getStats: (experimentId, dateRange) => getExperimentStats(config, experimentId, dateRange),\n }),\n };\n}\n"],"mappings":"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","names":["window"]}
@@ -1,20 +1,36 @@
1
- import { AnalyticsAdapter } from './index.js';
1
+ export { A as AbCookieConfig, R as ResolvedAbCookieNames, r as resolveAbCookieNames } from '../resolveAbCookieNames-DH8evjWm.js';
2
+ import { A as AnalyticsAdapter } from '../types-OJFBnrUD.js';
2
3
 
3
4
  interface ExperimentTrackerProps {
4
5
  experimentId: string;
5
6
  /**
6
7
  * Name of the cookie that holds the assigned variant bucket.
7
- * Default: `exp_${experimentId}`
8
+ * Use `resolveAbCookieNames(abCookies, experimentId).variantCookieName` to derive
9
+ * this from a shared `AbCookieConfig`. Default: `exp_${experimentId}`.
8
10
  */
9
11
  variantCookieName?: string;
10
- /** Default: 'ab_visitor_id' */
12
+ /**
13
+ * Name of the visitor ID cookie.
14
+ * Use `resolveAbCookieNames(abCookies, experimentId).visitorCookieName` to derive
15
+ * this from a shared `AbCookieConfig`. Default: 'ab_visitor_id'.
16
+ */
11
17
  visitorCookieName?: string;
12
18
  }
13
19
  declare function ExperimentTracker({ experimentId, variantCookieName, visitorCookieName, }: ExperimentTrackerProps): null;
14
20
 
15
21
  interface UseABConversionOptions {
16
22
  experimentId: string;
23
+ /**
24
+ * Name of the cookie that holds the assigned variant bucket.
25
+ * Use `resolveAbCookieNames(abCookies, experimentId).variantCookieName` to derive
26
+ * this from a shared `AbCookieConfig`. Default: `exp_${experimentId}`.
27
+ */
17
28
  variantCookieName?: string;
29
+ /**
30
+ * Name of the visitor ID cookie.
31
+ * Use `resolveAbCookieNames(abCookies, experimentId).visitorCookieName` to derive
32
+ * this from a shared `AbCookieConfig`. Default: 'ab_visitor_id'.
33
+ */
18
34
  visitorCookieName?: string;
19
35
  }
20
36
  type TrackConversionFn = (args: {
@@ -1,5 +1,21 @@
1
1
  "use client";
2
2
 
3
+ // src/cookie/utils/defaultGetExpCookieName.ts
4
+ function defaultGetExpCookieName(key) {
5
+ return `exp_${encodeURIComponent(key)}`;
6
+ }
7
+
8
+ // src/cookie/constants.ts
9
+ var DEFAULT_VISITOR_ID_COOKIE_NAME = "ab_visitor_id";
10
+
11
+ // src/cookie/utils/resolveAbCookieNames.ts
12
+ function resolveAbCookieNames(config, experimentId) {
13
+ return {
14
+ variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),
15
+ visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME
16
+ };
17
+ }
18
+
3
19
  // src/analytics/components/ABAnalyticsProvider.tsx
4
20
  import { createContext, useContext } from "react";
5
21
  import { jsx } from "react/jsx-runtime";
@@ -27,14 +43,11 @@ function getCookie(name) {
27
43
  return value != null ? decodeURIComponent(value) : null;
28
44
  }
29
45
 
30
- // src/analytics/constants.ts
31
- var DEFAULT_A_B_VISITOR_ID = "ab_visitor_id";
32
-
33
46
  // src/analytics/components/ExperimentTracker.tsx
34
47
  function ExperimentTracker({
35
48
  experimentId,
36
49
  variantCookieName,
37
- visitorCookieName = DEFAULT_A_B_VISITOR_ID
50
+ visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME
38
51
  }) {
39
52
  const adapter = useABAnalytics();
40
53
  useEffect(() => {
@@ -43,7 +56,7 @@ function ExperimentTracker({
43
56
  if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(sessionKey)) {
44
57
  return;
45
58
  }
46
- const resolvedVariantCookie = variantCookieName ?? `exp_${experimentId}`;
59
+ const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);
47
60
  const variantBucket = getCookie(resolvedVariantCookie);
48
61
  const visitorId = getCookie(visitorCookieName);
49
62
  if (!variantBucket || !visitorId) return;
@@ -58,14 +71,14 @@ import { useCallback } from "react";
58
71
  function useABConversion({
59
72
  experimentId,
60
73
  variantCookieName,
61
- visitorCookieName = DEFAULT_A_B_VISITOR_ID
74
+ visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME
62
75
  }) {
63
76
  const adapter = useABAnalytics();
64
77
  return useCallback(
65
78
  ({ goalId, goalValue }) => {
66
79
  if (!adapter) return;
67
- const cookieName = variantCookieName ?? `exp_${experimentId}`;
68
- const variantBucket = getCookie(cookieName);
80
+ const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);
81
+ const variantBucket = getCookie(resolvedVariantCookie);
69
82
  const visitorId = getCookie(visitorCookieName);
70
83
  if (!variantBucket || !visitorId) return;
71
84
  adapter.trackConversion({
@@ -82,6 +95,7 @@ function useABConversion({
82
95
  export {
83
96
  ABAnalyticsProvider,
84
97
  ExperimentTracker,
98
+ resolveAbCookieNames,
85
99
  useABAnalytics,
86
100
  useABConversion
87
101
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/analytics/components/ABAnalyticsProvider.tsx","../../src/analytics/components/ExperimentTracker.tsx","../../src/analytics/utils/getCookie.ts","../../src/analytics/constants.ts","../../src/analytics/hooks/useABConversion.ts"],"sourcesContent":["\"use client\";\r\n\r\nimport { createContext, useContext } from \"react\";\r\nimport type { AnalyticsAdapter } from \"../types\";\r\n\r\nexport const ABAnalyticsContext = createContext<AnalyticsAdapter | null>(null);\r\n\r\nexport function useABAnalytics() {\r\n return useContext(ABAnalyticsContext);\r\n}\r\n\r\nexport function ABAnalyticsProvider({\r\n adapter,\r\n children,\r\n}: {\r\n adapter: AnalyticsAdapter;\r\n children: React.ReactNode;\r\n}): React.ReactNode {\r\n return <ABAnalyticsContext.Provider value={adapter}>{children}</ABAnalyticsContext.Provider>;\r\n}\r\n","\"use client\";\r\n\r\nimport { useEffect } from \"react\";\r\nimport { getCookie } from \"../utils/getCookie\";\r\nimport { useABAnalytics } from \"./ABAnalyticsProvider\";\r\nimport { DEFAULT_A_B_VISITOR_ID } from \"../constants\";\r\n\r\nexport interface ExperimentTrackerProps {\r\n experimentId: string;\r\n /**\r\n * Name of the cookie that holds the assigned variant bucket.\r\n * Default: `exp_${experimentId}`\r\n */\r\n variantCookieName?: string;\r\n /** Default: 'ab_visitor_id' */\r\n visitorCookieName?: string;\r\n}\r\n\r\nexport function ExperimentTracker({\r\n experimentId,\r\n variantCookieName,\r\n visitorCookieName = DEFAULT_A_B_VISITOR_ID,\r\n}: ExperimentTrackerProps) {\r\n const adapter = useABAnalytics();\r\n\r\n useEffect(() => {\r\n if (!adapter) return;\r\n\r\n const sessionKey = `ab_tracked_${experimentId}`;\r\n if (typeof sessionStorage !== \"undefined\" && sessionStorage.getItem(sessionKey)) {\r\n return;\r\n }\r\n\r\n const resolvedVariantCookie = variantCookieName ?? `exp_${experimentId}`;\r\n const variantBucket = getCookie(resolvedVariantCookie);\r\n const visitorId = getCookie(visitorCookieName);\r\n\r\n if (!variantBucket || !visitorId) return;\r\n\r\n adapter.trackImpression({ experimentId, variantBucket, visitorId });\r\n\r\n sessionStorage.setItem(sessionKey, \"1\");\r\n }, [adapter, experimentId, variantCookieName, visitorCookieName]);\r\n\r\n return null;\r\n}\r\n","export function getCookie(name: string) {\r\n if (typeof document === \"undefined\") return null;\r\n\r\n const escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\r\n const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));\r\n\r\n if (!match) return null;\r\n\r\n const value = match[1];\r\n\r\n return value != null ? decodeURIComponent(value) : null;\r\n}\r\n","export const DEFAULT_A_B_VISITOR_ID = \"ab_visitor_id\";\r\n","\"use client\";\r\n\r\nimport { useCallback } from \"react\";\r\nimport { useABAnalytics } from \"../components/ABAnalyticsProvider\";\r\nimport { getCookie } from \"../utils/getCookie\";\r\nimport { DEFAULT_A_B_VISITOR_ID } from \"../constants\";\r\n\r\nexport interface UseABConversionOptions {\r\n experimentId: string;\r\n variantCookieName?: string;\r\n visitorCookieName?: string;\r\n}\r\n\r\nexport type TrackConversionFn = (args: { goalId: string; goalValue?: number }) => void;\r\n\r\nexport function useABConversion({\r\n experimentId,\r\n variantCookieName,\r\n visitorCookieName = DEFAULT_A_B_VISITOR_ID,\r\n}: UseABConversionOptions): TrackConversionFn {\r\n const adapter = useABAnalytics();\r\n\r\n return useCallback(\r\n ({ goalId, goalValue }: { goalId: string; goalValue?: number }) => {\r\n if (!adapter) return;\r\n\r\n const cookieName = variantCookieName ?? `exp_${experimentId}`;\r\n const variantBucket = getCookie(cookieName);\r\n const visitorId = getCookie(visitorCookieName);\r\n\r\n if (!variantBucket || !visitorId) return;\r\n\r\n adapter.trackConversion({\r\n experimentId,\r\n variantBucket,\r\n visitorId,\r\n goalId,\r\n goalValue,\r\n });\r\n },\r\n [adapter, experimentId, variantCookieName, visitorCookieName],\r\n );\r\n}\r\n"],"mappings":";;;AAEA,SAAS,eAAe,kBAAkB;AAgBjC;AAbF,IAAM,qBAAqB,cAAuC,IAAI;AAEtE,SAAS,iBAAiB;AAC/B,SAAO,WAAW,kBAAkB;AACtC;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAGoB;AAClB,SAAO,oBAAC,mBAAmB,UAAnB,EAA4B,OAAO,SAAU,UAAS;AAChE;;;ACjBA,SAAS,iBAAiB;;;ACFnB,SAAS,UAAU,MAAc;AACtC,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,QAAM,UAAU,KAAK,QAAQ,uBAAuB,MAAM;AAC1D,QAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,OAAO,WAAW,OAAO,UAAU,CAAC;AAE5E,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,QAAQ,MAAM,CAAC;AAErB,SAAO,SAAS,OAAO,mBAAmB,KAAK,IAAI;AACrD;;;ACXO,IAAM,yBAAyB;;;AFkB/B,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,oBAAoB;AACtB,GAA2B;AACzB,QAAM,UAAU,eAAe;AAE/B,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAEd,UAAM,aAAa,cAAc,YAAY;AAC7C,QAAI,OAAO,mBAAmB,eAAe,eAAe,QAAQ,UAAU,GAAG;AAC/E;AAAA,IACF;AAEA,UAAM,wBAAwB,qBAAqB,OAAO,YAAY;AACtE,UAAM,gBAAgB,UAAU,qBAAqB;AACrD,UAAM,YAAY,UAAU,iBAAiB;AAE7C,QAAI,CAAC,iBAAiB,CAAC,UAAW;AAElC,YAAQ,gBAAgB,EAAE,cAAc,eAAe,UAAU,CAAC;AAElE,mBAAe,QAAQ,YAAY,GAAG;AAAA,EACxC,GAAG,CAAC,SAAS,cAAc,mBAAmB,iBAAiB,CAAC;AAEhE,SAAO;AACT;;;AG3CA,SAAS,mBAAmB;AAarB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,oBAAoB;AACtB,GAA8C;AAC5C,QAAM,UAAU,eAAe;AAE/B,SAAO;AAAA,IACL,CAAC,EAAE,QAAQ,UAAU,MAA8C;AACjE,UAAI,CAAC,QAAS;AAEd,YAAM,aAAa,qBAAqB,OAAO,YAAY;AAC3D,YAAM,gBAAgB,UAAU,UAAU;AAC1C,YAAM,YAAY,UAAU,iBAAiB;AAE7C,UAAI,CAAC,iBAAiB,CAAC,UAAW;AAElC,cAAQ,gBAAgB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,SAAS,cAAc,mBAAmB,iBAAiB;AAAA,EAC9D;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/cookie/utils/defaultGetExpCookieName.ts","../../src/cookie/constants.ts","../../src/cookie/utils/resolveAbCookieNames.ts","../../src/analytics/components/ABAnalyticsProvider.tsx","../../src/analytics/components/ExperimentTracker.tsx","../../src/analytics/utils/getCookie.ts","../../src/analytics/hooks/useABConversion.ts"],"sourcesContent":["export function defaultGetExpCookieName(key: string) {\n return `exp_${encodeURIComponent(key)}`;\n}\n","export const DEFAULT_VISITOR_ID_COOKIE_NAME = \"ab_visitor_id\";\n","import type { AbCookieConfig } from \"../types\";\nimport { defaultGetExpCookieName } from \"./defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../constants\";\n\nexport interface ResolvedAbCookieNames {\n /** Resolved variant cookie name for the given experiment. */\n variantCookieName: string;\n /** Resolved visitor ID cookie name. */\n visitorCookieName: string;\n}\n\n/**\n * Resolves an `AbCookieConfig` + experiment ID into plain serializable strings.\n * Use this in Server Components to derive props for Client Components.\n */\nexport function resolveAbCookieNames(config: AbCookieConfig | undefined, experimentId: string): ResolvedAbCookieNames {\n return {\n variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),\n visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME,\n };\n}\n","\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { AnalyticsAdapter } from \"../types\";\n\nexport const ABAnalyticsContext = createContext<AnalyticsAdapter | null>(null);\n\nexport function useABAnalytics() {\n return useContext(ABAnalyticsContext);\n}\n\nexport function ABAnalyticsProvider({\n adapter,\n children,\n}: {\n adapter: AnalyticsAdapter;\n children: React.ReactNode;\n}): React.ReactNode {\n return <ABAnalyticsContext.Provider value={adapter}>{children}</ABAnalyticsContext.Provider>;\n}\n","\"use client\";\n\nimport { useEffect } from \"react\";\nimport { getCookie } from \"../utils/getCookie\";\nimport { useABAnalytics } from \"./ABAnalyticsProvider\";\nimport { defaultGetExpCookieName } from \"../../cookie/utils/defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../../cookie/constants\";\n\nexport interface ExperimentTrackerProps {\n experimentId: string;\n /**\n * Name of the cookie that holds the assigned variant bucket.\n * Use `resolveAbCookieNames(abCookies, experimentId).variantCookieName` to derive\n * this from a shared `AbCookieConfig`. Default: `exp_${experimentId}`.\n */\n variantCookieName?: string;\n /**\n * Name of the visitor ID cookie.\n * Use `resolveAbCookieNames(abCookies, experimentId).visitorCookieName` to derive\n * this from a shared `AbCookieConfig`. Default: 'ab_visitor_id'.\n */\n visitorCookieName?: string;\n}\n\nexport function ExperimentTracker({\n experimentId,\n variantCookieName,\n visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME,\n}: ExperimentTrackerProps) {\n const adapter = useABAnalytics();\n\n useEffect(() => {\n if (!adapter) return;\n\n const sessionKey = `ab_tracked_${experimentId}`;\n if (typeof sessionStorage !== \"undefined\" && sessionStorage.getItem(sessionKey)) {\n return;\n }\n\n const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);\n const variantBucket = getCookie(resolvedVariantCookie);\n const visitorId = getCookie(visitorCookieName);\n\n if (!variantBucket || !visitorId) return;\n\n adapter.trackImpression({ experimentId, variantBucket, visitorId });\n\n sessionStorage.setItem(sessionKey, \"1\");\n }, [adapter, experimentId, variantCookieName, visitorCookieName]);\n\n return null;\n}\n","export function getCookie(name: string) {\n if (typeof document === \"undefined\") return null;\n\n const escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));\n\n if (!match) return null;\n\n const value = match[1];\n\n return value != null ? decodeURIComponent(value) : null;\n}\n","\"use client\";\n\nimport { useCallback } from \"react\";\nimport { useABAnalytics } from \"../components/ABAnalyticsProvider\";\nimport { getCookie } from \"../utils/getCookie\";\nimport { defaultGetExpCookieName } from \"../../cookie/utils/defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../../cookie/constants\";\n\nexport interface UseABConversionOptions {\n experimentId: string;\n /**\n * Name of the cookie that holds the assigned variant bucket.\n * Use `resolveAbCookieNames(abCookies, experimentId).variantCookieName` to derive\n * this from a shared `AbCookieConfig`. Default: `exp_${experimentId}`.\n */\n variantCookieName?: string;\n /**\n * Name of the visitor ID cookie.\n * Use `resolveAbCookieNames(abCookies, experimentId).visitorCookieName` to derive\n * this from a shared `AbCookieConfig`. Default: 'ab_visitor_id'.\n */\n visitorCookieName?: string;\n}\n\nexport type TrackConversionFn = (args: { goalId: string; goalValue?: number }) => void;\n\nexport function useABConversion({\n experimentId,\n variantCookieName,\n visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME,\n}: UseABConversionOptions): TrackConversionFn {\n const adapter = useABAnalytics();\n\n return useCallback(\n ({ goalId, goalValue }: { goalId: string; goalValue?: number }) => {\n if (!adapter) return;\n\n const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);\n const variantBucket = getCookie(resolvedVariantCookie);\n const visitorId = getCookie(visitorCookieName);\n\n if (!variantBucket || !visitorId) return;\n\n adapter.trackConversion({\n experimentId,\n variantBucket,\n visitorId,\n goalId,\n goalValue,\n });\n },\n [adapter, experimentId, variantCookieName, visitorCookieName],\n );\n}\n"],"mappings":";;;AAAO,SAAS,wBAAwB,KAAa;AACnD,SAAO,OAAO,mBAAmB,GAAG,CAAC;AACvC;;;ACFO,IAAM,iCAAiC;;;ACevC,SAAS,qBAAqB,QAAoC,cAA6C;AACpH,SAAO;AAAA,IACL,oBAAoB,QAAQ,oBAAoB,yBAAyB,YAAY;AAAA,IACrF,mBAAmB,QAAQ,uBAAuB;AAAA,EACpD;AACF;;;AClBA,SAAS,eAAe,kBAAkB;AAgBjC;AAbF,IAAM,qBAAqB,cAAuC,IAAI;AAEtE,SAAS,iBAAiB;AAC/B,SAAO,WAAW,kBAAkB;AACtC;AAEO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAGoB;AAClB,SAAO,oBAAC,mBAAmB,UAAnB,EAA4B,OAAO,SAAU,UAAS;AAChE;;;ACjBA,SAAS,iBAAiB;;;ACFnB,SAAS,UAAU,MAAc;AACtC,MAAI,OAAO,aAAa,YAAa,QAAO;AAE5C,QAAM,UAAU,KAAK,QAAQ,uBAAuB,MAAM;AAC1D,QAAM,QAAQ,SAAS,OAAO,MAAM,IAAI,OAAO,WAAW,OAAO,UAAU,CAAC;AAE5E,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,QAAQ,MAAM,CAAC;AAErB,SAAO,SAAS,OAAO,mBAAmB,KAAK,IAAI;AACrD;;;ADaO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,oBAAoB;AACtB,GAA2B;AACzB,QAAM,UAAU,eAAe;AAE/B,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAEd,UAAM,aAAa,cAAc,YAAY;AAC7C,QAAI,OAAO,mBAAmB,eAAe,eAAe,QAAQ,UAAU,GAAG;AAC/E;AAAA,IACF;AAEA,UAAM,wBAAwB,qBAAqB,wBAAwB,YAAY;AACvF,UAAM,gBAAgB,UAAU,qBAAqB;AACrD,UAAM,YAAY,UAAU,iBAAiB;AAE7C,QAAI,CAAC,iBAAiB,CAAC,UAAW;AAElC,YAAQ,gBAAgB,EAAE,cAAc,eAAe,UAAU,CAAC;AAElE,mBAAe,QAAQ,YAAY,GAAG;AAAA,EACxC,GAAG,CAAC,SAAS,cAAc,mBAAmB,iBAAiB,CAAC;AAEhE,SAAO;AACT;;;AEjDA,SAAS,mBAAmB;AAwBrB,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA,oBAAoB;AACtB,GAA8C;AAC5C,QAAM,UAAU,eAAe;AAE/B,SAAO;AAAA,IACL,CAAC,EAAE,QAAQ,UAAU,MAA8C;AACjE,UAAI,CAAC,QAAS;AAEd,YAAM,wBAAwB,qBAAqB,wBAAwB,YAAY;AACvF,YAAM,gBAAgB,UAAU,qBAAqB;AACrD,YAAM,YAAY,UAAU,iBAAiB;AAE7C,UAAI,CAAC,iBAAiB,CAAC,UAAW;AAElC,cAAQ,gBAAgB;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,CAAC,SAAS,cAAc,mBAAmB,iBAAiB;AAAA,EAC9D;AACF;","names":[]}
@@ -1,72 +1,2 @@
1
- type TrackMetadata = Record<string, string | number | boolean>;
2
- interface TrackImpressionArgs {
3
- /** The experiment identifier — typically the URL path, e.g. "/en/about" */
4
- experimentId: string;
5
- /** The assigned variant bucket, e.g. "a", "b" */
6
- variantBucket: string;
7
- visitorId: string;
8
- locale?: string;
9
- metadata?: TrackMetadata;
10
- }
11
- interface TrackConversionArgs {
12
- /** The experiment identifier — typically the URL path, e.g. "/en/about" */
13
- experimentId: string;
14
- /** The assigned variant bucket, e.g. "a", "b" */
15
- variantBucket: string;
16
- visitorId: string;
17
- /** Identifies what conversion goal was achieved, e.g. "cta_click", "purchase" */
18
- goalId: string;
19
- /** Optional numeric value for statistics */
20
- goalValue?: number;
21
- locale?: string;
22
- metadata?: TrackMetadata;
23
- }
24
- interface DateRange {
25
- /** 'NdaysAgo' or 'YYYY-MM-DD' */
26
- startDate: string;
27
- /** 'NdaysAgo', 'today', or 'YYYY-MM-DD' */
28
- endDate: string;
29
- }
30
- interface VariantStats {
31
- bucket: string;
32
- impressions: number;
33
- /** Fraction of total impressions (0–1) */
34
- impressionShare: number;
35
- conversions: number;
36
- /** conversions / impressions (0–1), 0 when impressions = 0 */
37
- conversionRate: number;
38
- }
39
- interface ExperimentStats {
40
- experimentId: string;
41
- dateRange: DateRange;
42
- variants: VariantStats[];
43
- totals: {
44
- impressions: number;
45
- conversions: number;
46
- };
47
- }
48
- interface AnalyticsAdapter {
49
- /**
50
- * Fire when a user is assigned to and shown a variant.
51
- * Called client-side from ExperimentTracker.
52
- */
53
- trackImpression(args: TrackImpressionArgs): void;
54
- /**
55
- * Fire when a user completes a conversion goal.
56
- * Called client-side from useABConversion.
57
- */
58
- trackConversion(args: TrackConversionArgs): void;
59
- /**
60
- * Optional: fire an impression server-side (RSC / Server Action / middleware).
61
- * Implemented via GA4 Measurement Protocol when apiSecret is provided.
62
- */
63
- trackImpressionServer?(args: TrackImpressionArgs): Promise<void>;
64
- /**
65
- * Optional: fetch aggregated stats for an experiment.
66
- * Powers the ExperimentStatsWidget admin component.
67
- * Requires propertyId and getAccessToken in the adapter config.
68
- */
69
- getStats?(experimentId: string, dateRange?: DateRange): Promise<ExperimentStats>;
70
- }
71
-
72
- export type { AnalyticsAdapter, DateRange, ExperimentStats, TrackConversionArgs, TrackImpressionArgs, VariantStats };
1
+ export { A as AnalyticsAdapter, D as DateRange, E as ExperimentStats, T as TrackConversionArgs, a as TrackImpressionArgs, V as VariantStats } from '../types-OJFBnrUD.js';
2
+ export { A as AbCookieConfig, R as ResolvedAbCookieNames, r as resolveAbCookieNames } from '../resolveAbCookieNames-DH8evjWm.js';
@@ -1 +1,19 @@
1
+ // src/cookie/utils/defaultGetExpCookieName.ts
2
+ function defaultGetExpCookieName(key) {
3
+ return `exp_${encodeURIComponent(key)}`;
4
+ }
5
+
6
+ // src/cookie/constants.ts
7
+ var DEFAULT_VISITOR_ID_COOKIE_NAME = "ab_visitor_id";
8
+
9
+ // src/cookie/utils/resolveAbCookieNames.ts
10
+ function resolveAbCookieNames(config, experimentId) {
11
+ return {
12
+ variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),
13
+ visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME
14
+ };
15
+ }
16
+ export {
17
+ resolveAbCookieNames
18
+ };
1
19
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../../src/cookie/utils/defaultGetExpCookieName.ts","../../src/cookie/constants.ts","../../src/cookie/utils/resolveAbCookieNames.ts"],"sourcesContent":["export function defaultGetExpCookieName(key: string) {\n return `exp_${encodeURIComponent(key)}`;\n}\n","export const DEFAULT_VISITOR_ID_COOKIE_NAME = \"ab_visitor_id\";\n","import type { AbCookieConfig } from \"../types\";\nimport { defaultGetExpCookieName } from \"./defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../constants\";\n\nexport interface ResolvedAbCookieNames {\n /** Resolved variant cookie name for the given experiment. */\n variantCookieName: string;\n /** Resolved visitor ID cookie name. */\n visitorCookieName: string;\n}\n\n/**\n * Resolves an `AbCookieConfig` + experiment ID into plain serializable strings.\n * Use this in Server Components to derive props for Client Components.\n */\nexport function resolveAbCookieNames(config: AbCookieConfig | undefined, experimentId: string): ResolvedAbCookieNames {\n return {\n variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),\n visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME,\n };\n}\n"],"mappings":";AAAO,SAAS,wBAAwB,KAAa;AACnD,SAAO,OAAO,mBAAmB,GAAG,CAAC;AACvC;;;ACFO,IAAM,iCAAiC;;;ACevC,SAAS,qBAAqB,QAAoC,cAA6C;AACpH,SAAO;AAAA,IACL,oBAAoB,QAAQ,oBAAoB,yBAAyB,YAAY;AAAA,IACrF,mBAAmB,QAAQ,uBAAuB;AAAA,EACpD;AACF;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Plugin } from 'payload';
2
2
  import { A as AbTestingPluginConfig } from './config-CQrzAj5Q.js';
3
3
  export { C as CollectionABConfig, S as StorageAdapter } from './config-CQrzAj5Q.js';
4
+ export { A as AbCookieConfig, R as ResolvedAbCookieNames, r as resolveAbCookieNames } from './resolveAbCookieNames-DH8evjWm.js';
4
5
 
5
6
  declare const abTestingPlugin: <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>) => Plugin;
6
7
 
package/dist/index.js CHANGED
@@ -290,7 +290,24 @@ var abTestingPlugin = (pluginConfig) => (incomingConfig) => {
290
290
  globals: [...incomingConfig.globals ?? [], ...extraGlobals]
291
291
  };
292
292
  };
293
+
294
+ // src/cookie/utils/defaultGetExpCookieName.ts
295
+ function defaultGetExpCookieName(key) {
296
+ return `exp_${encodeURIComponent(key)}`;
297
+ }
298
+
299
+ // src/cookie/constants.ts
300
+ var DEFAULT_VISITOR_ID_COOKIE_NAME = "ab_visitor_id";
301
+
302
+ // src/cookie/utils/resolveAbCookieNames.ts
303
+ function resolveAbCookieNames(config, experimentId) {
304
+ return {
305
+ variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),
306
+ visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME
307
+ };
308
+ }
293
309
  export {
294
- abTestingPlugin
310
+ abTestingPlugin,
311
+ resolveAbCookieNames
295
312
  };
296
313
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/buildVariantToParentCollectionSlugsMap.ts","../src/utils/resolveId.ts","../src/utils/getLocales.ts","../src/hooks/buildAfterChangeHook.ts","../src/hooks/buildAfterDeleteHook.ts","../src/hooks/validateVariantPercentageSum.ts","../src/utils/isFieldPathExists.ts","../src/constants.ts","../src/utils/addHooksToVariantCollections.ts","../src/plugin.ts"],"sourcesContent":["import type { CollectionABConfig } from \"../types/config\";\n\nexport function buildVariantToParentCollectionSlugsMap<TVariantData extends object>(\n collections: Record<string, CollectionABConfig<TVariantData>>,\n) {\n const variantToParent = new Map<string, string>();\n\n for (const [parentSlug, abConfig] of Object.entries(collections)) {\n if (abConfig) variantToParent.set(abConfig.variantCollectionSlug, parentSlug);\n }\n\n return variantToParent;\n}\n","interface BaseDocument {\n id: number | string;\n}\n\nconst isValueIsBaseDocument = (value: object): value is BaseDocument => {\n return \"id\" in value;\n};\n\nexport function resolveId(value: unknown) {\n if (!value) return null;\n\n if (typeof value === \"number\" || typeof value === \"string\") return value;\n\n if (typeof value === \"object\" && isValueIsBaseDocument(value)) {\n return value.id;\n }\n\n return null;\n}\n","import type { Payload } from \"payload\";\n\nexport function getLocales(payload: Payload): (string | undefined)[] {\n const { localization } = payload.config;\n\n if (!localization) return [undefined];\n\n const { locales } = localization;\n\n if (!locales?.length) return [undefined];\n\n return locales.map((l) => (typeof l === \"string\" ? l : l.code));\n}\n","import type { CollectionAfterChangeHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterChangeHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterChangeHook {\n return async ({ doc, req, previousDoc }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n const previousPageId = previousDoc ? resolveId(previousDoc.page) : null;\n if (previousPageId && previousPageId !== pageId) {\n for (const locale of locales) {\n const oldPageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: previousPageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!oldPageDoc) continue;\n\n const oldManifestKey = abConfig.generatePath({ doc: oldPageDoc, locale });\n if (!oldManifestKey) continue;\n\n const remainingOldVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: previousPageId } }, { id: { not_equals: doc.id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingOldVariants.docs.length === 0) {\n await pluginConfig.storage.clear(oldManifestKey, payload);\n } else {\n const oldVariantData = remainingOldVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: oldPageDoc, variantDoc, locale }),\n );\n await pluginConfig.storage.write(oldManifestKey, oldVariantData, payload);\n }\n }\n }\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const allVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: { page: { equals: pageId } },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n const variantData = allVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n };\n}\n","import type { CollectionAfterDeleteHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterDeleteHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterDeleteHook {\n return async ({ doc, req, id }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const remainingVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: pageId } }, { id: { not_equals: id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingVariants.docs.length === 0) {\n await pluginConfig.storage.clear(manifestKey, payload);\n } else {\n const variantData = remainingVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n }\n };\n}\n","import { ValidationError } from \"payload\";\nimport type { CollectionBeforeChangeHook, CollectionSlug, Where } from \"payload\";\nimport { resolveId } from \"../utils/resolveId\";\n\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n return path.split(\".\").reduce((acc: unknown, key) => {\n if (acc !== null && typeof acc === \"object\") {\n return (acc as Record<string, unknown>)[key];\n }\n\n return undefined;\n }, obj);\n}\n\ninterface ValidateVariantPercentageSumConfig {\n variantCollectionSlug: CollectionSlug;\n parentField: string;\n passPercentageField: string;\n tenantField?: string;\n}\n\nexport function createValidateVariantPercentageSum(\n config: ValidateVariantPercentageSumConfig,\n): CollectionBeforeChangeHook {\n const { variantCollectionSlug, parentField, passPercentageField, tenantField } = config;\n\n return async ({ data, originalDoc, req, operation }) => {\n const passPercentage = getNestedValue(data, passPercentageField);\n if (passPercentage === undefined || passPercentage === null) return data;\n\n const parentId = resolveId(getNestedValue(data, parentField));\n if (!parentId) return data;\n\n const andConditions: Where[] = [{ [parentField]: { equals: parentId } }];\n\n if (tenantField) {\n const tenantId = resolveId(getNestedValue(data, tenantField));\n if (tenantId) {\n andConditions.push({ [tenantField]: { equals: tenantId } });\n }\n }\n\n if (operation === \"update\" && originalDoc?.id) {\n andConditions.push({ id: { not_equals: originalDoc.id } });\n }\n\n const { docs: siblings } = await req.payload.find({\n collection: variantCollectionSlug,\n where: { and: andConditions },\n depth: 0,\n req,\n });\n\n const existingSum = siblings.reduce((sum, doc) => {\n const percentage = getNestedValue(doc, passPercentageField);\n\n return sum + (typeof percentage === \"number\" ? percentage : 0);\n }, 0);\n\n if (existingSum + (passPercentage as number) > 100) {\n const remaining = 100 - existingSum;\n throw new ValidationError({\n errors: [\n {\n path: passPercentageField,\n message: `Total variant traffic for this page is ${existingSum}%. This variant cannot exceed ${remaining}% (would exceed 100%).`,\n },\n ],\n });\n }\n\n return data;\n };\n}\n","import type { Field } from \"payload\";\n\ntype TabLike = { name?: string; fields: Field[] };\n\ntype TabsFieldLike = { type: \"tabs\"; tabs: TabLike[] };\n\ntype FieldsContainer = { fields: Field[] };\n\nexport function isFieldPathExists(fields: Field[], path: string): boolean {\n const [head, ...rest] = path.split(\".\");\n\n for (const field of fields) {\n // Named fields\n if (\"name\" in field && field.name === head) {\n if (rest.length === 0) return true;\n\n if (\"fields\" in field && Array.isArray((field as unknown as FieldsContainer).fields)) {\n return isFieldPathExists((field as unknown as FieldsContainer).fields, rest.join(\".\"));\n }\n\n return false;\n }\n\n // Tabs field\n if (field.type === \"tabs\") {\n const { tabs } = field as unknown as TabsFieldLike;\n\n for (const tab of tabs) {\n if (tab.name) {\n if (tab.name === head) {\n if (rest.length === 0) return true;\n\n if (isFieldPathExists(tab.fields, rest.join(\".\"))) return true;\n }\n } else {\n if (isFieldPathExists(tab.fields, path)) return true;\n }\n }\n }\n\n // Transparent layout fields\n if (\n (field.type === \"row\" || field.type === \"collapsible\")\n && \"fields\" in field\n && Array.isArray((field as unknown as FieldsContainer).fields)\n ) {\n if (isFieldPathExists((field as unknown as FieldsContainer).fields, path)) return true;\n }\n }\n\n return false;\n}\n","export const DEFAULT_PASS_PERCENTAGE_FIELD = \"passPercentage\";\nexport const DEFAULT_PARENT_FIELD = \"page\";\n","import type { CollectionConfig, CollectionSlug, Config } from \"payload\";\nimport type { AbTestingPluginConfig } from \"../types/config\";\nimport { buildAfterChangeHook } from \"../hooks/buildAfterChangeHook\";\nimport { buildAfterDeleteHook } from \"../hooks/buildAfterDeleteHook\";\nimport { createValidateVariantPercentageSum } from \"../hooks/validateVariantPercentageSum\";\nimport { isFieldPathExists } from \"./isFieldPathExists\";\nimport { DEFAULT_PARENT_FIELD, DEFAULT_PASS_PERCENTAGE_FIELD } from \"../constants\";\n\nexport function addHooksToVariantCollections<TVariantData extends object>(\n config: Config,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n variantToParent: Map<string, string>,\n) {\n const patchedCollections = (config.collections ?? []).map((collection) => {\n const parentSlug = variantToParent.get(collection.slug);\n if (!parentSlug) return collection;\n\n const abConfig = pluginConfig.collections[parentSlug];\n if (!abConfig) return collection;\n\n let resolvedPassPercentageField: string | null = null;\n\n const passPercentagePath =\n typeof abConfig.passPercentageField === \"string\" ? abConfig.passPercentageField : DEFAULT_PASS_PERCENTAGE_FIELD;\n\n if (isFieldPathExists(collection.fields, passPercentagePath)) {\n resolvedPassPercentageField = passPercentagePath;\n }\n\n const beforeChangeHooks = [...(collection.hooks?.beforeChange ?? [])];\n\n if (resolvedPassPercentageField) {\n beforeChangeHooks.push(\n createValidateVariantPercentageSum({\n variantCollectionSlug: abConfig.variantCollectionSlug as CollectionSlug,\n parentField: abConfig.parentField ?? DEFAULT_PARENT_FIELD,\n passPercentageField: resolvedPassPercentageField,\n tenantField: abConfig.tenantField,\n }),\n );\n }\n\n return {\n ...collection,\n hooks: {\n ...collection.hooks,\n beforeChange: beforeChangeHooks,\n afterChange: [\n ...(collection.hooks?.afterChange ?? []),\n buildAfterChangeHook(parentSlug, abConfig, pluginConfig),\n ],\n afterDelete: [\n ...(collection.hooks?.afterDelete ?? []),\n buildAfterDeleteHook(parentSlug, abConfig, pluginConfig),\n ],\n },\n } as CollectionConfig;\n });\n\n return patchedCollections;\n}\n","import type { Config, Plugin } from \"payload\";\nimport type { AbTestingPluginConfig } from \"./types/config\";\nimport { buildVariantToParentCollectionSlugsMap } from \"./utils/buildVariantToParentCollectionSlugsMap\";\nimport { addHooksToVariantCollections } from \"./utils/addHooksToVariantCollections\";\n\nexport const abTestingPlugin =\n <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): Plugin =>\n (incomingConfig: Config): Config => {\n const { enabled = true, debug = false, collections, storage } = pluginConfig;\n\n if (!enabled) return incomingConfig;\n\n const extraGlobals = storage.createGlobal ? [storage.createGlobal(debug)] : [];\n\n const variantToParent = buildVariantToParentCollectionSlugsMap<TVariantData>(collections);\n\n const patchedCollections = addHooksToVariantCollections<TVariantData>(\n incomingConfig,\n pluginConfig,\n variantToParent,\n );\n\n return {\n ...incomingConfig,\n collections: patchedCollections,\n globals: [...(incomingConfig.globals ?? []), ...extraGlobals],\n };\n };\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/utils/buildVariantToParentCollectionSlugsMap.ts","../src/utils/resolveId.ts","../src/utils/getLocales.ts","../src/hooks/buildAfterChangeHook.ts","../src/hooks/buildAfterDeleteHook.ts","../src/hooks/validateVariantPercentageSum.ts","../src/utils/isFieldPathExists.ts","../src/constants.ts","../src/utils/addHooksToVariantCollections.ts","../src/plugin.ts","../src/cookie/utils/defaultGetExpCookieName.ts","../src/cookie/constants.ts","../src/cookie/utils/resolveAbCookieNames.ts"],"sourcesContent":["import type { CollectionABConfig } from \"../types/config\";\n\nexport function buildVariantToParentCollectionSlugsMap<TVariantData extends object>(\n collections: Record<string, CollectionABConfig<TVariantData>>,\n) {\n const variantToParent = new Map<string, string>();\n\n for (const [parentSlug, abConfig] of Object.entries(collections)) {\n if (abConfig) variantToParent.set(abConfig.variantCollectionSlug, parentSlug);\n }\n\n return variantToParent;\n}\n","interface BaseDocument {\n id: number | string;\n}\n\nconst isValueIsBaseDocument = (value: object): value is BaseDocument => {\n return \"id\" in value;\n};\n\nexport function resolveId(value: unknown) {\n if (!value) return null;\n\n if (typeof value === \"number\" || typeof value === \"string\") return value;\n\n if (typeof value === \"object\" && isValueIsBaseDocument(value)) {\n return value.id;\n }\n\n return null;\n}\n","import type { Payload } from \"payload\";\n\nexport function getLocales(payload: Payload): (string | undefined)[] {\n const { localization } = payload.config;\n\n if (!localization) return [undefined];\n\n const { locales } = localization;\n\n if (!locales?.length) return [undefined];\n\n return locales.map((l) => (typeof l === \"string\" ? l : l.code));\n}\n","import type { CollectionAfterChangeHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterChangeHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterChangeHook {\n return async ({ doc, req, previousDoc }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n const previousPageId = previousDoc ? resolveId(previousDoc.page) : null;\n if (previousPageId && previousPageId !== pageId) {\n for (const locale of locales) {\n const oldPageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: previousPageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!oldPageDoc) continue;\n\n const oldManifestKey = abConfig.generatePath({ doc: oldPageDoc, locale });\n if (!oldManifestKey) continue;\n\n const remainingOldVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: previousPageId } }, { id: { not_equals: doc.id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingOldVariants.docs.length === 0) {\n await pluginConfig.storage.clear(oldManifestKey, payload);\n } else {\n const oldVariantData = remainingOldVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: oldPageDoc, variantDoc, locale }),\n );\n await pluginConfig.storage.write(oldManifestKey, oldVariantData, payload);\n }\n }\n }\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const allVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: { page: { equals: pageId } },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n const variantData = allVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n };\n}\n","import type { CollectionAfterDeleteHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterDeleteHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterDeleteHook {\n return async ({ doc, req, id }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const remainingVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: pageId } }, { id: { not_equals: id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingVariants.docs.length === 0) {\n await pluginConfig.storage.clear(manifestKey, payload);\n } else {\n const variantData = remainingVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n }\n };\n}\n","import { ValidationError } from \"payload\";\nimport type { CollectionBeforeChangeHook, CollectionSlug, Where } from \"payload\";\nimport { resolveId } from \"../utils/resolveId\";\n\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n return path.split(\".\").reduce((acc: unknown, key) => {\n if (acc !== null && typeof acc === \"object\") {\n return (acc as Record<string, unknown>)[key];\n }\n\n return undefined;\n }, obj);\n}\n\ninterface ValidateVariantPercentageSumConfig {\n variantCollectionSlug: CollectionSlug;\n parentField: string;\n passPercentageField: string;\n tenantField?: string;\n}\n\nexport function createValidateVariantPercentageSum(\n config: ValidateVariantPercentageSumConfig,\n): CollectionBeforeChangeHook {\n const { variantCollectionSlug, parentField, passPercentageField, tenantField } = config;\n\n return async ({ data, originalDoc, req, operation }) => {\n const passPercentage = getNestedValue(data, passPercentageField);\n if (passPercentage === undefined || passPercentage === null) return data;\n\n const parentId = resolveId(getNestedValue(data, parentField));\n if (!parentId) return data;\n\n const andConditions: Where[] = [{ [parentField]: { equals: parentId } }];\n\n if (tenantField) {\n const tenantId = resolveId(getNestedValue(data, tenantField));\n if (tenantId) {\n andConditions.push({ [tenantField]: { equals: tenantId } });\n }\n }\n\n if (operation === \"update\" && originalDoc?.id) {\n andConditions.push({ id: { not_equals: originalDoc.id } });\n }\n\n const { docs: siblings } = await req.payload.find({\n collection: variantCollectionSlug,\n where: { and: andConditions },\n depth: 0,\n req,\n });\n\n const existingSum = siblings.reduce((sum, doc) => {\n const percentage = getNestedValue(doc, passPercentageField);\n\n return sum + (typeof percentage === \"number\" ? percentage : 0);\n }, 0);\n\n if (existingSum + (passPercentage as number) > 100) {\n const remaining = 100 - existingSum;\n throw new ValidationError({\n errors: [\n {\n path: passPercentageField,\n message: `Total variant traffic for this page is ${existingSum}%. This variant cannot exceed ${remaining}% (would exceed 100%).`,\n },\n ],\n });\n }\n\n return data;\n };\n}\n","import type { Field } from \"payload\";\n\ntype TabLike = { name?: string; fields: Field[] };\n\ntype TabsFieldLike = { type: \"tabs\"; tabs: TabLike[] };\n\ntype FieldsContainer = { fields: Field[] };\n\nexport function isFieldPathExists(fields: Field[], path: string): boolean {\n const [head, ...rest] = path.split(\".\");\n\n for (const field of fields) {\n // Named fields\n if (\"name\" in field && field.name === head) {\n if (rest.length === 0) return true;\n\n if (\"fields\" in field && Array.isArray((field as unknown as FieldsContainer).fields)) {\n return isFieldPathExists((field as unknown as FieldsContainer).fields, rest.join(\".\"));\n }\n\n return false;\n }\n\n // Tabs field\n if (field.type === \"tabs\") {\n const { tabs } = field as unknown as TabsFieldLike;\n\n for (const tab of tabs) {\n if (tab.name) {\n if (tab.name === head) {\n if (rest.length === 0) return true;\n\n if (isFieldPathExists(tab.fields, rest.join(\".\"))) return true;\n }\n } else {\n if (isFieldPathExists(tab.fields, path)) return true;\n }\n }\n }\n\n // Transparent layout fields\n if (\n (field.type === \"row\" || field.type === \"collapsible\")\n && \"fields\" in field\n && Array.isArray((field as unknown as FieldsContainer).fields)\n ) {\n if (isFieldPathExists((field as unknown as FieldsContainer).fields, path)) return true;\n }\n }\n\n return false;\n}\n","export const DEFAULT_PASS_PERCENTAGE_FIELD = \"passPercentage\";\nexport const DEFAULT_PARENT_FIELD = \"page\";\n","import type { CollectionConfig, CollectionSlug, Config } from \"payload\";\nimport type { AbTestingPluginConfig } from \"../types/config\";\nimport { buildAfterChangeHook } from \"../hooks/buildAfterChangeHook\";\nimport { buildAfterDeleteHook } from \"../hooks/buildAfterDeleteHook\";\nimport { createValidateVariantPercentageSum } from \"../hooks/validateVariantPercentageSum\";\nimport { isFieldPathExists } from \"./isFieldPathExists\";\nimport { DEFAULT_PARENT_FIELD, DEFAULT_PASS_PERCENTAGE_FIELD } from \"../constants\";\n\nexport function addHooksToVariantCollections<TVariantData extends object>(\n config: Config,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n variantToParent: Map<string, string>,\n) {\n const patchedCollections = (config.collections ?? []).map((collection) => {\n const parentSlug = variantToParent.get(collection.slug);\n if (!parentSlug) return collection;\n\n const abConfig = pluginConfig.collections[parentSlug];\n if (!abConfig) return collection;\n\n let resolvedPassPercentageField: string | null = null;\n\n const passPercentagePath =\n typeof abConfig.passPercentageField === \"string\" ? abConfig.passPercentageField : DEFAULT_PASS_PERCENTAGE_FIELD;\n\n if (isFieldPathExists(collection.fields, passPercentagePath)) {\n resolvedPassPercentageField = passPercentagePath;\n }\n\n const beforeChangeHooks = [...(collection.hooks?.beforeChange ?? [])];\n\n if (resolvedPassPercentageField) {\n beforeChangeHooks.push(\n createValidateVariantPercentageSum({\n variantCollectionSlug: abConfig.variantCollectionSlug as CollectionSlug,\n parentField: abConfig.parentField ?? DEFAULT_PARENT_FIELD,\n passPercentageField: resolvedPassPercentageField,\n tenantField: abConfig.tenantField,\n }),\n );\n }\n\n return {\n ...collection,\n hooks: {\n ...collection.hooks,\n beforeChange: beforeChangeHooks,\n afterChange: [\n ...(collection.hooks?.afterChange ?? []),\n buildAfterChangeHook(parentSlug, abConfig, pluginConfig),\n ],\n afterDelete: [\n ...(collection.hooks?.afterDelete ?? []),\n buildAfterDeleteHook(parentSlug, abConfig, pluginConfig),\n ],\n },\n } as CollectionConfig;\n });\n\n return patchedCollections;\n}\n","import type { Config, Plugin } from \"payload\";\nimport type { AbTestingPluginConfig } from \"./types/config\";\nimport { buildVariantToParentCollectionSlugsMap } from \"./utils/buildVariantToParentCollectionSlugsMap\";\nimport { addHooksToVariantCollections } from \"./utils/addHooksToVariantCollections\";\n\nexport const abTestingPlugin =\n <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): Plugin =>\n (incomingConfig: Config): Config => {\n const { enabled = true, debug = false, collections, storage } = pluginConfig;\n\n if (!enabled) return incomingConfig;\n\n const extraGlobals = storage.createGlobal ? [storage.createGlobal(debug)] : [];\n\n const variantToParent = buildVariantToParentCollectionSlugsMap<TVariantData>(collections);\n\n const patchedCollections = addHooksToVariantCollections<TVariantData>(\n incomingConfig,\n pluginConfig,\n variantToParent,\n );\n\n return {\n ...incomingConfig,\n collections: patchedCollections,\n globals: [...(incomingConfig.globals ?? []), ...extraGlobals],\n };\n };\n","export function defaultGetExpCookieName(key: string) {\n return `exp_${encodeURIComponent(key)}`;\n}\n","export const DEFAULT_VISITOR_ID_COOKIE_NAME = \"ab_visitor_id\";\n","import type { AbCookieConfig } from \"../types\";\nimport { defaultGetExpCookieName } from \"./defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../constants\";\n\nexport interface ResolvedAbCookieNames {\n /** Resolved variant cookie name for the given experiment. */\n variantCookieName: string;\n /** Resolved visitor ID cookie name. */\n visitorCookieName: string;\n}\n\n/**\n * Resolves an `AbCookieConfig` + experiment ID into plain serializable strings.\n * Use this in Server Components to derive props for Client Components.\n */\nexport function resolveAbCookieNames(config: AbCookieConfig | undefined, experimentId: string): ResolvedAbCookieNames {\n return {\n variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),\n visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,52 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { S as StorageAdapter } from '../config-CQrzAj5Q.js';
3
+ import { A as AbCookieConfig } from '../resolveAbCookieNames-DH8evjWm.js';
4
+ export { R as ResolvedAbCookieNames, r as resolveAbCookieNames } from '../resolveAbCookieNames-DH8evjWm.js';
5
+ import 'payload';
6
+
7
+ interface ResolveAbRewriteCookieConfig extends AbCookieConfig {
8
+ /**
9
+ * Prefix for the per-path bucket-assignment cookie.
10
+ * Full name: `{prefix}_{encodeURIComponent(visiblePathname)}`.
11
+ * Default: 'payload-ab-bucket'.
12
+ */
13
+ bucketCookiePrefix?: string;
14
+ /** Max age for the visitor ID cookie in seconds. Default: 31_536_000 (365 days). */
15
+ visitorIdMaxAge?: number;
16
+ /** Max age for the experiment bucket cookie in seconds. Default: 7_776_000 (90 days). */
17
+ expCookieMaxAge?: number;
18
+ }
19
+ interface ResolveAbRewriteConfig<TVariantData extends object = object> {
20
+ /** Storage adapter instance — must be the same one passed to the plugin. */
21
+ storage: StorageAdapter<TVariantData>;
22
+ /**
23
+ * Extract the bucket string from a variant record.
24
+ * The shape of TVariantData is whatever your `generateVariantData` returns,
25
+ * so you decide which field holds the bucket identifier.
26
+ */
27
+ getBucket: (variant: TVariantData) => string;
28
+ /**
29
+ * Extract the Next.js internal rewrite path from a variant record.
30
+ * Again, the field name is whatever you put in `generateVariantData`.
31
+ */
32
+ getRewritePath: (variant: TVariantData) => string;
33
+ /**
34
+ * Extract the traffic percentage (0–100) for a variant.
35
+ * When provided, variants are selected by weight and the remaining
36
+ * percentage (100 − sum of all variant weights) is assigned to 'original'.
37
+ * When omitted, all variants and 'original' share equal probability.
38
+ */
39
+ getPassPercentage?: (variant: TVariantData) => number;
40
+ /** Cookie names and TTLs — all have sensible defaults. */
41
+ cookies?: ResolveAbRewriteCookieConfig;
42
+ }
43
+
44
+ declare function createResolveAbRewrite<TVariantData extends object>(config: ResolveAbRewriteConfig<TVariantData>): (request: NextRequest,
45
+ /** The URL pathname visible to the user (used as bucket cookie key). */
46
+ visiblePathname: string,
47
+ /** The manifest key to look up — typically the internal rewrite path. */
48
+ manifestKey: string,
49
+ /** Path to rewrite to when no variant is selected ('original' bucket). */
50
+ originalRewritePath: string) => Promise<NextResponse | null>;
51
+
52
+ export { AbCookieConfig, type ResolveAbRewriteConfig, type ResolveAbRewriteCookieConfig, createResolveAbRewrite };
@@ -0,0 +1,118 @@
1
+ // src/middleware/resolveAbRewrite.ts
2
+ import { NextResponse } from "next/server";
3
+
4
+ // src/middleware/utils/pickUniformBucket.ts
5
+ function pickUniformBucket(variants, getBucket) {
6
+ const idx = Math.floor(Math.random() * (variants.length + 1));
7
+ if (idx === variants.length) return "original";
8
+ const selected = variants[idx];
9
+ return selected !== void 0 ? getBucket(selected) : "original";
10
+ }
11
+
12
+ // src/middleware/utils/pickWeightedBucket.ts
13
+ function pickWeightedBucket(variants, getBucket, getPassPercentage) {
14
+ const totalVariantWeight = variants.reduce((sum, v) => sum + getPassPercentage(v), 0);
15
+ const originalWeight = Math.max(0, 100 - totalVariantWeight);
16
+ const total = totalVariantWeight + originalWeight;
17
+ if (totalVariantWeight === 0) return pickUniformBucket(variants, getBucket);
18
+ const rand = Math.random() * total;
19
+ let cumulative = 0;
20
+ for (const variant of variants) {
21
+ cumulative += getPassPercentage(variant);
22
+ if (rand < cumulative) return getBucket(variant);
23
+ }
24
+ return "original";
25
+ }
26
+
27
+ // src/cookie/constants.ts
28
+ var DEFAULT_VISITOR_ID_COOKIE_NAME = "ab_visitor_id";
29
+
30
+ // src/cookie/utils/defaultGetExpCookieName.ts
31
+ function defaultGetExpCookieName(key) {
32
+ return `exp_${encodeURIComponent(key)}`;
33
+ }
34
+
35
+ // src/middleware/resolveAbRewrite.ts
36
+ var DEFAULT_BUCKET_COOKIE_PREFIX = "payload-ab-bucket";
37
+ var DEFAULT_VISITOR_MAX_AGE = 60 * 60 * 24 * 365;
38
+ var DEFAULT_EXP_MAX_AGE = 60 * 60 * 24 * 90;
39
+ function createResolveAbRewrite(config) {
40
+ const { storage, getBucket, getRewritePath, getPassPercentage, cookies: cookieConfig = {} } = config;
41
+ const {
42
+ bucketCookiePrefix = DEFAULT_BUCKET_COOKIE_PREFIX,
43
+ visitorIdCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME,
44
+ getExpCookieName = defaultGetExpCookieName,
45
+ visitorIdMaxAge = DEFAULT_VISITOR_MAX_AGE,
46
+ expCookieMaxAge = DEFAULT_EXP_MAX_AGE
47
+ } = cookieConfig;
48
+ return async function resolveAbRewrite(request, visiblePathname, manifestKey, originalRewritePath) {
49
+ let variants = null;
50
+ try {
51
+ variants = await storage.read(manifestKey);
52
+ } catch {
53
+ return null;
54
+ }
55
+ if (!variants?.length) return null;
56
+ const bucketCookieName = `${bucketCookiePrefix}_${encodeURIComponent(visiblePathname)}`;
57
+ const existingBucket = request.cookies.get(bucketCookieName)?.value;
58
+ const existingVisitorId = request.cookies.get(visitorIdCookieName)?.value;
59
+ const visitorId = existingVisitorId ?? crypto.randomUUID();
60
+ const expCookieName = getExpCookieName(manifestKey);
61
+ let bucket = existingBucket;
62
+ if (!bucket) {
63
+ bucket = getPassPercentage ? pickWeightedBucket(variants, getBucket, getPassPercentage) : pickUniformBucket(variants, getBucket);
64
+ }
65
+ const setAbCookies = (res2, assignedBucket, isNewAssignment) => {
66
+ if (!existingVisitorId) {
67
+ res2.cookies.set(visitorIdCookieName, visitorId, {
68
+ path: "/",
69
+ sameSite: "lax",
70
+ maxAge: visitorIdMaxAge
71
+ });
72
+ }
73
+ if (isNewAssignment) {
74
+ res2.cookies.set(expCookieName, assignedBucket, {
75
+ path: "/",
76
+ sameSite: "lax",
77
+ maxAge: expCookieMaxAge
78
+ });
79
+ }
80
+ };
81
+ if (bucket === "original") {
82
+ if (!existingBucket) {
83
+ const url2 = request.nextUrl.clone();
84
+ url2.pathname = originalRewritePath;
85
+ const res2 = NextResponse.rewrite(url2);
86
+ res2.cookies.set(bucketCookieName, "original", { path: "/", sameSite: "lax" });
87
+ setAbCookies(res2, "original", true);
88
+ return res2;
89
+ }
90
+ return null;
91
+ }
92
+ const match = variants.find((v) => getBucket(v) === bucket);
93
+ if (!match) return null;
94
+ const url = request.nextUrl.clone();
95
+ url.pathname = getRewritePath(match);
96
+ const res = NextResponse.rewrite(url);
97
+ if (!existingBucket) {
98
+ res.cookies.set(bucketCookieName, bucket, { path: "/", sameSite: "lax" });
99
+ setAbCookies(res, bucket, true);
100
+ } else {
101
+ setAbCookies(res, bucket, false);
102
+ }
103
+ return res;
104
+ };
105
+ }
106
+
107
+ // src/cookie/utils/resolveAbCookieNames.ts
108
+ function resolveAbCookieNames(config, experimentId) {
109
+ return {
110
+ variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),
111
+ visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME
112
+ };
113
+ }
114
+ export {
115
+ createResolveAbRewrite,
116
+ resolveAbCookieNames
117
+ };
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/middleware/resolveAbRewrite.ts","../../src/middleware/utils/pickUniformBucket.ts","../../src/middleware/utils/pickWeightedBucket.ts","../../src/cookie/constants.ts","../../src/cookie/utils/defaultGetExpCookieName.ts","../../src/cookie/utils/resolveAbCookieNames.ts"],"sourcesContent":["import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport type { ResolveAbRewriteConfig } from \"./types\";\nimport { pickWeightedBucket } from \"./utils/pickWeightedBucket\";\nimport { pickUniformBucket } from \"./utils/pickUniformBucket\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../cookie/constants\";\nimport { defaultGetExpCookieName } from \"../cookie/utils/defaultGetExpCookieName\";\n\nconst DEFAULT_BUCKET_COOKIE_PREFIX = \"payload-ab-bucket\";\nconst DEFAULT_VISITOR_MAX_AGE = 60 * 60 * 24 * 365;\nconst DEFAULT_EXP_MAX_AGE = 60 * 60 * 24 * 90;\n\nexport function createResolveAbRewrite<TVariantData extends object>(config: ResolveAbRewriteConfig<TVariantData>) {\n const { storage, getBucket, getRewritePath, getPassPercentage, cookies: cookieConfig = {} } = config;\n\n const {\n bucketCookiePrefix = DEFAULT_BUCKET_COOKIE_PREFIX,\n visitorIdCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME,\n getExpCookieName = defaultGetExpCookieName,\n visitorIdMaxAge = DEFAULT_VISITOR_MAX_AGE,\n expCookieMaxAge = DEFAULT_EXP_MAX_AGE,\n } = cookieConfig;\n\n return async function resolveAbRewrite(\n request: NextRequest,\n /** The URL pathname visible to the user (used as bucket cookie key). */\n visiblePathname: string,\n /** The manifest key to look up — typically the internal rewrite path. */\n manifestKey: string,\n /** Path to rewrite to when no variant is selected ('original' bucket). */\n originalRewritePath: string,\n ): Promise<NextResponse | null> {\n let variants: TVariantData[] | null = null;\n\n try {\n variants = await storage.read(manifestKey);\n } catch {\n return null;\n }\n\n if (!variants?.length) return null;\n\n const bucketCookieName = `${bucketCookiePrefix}_${encodeURIComponent(visiblePathname)}`;\n const existingBucket = request.cookies.get(bucketCookieName)?.value;\n\n const existingVisitorId = request.cookies.get(visitorIdCookieName)?.value;\n const visitorId = existingVisitorId ?? crypto.randomUUID();\n\n const expCookieName = getExpCookieName(manifestKey);\n\n let bucket = existingBucket;\n if (!bucket) {\n bucket =\n getPassPercentage ?\n pickWeightedBucket(variants, getBucket, getPassPercentage)\n : pickUniformBucket(variants, getBucket);\n }\n\n const setAbCookies = (res: NextResponse, assignedBucket: string, isNewAssignment: boolean) => {\n if (!existingVisitorId) {\n res.cookies.set(visitorIdCookieName, visitorId, {\n path: \"/\",\n sameSite: \"lax\",\n maxAge: visitorIdMaxAge,\n });\n }\n\n if (isNewAssignment) {\n res.cookies.set(expCookieName, assignedBucket, {\n path: \"/\",\n sameSite: \"lax\",\n maxAge: expCookieMaxAge,\n });\n }\n };\n\n if (bucket === \"original\") {\n if (!existingBucket) {\n const url = request.nextUrl.clone();\n\n url.pathname = originalRewritePath;\n\n const res = NextResponse.rewrite(url);\n\n res.cookies.set(bucketCookieName, \"original\", { path: \"/\", sameSite: \"lax\" });\n setAbCookies(res, \"original\", true);\n\n return res;\n }\n\n return null;\n }\n\n const match = variants.find((v) => getBucket(v) === bucket);\n if (!match) return null;\n\n const url = request.nextUrl.clone();\n url.pathname = getRewritePath(match);\n const res = NextResponse.rewrite(url);\n\n if (!existingBucket) {\n res.cookies.set(bucketCookieName, bucket, { path: \"/\", sameSite: \"lax\" });\n setAbCookies(res, bucket, true);\n } else {\n setAbCookies(res, bucket, false);\n }\n\n return res;\n };\n}\n","export function pickUniformBucket<T extends object>(variants: T[], getBucket: (v: T) => string) {\n const idx = Math.floor(Math.random() * (variants.length + 1));\n\n if (idx === variants.length) return \"original\";\n\n const selected = variants[idx];\n\n return selected !== undefined ? getBucket(selected) : \"original\";\n}\n","import { pickUniformBucket } from \"./pickUniformBucket\";\n\nexport function pickWeightedBucket<T extends object>(\n variants: T[],\n getBucket: (v: T) => string,\n getPassPercentage: (v: T) => number,\n) {\n const totalVariantWeight = variants.reduce((sum, v) => sum + getPassPercentage(v), 0);\n const originalWeight = Math.max(0, 100 - totalVariantWeight);\n const total = totalVariantWeight + originalWeight;\n\n if (totalVariantWeight === 0) return pickUniformBucket(variants, getBucket);\n\n const rand = Math.random() * total;\n\n let cumulative = 0;\n for (const variant of variants) {\n cumulative += getPassPercentage(variant);\n\n if (rand < cumulative) return getBucket(variant);\n }\n\n return \"original\";\n}\n","export const DEFAULT_VISITOR_ID_COOKIE_NAME = \"ab_visitor_id\";\n","export function defaultGetExpCookieName(key: string) {\n return `exp_${encodeURIComponent(key)}`;\n}\n","import type { AbCookieConfig } from \"../types\";\nimport { defaultGetExpCookieName } from \"./defaultGetExpCookieName\";\nimport { DEFAULT_VISITOR_ID_COOKIE_NAME } from \"../constants\";\n\nexport interface ResolvedAbCookieNames {\n /** Resolved variant cookie name for the given experiment. */\n variantCookieName: string;\n /** Resolved visitor ID cookie name. */\n visitorCookieName: string;\n}\n\n/**\n * Resolves an `AbCookieConfig` + experiment ID into plain serializable strings.\n * Use this in Server Components to derive props for Client Components.\n */\nexport function resolveAbCookieNames(config: AbCookieConfig | undefined, experimentId: string): ResolvedAbCookieNames {\n return {\n variantCookieName: (config?.getExpCookieName ?? defaultGetExpCookieName)(experimentId),\n visitorCookieName: config?.visitorIdCookieName ?? DEFAULT_VISITOR_ID_COOKIE_NAME,\n };\n}\n"],"mappings":"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","names":["res","url"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared cookie name configuration consumed by both middleware and analytics utilities.
3
+ * Create one instance and pass it to `createResolveAbRewrite` and all analytics
4
+ * components/hooks to keep cookie names in sync automatically.
5
+ */
6
+ interface AbCookieConfig {
7
+ /**
8
+ * Cookie name used to track a stable visitor across sessions.
9
+ * Default: 'ab_visitor_id'.
10
+ */
11
+ visitorIdCookieName?: string;
12
+ /**
13
+ * Derive the experiment tracking cookie name from the experiment/manifest key.
14
+ * This cookie is written by middleware and read by analytics utilities.
15
+ * Default: `exp_${encodeURIComponent(key)}`.
16
+ */
17
+ getExpCookieName?: (key: string) => string;
18
+ }
19
+
20
+ interface ResolvedAbCookieNames {
21
+ /** Resolved variant cookie name for the given experiment. */
22
+ variantCookieName: string;
23
+ /** Resolved visitor ID cookie name. */
24
+ visitorCookieName: string;
25
+ }
26
+ /**
27
+ * Resolves an `AbCookieConfig` + experiment ID into plain serializable strings.
28
+ * Use this in Server Components to derive props for Client Components.
29
+ */
30
+ declare function resolveAbCookieNames(config: AbCookieConfig | undefined, experimentId: string): ResolvedAbCookieNames;
31
+
32
+ export { type AbCookieConfig as A, type ResolvedAbCookieNames as R, resolveAbCookieNames as r };
@@ -0,0 +1,72 @@
1
+ type TrackMetadata = Record<string, string | number | boolean>;
2
+ interface TrackImpressionArgs {
3
+ /** The experiment identifier — typically the URL path, e.g. "/en/about" */
4
+ experimentId: string;
5
+ /** The assigned variant bucket, e.g. "a", "b" */
6
+ variantBucket: string;
7
+ visitorId: string;
8
+ locale?: string;
9
+ metadata?: TrackMetadata;
10
+ }
11
+ interface TrackConversionArgs {
12
+ /** The experiment identifier — typically the URL path, e.g. "/en/about" */
13
+ experimentId: string;
14
+ /** The assigned variant bucket, e.g. "a", "b" */
15
+ variantBucket: string;
16
+ visitorId: string;
17
+ /** Identifies what conversion goal was achieved, e.g. "cta_click", "purchase" */
18
+ goalId: string;
19
+ /** Optional numeric value for statistics */
20
+ goalValue?: number;
21
+ locale?: string;
22
+ metadata?: TrackMetadata;
23
+ }
24
+ interface DateRange {
25
+ /** 'NdaysAgo' or 'YYYY-MM-DD' */
26
+ startDate: string;
27
+ /** 'NdaysAgo', 'today', or 'YYYY-MM-DD' */
28
+ endDate: string;
29
+ }
30
+ interface VariantStats {
31
+ bucket: string;
32
+ impressions: number;
33
+ /** Fraction of total impressions (0–1) */
34
+ impressionShare: number;
35
+ conversions: number;
36
+ /** conversions / impressions (0–1), 0 when impressions = 0 */
37
+ conversionRate: number;
38
+ }
39
+ interface ExperimentStats {
40
+ experimentId: string;
41
+ dateRange: DateRange;
42
+ variants: VariantStats[];
43
+ totals: {
44
+ impressions: number;
45
+ conversions: number;
46
+ };
47
+ }
48
+ interface AnalyticsAdapter {
49
+ /**
50
+ * Fire when a user is assigned to and shown a variant.
51
+ * Called client-side from ExperimentTracker.
52
+ */
53
+ trackImpression(args: TrackImpressionArgs): void;
54
+ /**
55
+ * Fire when a user completes a conversion goal.
56
+ * Called client-side from useABConversion.
57
+ */
58
+ trackConversion(args: TrackConversionArgs): void;
59
+ /**
60
+ * Optional: fire an impression server-side (RSC / Server Action / middleware).
61
+ * Implemented via GA4 Measurement Protocol when apiSecret is provided.
62
+ */
63
+ trackImpressionServer?(args: TrackImpressionArgs): Promise<void>;
64
+ /**
65
+ * Optional: fetch aggregated stats for an experiment.
66
+ * Powers the ExperimentStatsWidget admin component.
67
+ * Requires propertyId and getAccessToken in the adapter config.
68
+ */
69
+ getStats?(experimentId: string, dateRange?: DateRange): Promise<ExperimentStats>;
70
+ }
71
+
72
+ export type { AnalyticsAdapter as A, DateRange as D, ExperimentStats as E, TrackConversionArgs as T, VariantStats as V, TrackImpressionArgs as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kiryl.pekarski/payload-plugin-ab",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "A/B testing plugin for Payload CMS",
5
5
  "keywords": [
6
6
  "payload-cms",
@@ -45,6 +45,9 @@
45
45
  ],
46
46
  "analytics/adapters/google-analytics": [
47
47
  "./dist/analytics/adapters/googleAnalytics/index.d.ts"
48
+ ],
49
+ "middleware": [
50
+ "./dist/middleware/index.d.ts"
48
51
  ]
49
52
  }
50
53
  },
@@ -72,13 +75,21 @@
72
75
  "./analytics/adapters/google-analytics": {
73
76
  "import": "./dist/analytics/adapters/googleAnalytics/index.js",
74
77
  "types": "./dist/analytics/adapters/googleAnalytics/index.d.ts"
78
+ },
79
+ "./middleware": {
80
+ "import": "./dist/middleware/index.js",
81
+ "types": "./dist/middleware/index.d.ts"
75
82
  }
76
83
  },
77
84
  "peerDependencies": {
85
+ "next": "^14.0.0 || ^15.0.0",
78
86
  "payload": "^3.0.0",
79
87
  "react": "^18.0.0 || ^19.0.0"
80
88
  },
81
89
  "peerDependenciesMeta": {
90
+ "next": {
91
+ "optional": true
92
+ },
82
93
  "react": {
83
94
  "optional": true
84
95
  }
@@ -91,7 +102,8 @@
91
102
  "@types/react": "^19.0.0",
92
103
  "eslint": "^9.0.0",
93
104
  "eslint-config-prettier": "^9.0.0",
94
- "payload": "^3.73.0",
105
+ "next": "15.4.10",
106
+ "payload": "3.73.0",
95
107
  "prettier": "^3.0.0",
96
108
  "tsup": "^8.0.0",
97
109
  "typescript": "^5.0.0",