@focus-reactive/payload-plugin-ab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ import { S as StorageAdapter } from '../../config-Bq-Mi7k_.js';
2
+ import 'payload';
3
+
4
+ interface PayloadGlobalAdapterConfig {
5
+ /** Slug for the Payload Global that stores the manifest. Default: '_abManifest' */
6
+ globalSlug?: string;
7
+ /** Server URL used to fetch the global via REST in middleware. Example: 'https://example.com' */
8
+ serverURL?: string;
9
+ /** Payload API route prefix. Default: '/api' */
10
+ apiRoute?: string;
11
+ }
12
+
13
+ declare function payloadGlobalAdapter<TVariantData extends object>(config?: PayloadGlobalAdapterConfig): StorageAdapter<TVariantData>;
14
+
15
+ export { payloadGlobalAdapter };
@@ -0,0 +1,82 @@
1
+ // src/adapters/payloadGlobal/api/fetchManifest.ts
2
+ async function fetchManifest(serverURL, apiRoute, slug, path) {
3
+ try {
4
+ const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {
5
+ cache: "no-store"
6
+ });
7
+ if (!res.ok) return null;
8
+ const data = await res.json();
9
+ return data?.manifest?.[path] ?? null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ // src/adapters/payloadGlobal/api/readManifest.ts
16
+ async function readManifest(payload, slug) {
17
+ try {
18
+ const doc = await payload.findGlobal({ slug, overrideAccess: true });
19
+ return doc?.manifest ?? {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ // src/adapters/payloadGlobal/utils/createGlobal.ts
26
+ function createGlobal(slug, debug) {
27
+ return {
28
+ slug,
29
+ access: {
30
+ read: () => true
31
+ },
32
+ admin: {
33
+ hidden: !debug,
34
+ group: "System"
35
+ },
36
+ fields: [
37
+ {
38
+ name: "manifest",
39
+ type: "json",
40
+ admin: {
41
+ description: "A/B testing manifest. Managed automatically \u2014 do not edit manually."
42
+ }
43
+ }
44
+ ]
45
+ };
46
+ }
47
+
48
+ // src/adapters/payloadGlobal/index.ts
49
+ function payloadGlobalAdapter(config) {
50
+ const slug = config?.globalSlug ?? "_abManifest";
51
+ return {
52
+ async write(path, variants, payload) {
53
+ const currentManifest = await readManifest(payload, slug);
54
+ await payload.updateGlobal({
55
+ slug,
56
+ data: { manifest: { ...currentManifest, [path]: variants } },
57
+ overrideAccess: true
58
+ });
59
+ },
60
+ async read(path) {
61
+ const serverURL = config?.serverURL ?? "";
62
+ const apiRoute = config?.apiRoute ?? "/api";
63
+ return fetchManifest(serverURL, apiRoute, slug, path);
64
+ },
65
+ async clear(path, payload) {
66
+ const currentManifest = await readManifest(payload, slug);
67
+ delete currentManifest[path];
68
+ await payload.updateGlobal({
69
+ slug,
70
+ data: { manifest: currentManifest },
71
+ overrideAccess: true
72
+ });
73
+ },
74
+ createGlobal(debug = false) {
75
+ return createGlobal(slug, debug);
76
+ }
77
+ };
78
+ }
79
+ export {
80
+ payloadGlobalAdapter
81
+ };
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/payloadGlobal/api/fetchManifest.ts","../../../src/adapters/payloadGlobal/api/readManifest.ts","../../../src/adapters/payloadGlobal/utils/createGlobal.ts","../../../src/adapters/payloadGlobal/index.ts"],"sourcesContent":["export async function fetchManifest<TVariantData extends object>(\n serverURL: string,\n apiRoute: string,\n slug: string,\n path: string,\n): Promise<TVariantData[] | null> {\n try {\n const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {\n cache: \"no-store\",\n });\n\n if (!res.ok) return null;\n\n const data = await res.json();\n\n return (data?.manifest?.[path] as TVariantData[]) ?? null;\n } catch {\n return null;\n }\n}\n","import type { GlobalSlug, Payload } from \"payload\";\nimport type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest(payload: Payload, slug: string): Promise<Manifest> {\n try {\n const doc = await payload.findGlobal({ slug: slug as GlobalSlug, overrideAccess: true });\n\n return (doc?.manifest as Manifest) ?? {};\n } catch {\n return {};\n }\n}\n","import type { GlobalConfig } from \"payload\";\n\nexport function createGlobal(slug: string, debug: boolean): GlobalConfig {\n return {\n slug,\n access: {\n read: () => true,\n },\n admin: {\n hidden: !debug,\n group: \"System\",\n },\n fields: [\n {\n name: \"manifest\",\n type: \"json\",\n admin: {\n description: \"A/B testing manifest. Managed automatically — do not edit manually.\",\n },\n },\n ],\n };\n}\n","import type { GlobalSlug } from \"payload\";\nimport type { StorageAdapter } from \"../../types/config\";\nimport type { PayloadGlobalAdapterConfig } from \"./config\";\nimport { fetchManifest } from \"./api/fetchManifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { createGlobal } from \"./utils/createGlobal\";\n\nexport function payloadGlobalAdapter<TVariantData extends object>(\n config?: PayloadGlobalAdapterConfig,\n): StorageAdapter<TVariantData> {\n const slug = config?.globalSlug ?? \"_abManifest\";\n\n return {\n async write(path, variants, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: { ...currentManifest, [path]: variants } },\n overrideAccess: true,\n });\n },\n\n async read(path) {\n const serverURL = config?.serverURL ?? \"\";\n const apiRoute = config?.apiRoute ?? \"/api\";\n\n return fetchManifest<TVariantData>(serverURL, apiRoute, slug, path);\n },\n\n async clear(path, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n delete currentManifest[path];\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: currentManifest },\n overrideAccess: true,\n });\n },\n\n createGlobal(debug = false) {\n return createGlobal(slug, debug);\n },\n };\n}\n"],"mappings":";AAAA,eAAsB,cACpB,WACA,UACA,MACA,MACgC;AAChC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,YAAY,IAAI,IAAI;AAAA,MACjE,OAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,IAAI,GAAI,QAAO;AAEpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,WAAQ,MAAM,WAAW,IAAI,KAAwB;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AChBA,eAAsB,aAAa,SAAkB,MAAiC;AACpF,MAAI;AACF,UAAM,MAAM,MAAM,QAAQ,WAAW,EAAE,MAA0B,gBAAgB,KAAK,CAAC;AAEvF,WAAQ,KAAK,YAAyB,CAAC;AAAA,EACzC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACTO,SAAS,aAAa,MAAc,OAA8B;AACvE,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,MAAM;AAAA,IACd;AAAA,IACA,OAAO;AAAA,MACL,QAAQ,CAAC;AAAA,MACT,OAAO;AAAA,IACT;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,UACL,aAAa;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACfO,SAAS,qBACd,QAC8B;AAC9B,QAAM,OAAO,QAAQ,cAAc;AAEnC,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,UAAU,SAAS;AACnC,YAAM,kBAAkB,MAAM,aAAa,SAAS,IAAI;AAExD,YAAM,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA,MAAM,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAAC,IAAI,GAAG,SAAS,EAAE;AAAA,QAC3D,gBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,KAAK,MAAM;AACf,YAAM,YAAY,QAAQ,aAAa;AACvC,YAAM,WAAW,QAAQ,YAAY;AAErC,aAAO,cAA4B,WAAW,UAAU,MAAM,IAAI;AAAA,IACpE;AAAA,IAEA,MAAM,MAAM,MAAM,SAAS;AACzB,YAAM,kBAAkB,MAAM,aAAa,SAAS,IAAI;AAExD,aAAO,gBAAgB,IAAI;AAE3B,YAAM,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA,MAAM,EAAE,UAAU,gBAAgB;AAAA,QAClC,gBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,IAEA,aAAa,QAAQ,OAAO;AAC1B,aAAO,aAAa,MAAM,KAAK;AAAA,IACjC;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ import { S as StorageAdapter } from '../../config-Bq-Mi7k_.js';
2
+ import 'payload';
3
+
4
+ interface VercelEdgeAdapterConfig {
5
+ configID: string;
6
+ configURL: string;
7
+ vercelRestAPIAccessToken: string;
8
+ teamID?: string;
9
+ /** Top-level key in Edge Config that holds the manifest. Default: 'ab-testing' */
10
+ manifestKey?: string;
11
+ }
12
+
13
+ /**
14
+ * Vercel Edge Config adapter.
15
+ * Requires "pnpm add \@vercel/edge-config" and the following env vars:
16
+ * EDGE_CONFIG, EDGE_CONFIG_ID, VERCEL_REST_API_ACCESS_TOKEN
17
+ */
18
+ declare function vercelEdgeAdapter<TVariantData extends object>(config: VercelEdgeAdapterConfig): StorageAdapter<TVariantData>;
19
+
20
+ export { vercelEdgeAdapter };
@@ -0,0 +1,66 @@
1
+ // src/adapters/vercelEdge/api/readManifest.ts
2
+ async function readManifest(manifestKey) {
3
+ try {
4
+ const { get } = await import("@vercel/edge-config");
5
+ const manifest = await get(manifestKey);
6
+ return manifest ?? {};
7
+ } catch {
8
+ return {};
9
+ }
10
+ }
11
+
12
+ // src/adapters/vercelEdge/utils/buildHeaders.ts
13
+ function buildHeaders(config) {
14
+ return {
15
+ "Content-Type": "application/json",
16
+ Authorization: `Bearer ${config.vercelRestAPIAccessToken}`
17
+ };
18
+ }
19
+
20
+ // src/adapters/vercelEdge/api/updateEdgeConfig.ts
21
+ async function updateEdgeConfig(config, manifestKey, value) {
22
+ const teamQuery = config.teamID ? `?teamId=${config.teamID}` : "";
23
+ await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {
24
+ method: "PATCH",
25
+ headers: buildHeaders(config),
26
+ body: JSON.stringify({ items: [{ operation: "upsert", key: manifestKey, value }] })
27
+ });
28
+ }
29
+
30
+ // src/adapters/vercelEdge/index.ts
31
+ function vercelEdgeAdapter(config) {
32
+ const manifestKey = config.manifestKey ?? "ab-testing";
33
+ let localCache = null;
34
+ async function getManifest() {
35
+ if (localCache === null) {
36
+ localCache = await readManifest(manifestKey);
37
+ }
38
+ return localCache;
39
+ }
40
+ return {
41
+ async write(path, variants) {
42
+ const currentManifest = await getManifest();
43
+ localCache = { ...currentManifest, [path]: variants };
44
+ await updateEdgeConfig(config, manifestKey, localCache);
45
+ },
46
+ async read(path) {
47
+ try {
48
+ const manifest = await readManifest(manifestKey);
49
+ return manifest?.[path] ?? null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ },
54
+ async clear(path) {
55
+ const currentManifest = await getManifest();
56
+ const updated = { ...currentManifest };
57
+ delete updated[path];
58
+ localCache = updated;
59
+ await updateEdgeConfig(config, manifestKey, localCache);
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ vercelEdgeAdapter
65
+ };
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/vercelEdge/api/readManifest.ts","../../../src/adapters/vercelEdge/utils/buildHeaders.ts","../../../src/adapters/vercelEdge/api/updateEdgeConfig.ts","../../../src/adapters/vercelEdge/index.ts"],"sourcesContent":["import type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest<TVariantData extends object = object>(\n manifestKey: string,\n): Promise<Manifest<TVariantData>> {\n try {\n const { get } = await import(\"@vercel/edge-config\");\n\n const manifest = await get<Manifest<TVariantData>>(manifestKey);\n\n return manifest ?? {};\n } catch {\n return {};\n }\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\n\nexport function buildHeaders(config: VercelEdgeAdapterConfig): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${config.vercelRestAPIAccessToken}`,\n };\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\nimport { buildHeaders } from \"../utils/buildHeaders\";\n\nexport async function updateEdgeConfig(config: VercelEdgeAdapterConfig, manifestKey: string, value: unknown) {\n const teamQuery = config.teamID ? `?teamId=${config.teamID}` : \"\";\n\n await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {\n method: \"PATCH\",\n headers: buildHeaders(config),\n body: JSON.stringify({ items: [{ operation: \"upsert\", key: manifestKey, value }] }),\n });\n}\n","import type { StorageAdapter } from \"../../types/config\";\nimport type { Manifest } from \"../../types/manifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { updateEdgeConfig } from \"./api/updateEdgeConfig\";\nimport type { VercelEdgeAdapterConfig } from \"./config\";\n\n/**\n * Vercel Edge Config adapter.\n * Requires \"pnpm add \\@vercel/edge-config\" and the following env vars:\n * EDGE_CONFIG, EDGE_CONFIG_ID, VERCEL_REST_API_ACCESS_TOKEN\n */\nexport function vercelEdgeAdapter<TVariantData extends object>(\n config: VercelEdgeAdapterConfig,\n): StorageAdapter<TVariantData> {\n const manifestKey = config.manifestKey ?? \"ab-testing\";\n let localCache: Manifest<TVariantData> | null = null;\n\n async function getManifest() {\n if (localCache === null) {\n localCache = await readManifest<TVariantData>(manifestKey);\n }\n\n return localCache;\n }\n\n return {\n async write(path, variants) {\n const currentManifest = await getManifest();\n\n localCache = { ...currentManifest, [path]: variants };\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n\n async read(path) {\n try {\n const manifest = await readManifest<TVariantData>(manifestKey);\n\n return manifest?.[path] ?? null;\n } catch {\n return null;\n }\n },\n\n async clear(path) {\n const currentManifest = await getManifest();\n const updated = { ...currentManifest };\n\n delete updated[path];\n\n localCache = updated;\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n };\n}\n"],"mappings":";AAEA,eAAsB,aACpB,aACiC;AACjC,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,OAAO,qBAAqB;AAElD,UAAM,WAAW,MAAM,IAA4B,WAAW;AAE9D,WAAO,YAAY,CAAC;AAAA,EACtB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACZO,SAAS,aAAa,QAAyD;AACpF,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,eAAe,UAAU,OAAO,wBAAwB;AAAA,EAC1D;AACF;;;ACJA,eAAsB,iBAAiB,QAAiC,aAAqB,OAAgB;AAC3G,QAAM,YAAY,OAAO,SAAS,WAAW,OAAO,MAAM,KAAK;AAE/D,QAAM,MAAM,yCAAyC,OAAO,QAAQ,SAAS,SAAS,IAAI;AAAA,IACxF,QAAQ;AAAA,IACR,SAAS,aAAa,MAAM;AAAA,IAC5B,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,WAAW,UAAU,KAAK,aAAa,MAAM,CAAC,EAAE,CAAC;AAAA,EACpF,CAAC;AACH;;;ACAO,SAAS,kBACd,QAC8B;AAC9B,QAAM,cAAc,OAAO,eAAe;AAC1C,MAAI,aAA4C;AAEhD,iBAAe,cAAc;AAC3B,QAAI,eAAe,MAAM;AACvB,mBAAa,MAAM,aAA2B,WAAW;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,UAAU;AAC1B,YAAM,kBAAkB,MAAM,YAAY;AAE1C,mBAAa,EAAE,GAAG,iBAAiB,CAAC,IAAI,GAAG,SAAS;AAEpD,YAAM,iBAAiB,QAAQ,aAAa,UAAU;AAAA,IACxD;AAAA,IAEA,MAAM,KAAK,MAAM;AACf,UAAI;AACF,cAAM,WAAW,MAAM,aAA2B,WAAW;AAE7D,eAAO,WAAW,IAAI,KAAK;AAAA,MAC7B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,MAAM;AAChB,YAAM,kBAAkB,MAAM,YAAY;AAC1C,YAAM,UAAU,EAAE,GAAG,gBAAgB;AAErC,aAAO,QAAQ,IAAI;AAEnB,mBAAa;AAEb,YAAM,iBAAiB,QAAQ,aAAa,UAAU;AAAA,IACxD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,30 @@
1
+ import { A as AnalyticsAdapter } from '../../../types-OJFBnrUD.js';
2
+
3
+ interface GoogleAnalyticsAdapterConfig {
4
+ /** GA4 Measurement ID, e.g. "G-XXXXXXXXXX" */
5
+ measurementId: string;
6
+ /** GA4 Measurement Protocol API secret — enables trackImpressionServer() */
7
+ apiSecret?: string;
8
+ /**
9
+ * GA4 Property resource name for the Data API — enables getStats().
10
+ * Format: "properties/XXXXXXXXX"
11
+ */
12
+ propertyId?: string;
13
+ /**
14
+ * Returns a valid OAuth2 access token for the GA4 Data API.
15
+ * Required for getStats(). Scopes needed: analytics.readonly.
16
+ * @example
17
+ * import { GoogleAuth } from 'google-auth-library'
18
+ * const auth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/analytics.readonly'] })
19
+ * getAccessToken: () => auth.getAccessToken()
20
+ */
21
+ getAccessToken?: () => Promise<string>;
22
+ /** Custom event name for impressions. Default: 'ab_impression' */
23
+ impressionEventName?: string;
24
+ /** Custom event name for conversions. Default: 'ab_conversion' */
25
+ conversionEventName?: string;
26
+ }
27
+
28
+ declare function googleAnalyticsAdapter(config: GoogleAnalyticsAdapterConfig): AnalyticsAdapter;
29
+
30
+ export { type GoogleAnalyticsAdapterConfig, googleAnalyticsAdapter };
@@ -0,0 +1,188 @@
1
+ // src/analytics/adapters/googleAnalytics/constants.ts
2
+ var DEFAULT_IMPRESSION_EVENT_NAME = "ab_impression";
3
+ var DEFAULT_CONVERSION_EVENT_NAME = "ab_conversion";
4
+ var MEASUREMENT_PROTOCOL_URL = "https://www.google-analytics.com/mp/collect";
5
+ var DATA_API_BASE = "https://analyticsdata.googleapis.com/v1beta";
6
+
7
+ // src/analytics/adapters/googleAnalytics/utils/canUseGtag.ts
8
+ var canUseGtag = (window2) => {
9
+ return typeof window2 !== "undefined" && typeof window2.gtag === "function";
10
+ };
11
+
12
+ // src/analytics/adapters/googleAnalytics/utils/waitForGtag.ts
13
+ function waitForGtag(callback, options = {}) {
14
+ const { interval = 50, timeout = 5e3 } = options;
15
+ if (canUseGtag(window)) {
16
+ callback(window.gtag);
17
+ return;
18
+ }
19
+ const start = Date.now();
20
+ const id = setInterval(() => {
21
+ if (canUseGtag(window)) {
22
+ clearInterval(id);
23
+ callback(window.gtag);
24
+ } else if (Date.now() - start >= timeout) {
25
+ clearInterval(id);
26
+ }
27
+ }, interval);
28
+ }
29
+
30
+ // src/analytics/adapters/googleAnalytics/client.ts
31
+ function trackImpressionClient(config, { experimentId, variantBucket, visitorId, locale, metadata }) {
32
+ waitForGtag((gtag) => {
33
+ gtag("event", config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME, {
34
+ experiment_id: experimentId,
35
+ variant_bucket: variantBucket,
36
+ visitor_id: visitorId,
37
+ ...locale !== void 0 && { locale },
38
+ ...metadata
39
+ });
40
+ });
41
+ }
42
+ function trackConversionClient(config, { experimentId, goalId, variantBucket, visitorId, goalValue, locale, metadata }) {
43
+ if (!canUseGtag(window)) {
44
+ return;
45
+ }
46
+ window.gtag("event", config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME, {
47
+ experiment_id: experimentId,
48
+ variant_bucket: variantBucket,
49
+ visitor_id: visitorId,
50
+ goal_id: goalId,
51
+ ...goalValue !== void 0 && { value: goalValue },
52
+ ...locale !== void 0 && { locale },
53
+ ...metadata
54
+ });
55
+ }
56
+
57
+ // src/analytics/adapters/googleAnalytics/server.ts
58
+ async function trackImpressionServer(config, { experimentId, variantBucket, visitorId, locale, metadata }) {
59
+ if (!config.apiSecret) return;
60
+ const url = `${MEASUREMENT_PROTOCOL_URL}?measurement_id=${config.measurementId}&api_secret=${config.apiSecret}`;
61
+ await fetch(url, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({
65
+ client_id: visitorId,
66
+ events: [
67
+ {
68
+ name: config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME,
69
+ params: {
70
+ experiment_id: experimentId,
71
+ variant_bucket: variantBucket,
72
+ visitor_id: visitorId,
73
+ engagement_time_msec: 1,
74
+ ...locale !== void 0 && { locale },
75
+ ...metadata
76
+ }
77
+ }
78
+ ]
79
+ })
80
+ });
81
+ }
82
+
83
+ // src/analytics/adapters/googleAnalytics/stats.ts
84
+ function parseReport(report) {
85
+ const result = /* @__PURE__ */ new Map();
86
+ if (!report) return result;
87
+ for (const row of report.rows ?? []) {
88
+ const bucket = row.dimensionValues[0]?.value;
89
+ const raw = row.metricValues[0]?.value;
90
+ if (bucket != null && raw != null) {
91
+ result.set(bucket, parseInt(raw, 10));
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ async function getExperimentStats(config, experimentId, dateRange = { startDate: "30daysAgo", endDate: "today" }) {
97
+ if (!config.propertyId || !config.getAccessToken) {
98
+ throw new Error(
99
+ "payload-plugin-ab: getStats() requires propertyId and getAccessToken to be set in GoogleAnalyticsAdapterConfig."
100
+ );
101
+ }
102
+ const accessToken = await config.getAccessToken();
103
+ const url = `${DATA_API_BASE}/${config.propertyId}:batchRunReports`;
104
+ const makeReport = (eventName) => ({
105
+ dimensions: [{ name: "customEvent:variant_bucket" }],
106
+ metrics: [{ name: "eventCount" }],
107
+ dimensionFilter: {
108
+ andGroup: {
109
+ expressions: [
110
+ {
111
+ filter: {
112
+ fieldName: "eventName",
113
+ stringFilter: { matchType: "EXACT", value: eventName }
114
+ }
115
+ },
116
+ {
117
+ filter: {
118
+ fieldName: "customEvent:experiment_id",
119
+ stringFilter: { matchType: "EXACT", value: experimentId }
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ },
125
+ dateRanges: [dateRange]
126
+ });
127
+ const res = await fetch(url, {
128
+ method: "POST",
129
+ headers: {
130
+ Authorization: `Bearer ${accessToken}`,
131
+ "Content-Type": "application/json"
132
+ },
133
+ body: JSON.stringify({
134
+ requests: [
135
+ makeReport(config.impressionEventName ?? DEFAULT_IMPRESSION_EVENT_NAME),
136
+ makeReport(config.conversionEventName ?? DEFAULT_CONVERSION_EVENT_NAME)
137
+ ]
138
+ })
139
+ });
140
+ if (!res.ok) {
141
+ const body = await res.text();
142
+ throw new Error(`payload-plugin-ab: GA4 Data API responded with ${res.status}: ${body}`);
143
+ }
144
+ const data = await res.json();
145
+ const impressionMap = parseReport(data.reports[0]);
146
+ const conversionMap = parseReport(data.reports[1]);
147
+ const allBuckets = /* @__PURE__ */ new Set([...impressionMap.keys(), ...conversionMap.keys()]);
148
+ const totalImpressions = [...impressionMap.values()].reduce((acc, n) => acc + n, 0);
149
+ const totalConversions = [...conversionMap.values()].reduce((acc, n) => acc + n, 0);
150
+ const variants = [...allBuckets].map((bucket) => {
151
+ const impressions = impressionMap.get(bucket) ?? 0;
152
+ const conversions = conversionMap.get(bucket) ?? 0;
153
+ return {
154
+ bucket,
155
+ impressions,
156
+ impressionShare: totalImpressions > 0 ? impressions / totalImpressions : 0,
157
+ conversions,
158
+ conversionRate: impressions > 0 ? conversions / impressions : 0
159
+ };
160
+ });
161
+ return {
162
+ experimentId,
163
+ dateRange,
164
+ variants,
165
+ totals: {
166
+ impressions: totalImpressions,
167
+ conversions: totalConversions
168
+ }
169
+ };
170
+ }
171
+
172
+ // src/analytics/adapters/googleAnalytics/index.ts
173
+ function googleAnalyticsAdapter(config) {
174
+ return {
175
+ trackImpression: (args) => trackImpressionClient(config, args),
176
+ trackConversion: (args) => trackConversionClient(config, args),
177
+ ...config.apiSecret != null && {
178
+ trackImpressionServer: (args) => trackImpressionServer(config, args)
179
+ },
180
+ ...config.propertyId != null && config.getAccessToken != null && {
181
+ getStats: (experimentId, dateRange) => getExperimentStats(config, experimentId, dateRange)
182
+ }
183
+ };
184
+ }
185
+ export {
186
+ googleAnalyticsAdapter
187
+ };
188
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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\";\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"]}
@@ -0,0 +1,48 @@
1
+ export { A as AbCookieConfig, R as ResolvedAbCookieNames, r as resolveAbCookieNames } from '../resolveAbCookieNames-DH8evjWm.js';
2
+ import { A as AnalyticsAdapter } from '../types-OJFBnrUD.js';
3
+
4
+ interface ExperimentTrackerProps {
5
+ experimentId: string;
6
+ /**
7
+ * Name of the cookie that holds the assigned variant bucket.
8
+ * Use `resolveAbCookieNames(abCookies, experimentId).variantCookieName` to derive
9
+ * this from a shared `AbCookieConfig`. Default: `exp_${experimentId}`.
10
+ */
11
+ variantCookieName?: string;
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
+ */
17
+ visitorCookieName?: string;
18
+ }
19
+ declare function ExperimentTracker({ experimentId, variantCookieName, visitorCookieName, }: ExperimentTrackerProps): null;
20
+
21
+ interface UseABConversionOptions {
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
+ */
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
+ */
34
+ visitorCookieName?: string;
35
+ }
36
+ type TrackConversionFn = (args: {
37
+ goalId: string;
38
+ goalValue?: number;
39
+ }) => void;
40
+ declare function useABConversion({ experimentId, variantCookieName, visitorCookieName, }: UseABConversionOptions): TrackConversionFn;
41
+
42
+ declare function useABAnalytics(): AnalyticsAdapter | null;
43
+ declare function ABAnalyticsProvider({ adapter, children, }: {
44
+ adapter: AnalyticsAdapter;
45
+ children: React.ReactNode;
46
+ }): React.ReactNode;
47
+
48
+ export { ABAnalyticsProvider, ExperimentTracker, type ExperimentTrackerProps, type TrackConversionFn, type UseABConversionOptions, useABAnalytics, useABConversion };
@@ -0,0 +1,102 @@
1
+ "use client";
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
+
19
+ // src/analytics/components/ABAnalyticsProvider.tsx
20
+ import { createContext, useContext } from "react";
21
+ import { jsx } from "react/jsx-runtime";
22
+ var ABAnalyticsContext = createContext(null);
23
+ function useABAnalytics() {
24
+ return useContext(ABAnalyticsContext);
25
+ }
26
+ function ABAnalyticsProvider({
27
+ adapter,
28
+ children
29
+ }) {
30
+ return /* @__PURE__ */ jsx(ABAnalyticsContext.Provider, { value: adapter, children });
31
+ }
32
+
33
+ // src/analytics/components/ExperimentTracker.tsx
34
+ import { useEffect } from "react";
35
+
36
+ // src/analytics/utils/getCookie.ts
37
+ function getCookie(name) {
38
+ if (typeof document === "undefined") return null;
39
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
41
+ if (!match) return null;
42
+ const value = match[1];
43
+ return value != null ? decodeURIComponent(value) : null;
44
+ }
45
+
46
+ // src/analytics/components/ExperimentTracker.tsx
47
+ function ExperimentTracker({
48
+ experimentId,
49
+ variantCookieName,
50
+ visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME
51
+ }) {
52
+ const adapter = useABAnalytics();
53
+ useEffect(() => {
54
+ if (!adapter) return;
55
+ const sessionKey = `ab_tracked_${experimentId}`;
56
+ if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(sessionKey)) {
57
+ return;
58
+ }
59
+ const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);
60
+ const variantBucket = getCookie(resolvedVariantCookie);
61
+ const visitorId = getCookie(visitorCookieName);
62
+ if (!variantBucket || !visitorId) return;
63
+ adapter.trackImpression({ experimentId, variantBucket, visitorId });
64
+ sessionStorage.setItem(sessionKey, "1");
65
+ }, [adapter, experimentId, variantCookieName, visitorCookieName]);
66
+ return null;
67
+ }
68
+
69
+ // src/analytics/hooks/useABConversion.ts
70
+ import { useCallback } from "react";
71
+ function useABConversion({
72
+ experimentId,
73
+ variantCookieName,
74
+ visitorCookieName = DEFAULT_VISITOR_ID_COOKIE_NAME
75
+ }) {
76
+ const adapter = useABAnalytics();
77
+ return useCallback(
78
+ ({ goalId, goalValue }) => {
79
+ if (!adapter) return;
80
+ const resolvedVariantCookie = variantCookieName ?? defaultGetExpCookieName(experimentId);
81
+ const variantBucket = getCookie(resolvedVariantCookie);
82
+ const visitorId = getCookie(visitorCookieName);
83
+ if (!variantBucket || !visitorId) return;
84
+ adapter.trackConversion({
85
+ experimentId,
86
+ variantBucket,
87
+ visitorId,
88
+ goalId,
89
+ goalValue
90
+ });
91
+ },
92
+ [adapter, experimentId, variantCookieName, visitorCookieName]
93
+ );
94
+ }
95
+ export {
96
+ ABAnalyticsProvider,
97
+ ExperimentTracker,
98
+ resolveAbCookieNames,
99
+ useABAnalytics,
100
+ useABConversion
101
+ };
102
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
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":[]}
@@ -0,0 +1,2 @@
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';
@@ -0,0 +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
+ };
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
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":[]}