@kiryl.pekarski/payload-plugin-ab 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/adapters/googleAnalytics/index.d.ts +30 -0
- package/dist/analytics/adapters/googleAnalytics/index.js +188 -0
- package/dist/analytics/adapters/googleAnalytics/index.js.map +1 -0
- package/dist/analytics/client.d.ts +32 -0
- package/dist/analytics/client.js +88 -0
- package/dist/analytics/client.js.map +1 -0
- package/dist/analytics/index.d.ts +72 -0
- package/dist/analytics/index.js +1 -0
- package/dist/analytics/index.js.map +1 -0
- package/package.json +30 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AnalyticsAdapter } from '../../index.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\";\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":"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","names":["window"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AnalyticsAdapter } from './index.js';
|
|
2
|
+
|
|
3
|
+
interface ExperimentTrackerProps {
|
|
4
|
+
experimentId: string;
|
|
5
|
+
/**
|
|
6
|
+
* Name of the cookie that holds the assigned variant bucket.
|
|
7
|
+
* Default: `exp_${experimentId}`
|
|
8
|
+
*/
|
|
9
|
+
variantCookieName?: string;
|
|
10
|
+
/** Default: 'ab_visitor_id' */
|
|
11
|
+
visitorCookieName?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function ExperimentTracker({ experimentId, variantCookieName, visitorCookieName, }: ExperimentTrackerProps): null;
|
|
14
|
+
|
|
15
|
+
interface UseABConversionOptions {
|
|
16
|
+
experimentId: string;
|
|
17
|
+
variantCookieName?: string;
|
|
18
|
+
visitorCookieName?: string;
|
|
19
|
+
}
|
|
20
|
+
type TrackConversionFn = (args: {
|
|
21
|
+
goalId: string;
|
|
22
|
+
goalValue?: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
declare function useABConversion({ experimentId, variantCookieName, visitorCookieName, }: UseABConversionOptions): TrackConversionFn;
|
|
25
|
+
|
|
26
|
+
declare function useABAnalytics(): AnalyticsAdapter | null;
|
|
27
|
+
declare function ABAnalyticsProvider({ adapter, children, }: {
|
|
28
|
+
adapter: AnalyticsAdapter;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}): React.ReactNode;
|
|
31
|
+
|
|
32
|
+
export { ABAnalyticsProvider, ExperimentTracker, type ExperimentTrackerProps, type TrackConversionFn, type UseABConversionOptions, useABAnalytics, useABConversion };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/analytics/components/ABAnalyticsProvider.tsx
|
|
4
|
+
import { createContext, useContext } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
var ABAnalyticsContext = createContext(null);
|
|
7
|
+
function useABAnalytics() {
|
|
8
|
+
return useContext(ABAnalyticsContext);
|
|
9
|
+
}
|
|
10
|
+
function ABAnalyticsProvider({
|
|
11
|
+
adapter,
|
|
12
|
+
children
|
|
13
|
+
}) {
|
|
14
|
+
return /* @__PURE__ */ jsx(ABAnalyticsContext.Provider, { value: adapter, children });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/analytics/components/ExperimentTracker.tsx
|
|
18
|
+
import { useEffect } from "react";
|
|
19
|
+
|
|
20
|
+
// src/analytics/utils/getCookie.ts
|
|
21
|
+
function getCookie(name) {
|
|
22
|
+
if (typeof document === "undefined") return null;
|
|
23
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
const value = match[1];
|
|
27
|
+
return value != null ? decodeURIComponent(value) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/analytics/constants.ts
|
|
31
|
+
var DEFAULT_A_B_VISITOR_ID = "ab_visitor_id";
|
|
32
|
+
|
|
33
|
+
// src/analytics/components/ExperimentTracker.tsx
|
|
34
|
+
function ExperimentTracker({
|
|
35
|
+
experimentId,
|
|
36
|
+
variantCookieName,
|
|
37
|
+
visitorCookieName = DEFAULT_A_B_VISITOR_ID
|
|
38
|
+
}) {
|
|
39
|
+
const adapter = useABAnalytics();
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!adapter) return;
|
|
42
|
+
const sessionKey = `ab_tracked_${experimentId}`;
|
|
43
|
+
if (typeof sessionStorage !== "undefined" && sessionStorage.getItem(sessionKey)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const resolvedVariantCookie = variantCookieName ?? `exp_${experimentId}`;
|
|
47
|
+
const variantBucket = getCookie(resolvedVariantCookie);
|
|
48
|
+
const visitorId = getCookie(visitorCookieName);
|
|
49
|
+
if (!variantBucket || !visitorId) return;
|
|
50
|
+
adapter.trackImpression({ experimentId, variantBucket, visitorId });
|
|
51
|
+
sessionStorage.setItem(sessionKey, "1");
|
|
52
|
+
}, [adapter, experimentId, variantCookieName, visitorCookieName]);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/analytics/hooks/useABConversion.ts
|
|
57
|
+
import { useCallback } from "react";
|
|
58
|
+
function useABConversion({
|
|
59
|
+
experimentId,
|
|
60
|
+
variantCookieName,
|
|
61
|
+
visitorCookieName = DEFAULT_A_B_VISITOR_ID
|
|
62
|
+
}) {
|
|
63
|
+
const adapter = useABAnalytics();
|
|
64
|
+
return useCallback(
|
|
65
|
+
({ goalId, goalValue }) => {
|
|
66
|
+
if (!adapter) return;
|
|
67
|
+
const cookieName = variantCookieName ?? `exp_${experimentId}`;
|
|
68
|
+
const variantBucket = getCookie(cookieName);
|
|
69
|
+
const visitorId = getCookie(visitorCookieName);
|
|
70
|
+
if (!variantBucket || !visitorId) return;
|
|
71
|
+
adapter.trackConversion({
|
|
72
|
+
experimentId,
|
|
73
|
+
variantBucket,
|
|
74
|
+
visitorId,
|
|
75
|
+
goalId,
|
|
76
|
+
goalValue
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
[adapter, experimentId, variantCookieName, visitorCookieName]
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
ABAnalyticsProvider,
|
|
84
|
+
ExperimentTracker,
|
|
85
|
+
useABAnalytics,
|
|
86
|
+
useABConversion
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -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, DateRange, ExperimentStats, TrackConversionArgs, TrackImpressionArgs, VariantStats };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiryl.pekarski/payload-plugin-ab",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A/B testing plugin for Payload CMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload-cms",
|
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
],
|
|
37
37
|
"adapters/vercel-edge": [
|
|
38
38
|
"./dist/adapters/vercelEdge/index.d.ts"
|
|
39
|
+
],
|
|
40
|
+
"analytics": [
|
|
41
|
+
"./dist/analytics/index.d.ts"
|
|
42
|
+
],
|
|
43
|
+
"analytics/client": [
|
|
44
|
+
"./dist/analytics/client.d.ts"
|
|
45
|
+
],
|
|
46
|
+
"analytics/adapters/google-analytics": [
|
|
47
|
+
"./dist/analytics/adapters/googleAnalytics/index.d.ts"
|
|
39
48
|
]
|
|
40
49
|
}
|
|
41
50
|
},
|
|
@@ -51,16 +60,35 @@
|
|
|
51
60
|
"./adapters/vercel-edge": {
|
|
52
61
|
"import": "./dist/adapters/vercelEdge/index.js",
|
|
53
62
|
"types": "./dist/adapters/vercelEdge/index.d.ts"
|
|
63
|
+
},
|
|
64
|
+
"./analytics": {
|
|
65
|
+
"import": "./dist/analytics/index.js",
|
|
66
|
+
"types": "./dist/analytics/index.d.ts"
|
|
67
|
+
},
|
|
68
|
+
"./analytics/client": {
|
|
69
|
+
"import": "./dist/analytics/client.js",
|
|
70
|
+
"types": "./dist/analytics/client.d.ts"
|
|
71
|
+
},
|
|
72
|
+
"./analytics/adapters/google-analytics": {
|
|
73
|
+
"import": "./dist/analytics/adapters/googleAnalytics/index.js",
|
|
74
|
+
"types": "./dist/analytics/adapters/googleAnalytics/index.d.ts"
|
|
54
75
|
}
|
|
55
76
|
},
|
|
56
77
|
"peerDependencies": {
|
|
57
|
-
"payload": "^3.0.0"
|
|
78
|
+
"payload": "^3.0.0",
|
|
79
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
58
85
|
},
|
|
59
86
|
"optionalDependencies": {
|
|
60
87
|
"@vercel/edge-config": "^1.0.0"
|
|
61
88
|
},
|
|
62
89
|
"devDependencies": {
|
|
63
90
|
"@types/node": "^20.0.0",
|
|
91
|
+
"@types/react": "^19.0.0",
|
|
64
92
|
"eslint": "^9.0.0",
|
|
65
93
|
"eslint-config-prettier": "^9.0.0",
|
|
66
94
|
"payload": "^3.73.0",
|