@levi123/experiment 4.0.0-dev.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.
- package/README.md +464 -0
- package/dist/esm/components/experiment-wrapper.d.ts +16 -0
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/constants/index.d.ts +12 -0
- package/dist/esm/core/ab-testing.d.ts +3 -0
- package/dist/esm/core/index.d.ts +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +685 -0
- package/dist/esm/index.mjs +685 -0
- package/dist/esm/tracking/tracker.d.ts +12 -0
- package/dist/esm/types/devices.d.ts +5 -0
- package/dist/esm/types/experiments.d.ts +82 -0
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/esm/utils/cookie.d.ts +21 -0
- package/dist/esm/utils/devices.d.ts +2 -0
- package/dist/esm/utils/experiment-wrapper-helpers.d.ts +21 -0
- package/dist/esm/utils/index.d.ts +6 -0
- package/dist/esm/utils/session.d.ts +1 -0
- package/dist/esm/utils/storage.d.ts +3 -0
- package/dist/esm/utils/user.d.ts +2 -0
- package/dist/umd/components/experiment-wrapper.d.ts +16 -0
- package/dist/umd/components/index.d.ts +1 -0
- package/dist/umd/constants/index.d.ts +12 -0
- package/dist/umd/core/ab-testing.d.ts +3 -0
- package/dist/umd/core/index.d.ts +1 -0
- package/dist/umd/index.d.ts +5 -0
- package/dist/umd/index.js +1 -0
- package/dist/umd/tracking/tracker.d.ts +12 -0
- package/dist/umd/types/devices.d.ts +5 -0
- package/dist/umd/types/experiments.d.ts +82 -0
- package/dist/umd/types/index.d.ts +2 -0
- package/dist/umd/utils/cookie.d.ts +21 -0
- package/dist/umd/utils/devices.d.ts +2 -0
- package/dist/umd/utils/experiment-wrapper-helpers.d.ts +21 -0
- package/dist/umd/utils/index.d.ts +6 -0
- package/dist/umd/utils/session.d.ts +1 -0
- package/dist/umd/utils/storage.d.ts +3 -0
- package/dist/umd/utils/user.d.ts +2 -0
- package/package.json +64 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
const EXPERIMENT_STORAGE_KEYS = {
|
|
3
|
+
PREFIX: 'gemx-ab-test:',
|
|
4
|
+
SESSION_ID: 'gemx-ab-session-id',
|
|
5
|
+
USER_ID: 'gemx-ab-user-id',
|
|
6
|
+
};
|
|
7
|
+
const EXPERIMENT_ATTRIBUTES = {
|
|
8
|
+
config: 'config',
|
|
9
|
+
trackingEndpoint: 'tracking-endpoint',
|
|
10
|
+
gaTracking: 'ga-tracking',
|
|
11
|
+
autoTrack: 'auto-track',
|
|
12
|
+
fallbackVariant: 'fallback-variant',
|
|
13
|
+
debug: 'debug',
|
|
14
|
+
loading: 'loading',
|
|
15
|
+
onTrack: 'on-track',
|
|
16
|
+
onVariantAssigned: 'on-variant-assigned',
|
|
17
|
+
onError: 'on-error',
|
|
18
|
+
selectedVariant: 'selected-variant',
|
|
19
|
+
};
|
|
20
|
+
const EXPERIMENT_EVENTS = {
|
|
21
|
+
VARIANT_ASSIGNED: 'variant-assigned',
|
|
22
|
+
TRACK: 'track',
|
|
23
|
+
ERROR: 'error',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isBrowser$1 = () => typeof window !== 'undefined';
|
|
27
|
+
/**
|
|
28
|
+
* Parse cookie string into key-value pairs
|
|
29
|
+
*/
|
|
30
|
+
const parseCookies = (cookieString = '') => {
|
|
31
|
+
return cookieString
|
|
32
|
+
.split(';')
|
|
33
|
+
.map((cookie) => cookie.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.reduce((acc, cookie) => {
|
|
36
|
+
const [key, ...valueParts] = cookie.split('=');
|
|
37
|
+
if (key) {
|
|
38
|
+
acc[key] = decodeURIComponent(valueParts.join('='));
|
|
39
|
+
}
|
|
40
|
+
return acc;
|
|
41
|
+
}, {});
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Get cookie value (works on both client and server)
|
|
45
|
+
* @param key - Cookie name
|
|
46
|
+
* @param cookieString - Optional cookie string from server (e.g., from request headers)
|
|
47
|
+
*/
|
|
48
|
+
const getCookie = (key, cookieString) => {
|
|
49
|
+
if (isBrowser$1()) {
|
|
50
|
+
// Client-side: read from document.cookie
|
|
51
|
+
const cookies = parseCookies(document.cookie);
|
|
52
|
+
return cookies[key] || null;
|
|
53
|
+
}
|
|
54
|
+
else if (cookieString) {
|
|
55
|
+
// Server-side: parse from provided cookie string
|
|
56
|
+
const cookies = parseCookies(cookieString);
|
|
57
|
+
return cookies[key] || null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Set cookie (client-side only)
|
|
63
|
+
* @param key - Cookie name
|
|
64
|
+
* @param value - Cookie value
|
|
65
|
+
* @param days - Number of days until expiration (default: 365)
|
|
66
|
+
*/
|
|
67
|
+
const setCookie = (key, value, days = 30) => {
|
|
68
|
+
if (!isBrowser$1())
|
|
69
|
+
return;
|
|
70
|
+
if (getCookie(key))
|
|
71
|
+
return;
|
|
72
|
+
const expires = new Date();
|
|
73
|
+
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
|
74
|
+
document.cookie = `${key}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/; SameSite=None; Secure`;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Remove cookie (client-side only)
|
|
78
|
+
*/
|
|
79
|
+
const removeCookie = (key) => {
|
|
80
|
+
if (!isBrowser$1())
|
|
81
|
+
return;
|
|
82
|
+
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function getExperimentKey(experimentId) {
|
|
86
|
+
return `${EXPERIMENT_STORAGE_KEYS.PREFIX}${experimentId}`;
|
|
87
|
+
}
|
|
88
|
+
function getVariant(config, cookieString) {
|
|
89
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
90
|
+
const { experimentId, variants, weights } = config;
|
|
91
|
+
if (!experimentId || !variants || variants.length === 0) {
|
|
92
|
+
return 'control';
|
|
93
|
+
}
|
|
94
|
+
const experimentKey = getExperimentKey(experimentId);
|
|
95
|
+
try {
|
|
96
|
+
let persistedVariant = getCookie(experimentKey, cookieString);
|
|
97
|
+
if (!persistedVariant) {
|
|
98
|
+
// Determine weights - use config.weights or individual variant weights or equal distribution
|
|
99
|
+
let variantWeights;
|
|
100
|
+
if (weights && weights.length === variants.length) {
|
|
101
|
+
// Use provided weights array
|
|
102
|
+
variantWeights = weights;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Use individual variant weights or equal distribution
|
|
106
|
+
variantWeights = variants.map((variant) => variant.weight !== undefined ? variant.weight : 1 / variants.length);
|
|
107
|
+
}
|
|
108
|
+
// Calculate cumulative weights for selection
|
|
109
|
+
const totalWeight = variantWeights.reduce((sum, w) => sum + w, 0);
|
|
110
|
+
const rand = Math.random() * totalWeight;
|
|
111
|
+
let weightSum = 0;
|
|
112
|
+
for (let i = 0; i < variants.length; i++) {
|
|
113
|
+
weightSum += variantWeights[i] || 0;
|
|
114
|
+
if (rand < weightSum) {
|
|
115
|
+
persistedVariant = (_d = (_b = (_a = variants[i]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : (_c = variants[0]) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : 'control';
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Fallback to first variant if selection failed
|
|
120
|
+
if (!persistedVariant) {
|
|
121
|
+
persistedVariant = (_f = (_e = variants[0]) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : 'control';
|
|
122
|
+
}
|
|
123
|
+
// Save to cookie
|
|
124
|
+
setCookie(experimentKey, persistedVariant);
|
|
125
|
+
}
|
|
126
|
+
return persistedVariant || ((_g = variants[0]) === null || _g === void 0 ? void 0 : _g.name) || 'control';
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return ((_h = variants[0]) === null || _h === void 0 ? void 0 : _h.name) || 'control';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var IDeviceType;
|
|
134
|
+
(function (IDeviceType) {
|
|
135
|
+
IDeviceType["MOBILE"] = "mobile";
|
|
136
|
+
IDeviceType["DESKTOP"] = "desktop";
|
|
137
|
+
IDeviceType["TABLET"] = "tablet";
|
|
138
|
+
})(IDeviceType || (IDeviceType = {}));
|
|
139
|
+
|
|
140
|
+
const getDeviceType = (agent) => {
|
|
141
|
+
const userAgent = agent !== null && agent !== void 0 ? agent : navigator.userAgent;
|
|
142
|
+
if (/tablet|ipad|playbook|silk/i.test(userAgent)) {
|
|
143
|
+
return IDeviceType.TABLET;
|
|
144
|
+
}
|
|
145
|
+
if (/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(userAgent)) {
|
|
146
|
+
return IDeviceType.MOBILE;
|
|
147
|
+
}
|
|
148
|
+
return IDeviceType.DESKTOP;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Sets all attributes on the gx-experiment-wrapper element
|
|
153
|
+
*/
|
|
154
|
+
const setExperimentWrapperAttributes = (element, config) => {
|
|
155
|
+
var _a, _b, _c, _d;
|
|
156
|
+
if (!element)
|
|
157
|
+
return;
|
|
158
|
+
const attributesToSet = [];
|
|
159
|
+
// Set required config
|
|
160
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.config, JSON.stringify(config.config)]);
|
|
161
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.gaTracking, ((_a = config.gaTracking) !== null && _a !== void 0 ? _a : false).toString()]);
|
|
162
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.autoTrack, ((_b = config.autoTrack) !== null && _b !== void 0 ? _b : true).toString()]);
|
|
163
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.debug, ((_c = config.debug) !== null && _c !== void 0 ? _c : false).toString()]);
|
|
164
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.loading, ((_d = config.loading) !== null && _d !== void 0 ? _d : false).toString()]);
|
|
165
|
+
// Set optional attributes
|
|
166
|
+
if (config.trackingEndpoint) {
|
|
167
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.trackingEndpoint, config.trackingEndpoint]);
|
|
168
|
+
}
|
|
169
|
+
if (config.fallbackVariant) {
|
|
170
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.fallbackVariant, config.fallbackVariant]);
|
|
171
|
+
}
|
|
172
|
+
if (config.selectedVariant) {
|
|
173
|
+
attributesToSet.push([EXPERIMENT_ATTRIBUTES.selectedVariant, config.selectedVariant]);
|
|
174
|
+
}
|
|
175
|
+
// Set all attributes in a single microtask to minimize re-initializations
|
|
176
|
+
if (attributesToSet.length > 0) {
|
|
177
|
+
requestAnimationFrame(() => {
|
|
178
|
+
attributesToSet.forEach(([attr, value]) => {
|
|
179
|
+
element.setAttribute(attr, value);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Sets up event listeners on the gx-experiment-wrapper element
|
|
186
|
+
*/
|
|
187
|
+
const setupExperimentWrapperEventListeners = (element, callbacks = {}) => {
|
|
188
|
+
if (!element)
|
|
189
|
+
return () => { };
|
|
190
|
+
const handleVariantAssigned = (event) => {
|
|
191
|
+
var _a;
|
|
192
|
+
const customEvent = event;
|
|
193
|
+
const { variant, experimentId } = customEvent.detail;
|
|
194
|
+
console.log('🚀 ~ Experiment Wrapper: Variant assigned', {
|
|
195
|
+
variant,
|
|
196
|
+
experimentId,
|
|
197
|
+
});
|
|
198
|
+
(_a = callbacks.onVariantAssigned) === null || _a === void 0 ? void 0 : _a.call(callbacks, variant, experimentId);
|
|
199
|
+
};
|
|
200
|
+
const handleTrack = (event) => {
|
|
201
|
+
var _a;
|
|
202
|
+
const customEvent = event;
|
|
203
|
+
console.log('🚀 ~ Experiment Wrapper: Track event', customEvent.detail);
|
|
204
|
+
(_a = callbacks.onTrack) === null || _a === void 0 ? void 0 : _a.call(callbacks, customEvent.detail.event, customEvent.detail);
|
|
205
|
+
};
|
|
206
|
+
const handleError = (event) => {
|
|
207
|
+
var _a;
|
|
208
|
+
const customEvent = event;
|
|
209
|
+
const { error, experimentId: expId } = customEvent.detail;
|
|
210
|
+
console.log('🚀 ~ Experiment Wrapper: Error event', {
|
|
211
|
+
error,
|
|
212
|
+
experimentId: expId,
|
|
213
|
+
});
|
|
214
|
+
(_a = callbacks.onError) === null || _a === void 0 ? void 0 : _a.call(callbacks, error, expId);
|
|
215
|
+
};
|
|
216
|
+
const abortController = new AbortController();
|
|
217
|
+
element.addEventListener(EXPERIMENT_EVENTS.VARIANT_ASSIGNED, handleVariantAssigned, {
|
|
218
|
+
signal: abortController.signal,
|
|
219
|
+
});
|
|
220
|
+
element.addEventListener(EXPERIMENT_EVENTS.TRACK, handleTrack, {
|
|
221
|
+
signal: abortController.signal,
|
|
222
|
+
});
|
|
223
|
+
element.addEventListener(EXPERIMENT_EVENTS.ERROR, handleError, {
|
|
224
|
+
signal: abortController.signal,
|
|
225
|
+
});
|
|
226
|
+
// Return cleanup function
|
|
227
|
+
return () => {
|
|
228
|
+
abortController.abort();
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Complete setup function that configures both attributes and event listeners
|
|
233
|
+
*/
|
|
234
|
+
const setupExperimentWrapper = (element, config) => {
|
|
235
|
+
if (!element)
|
|
236
|
+
return () => { };
|
|
237
|
+
// Set attributes
|
|
238
|
+
setExperimentWrapperAttributes(element, config);
|
|
239
|
+
// Setup event listeners and return cleanup function
|
|
240
|
+
return setupExperimentWrapperEventListeners(element, config.callbacks);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const getOrCreateSessionId = () => {
|
|
244
|
+
let sessionId = sessionStorage.getItem(EXPERIMENT_STORAGE_KEYS.SESSION_ID);
|
|
245
|
+
if (!sessionId) {
|
|
246
|
+
sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
247
|
+
sessionStorage.setItem(EXPERIMENT_STORAGE_KEYS.SESSION_ID, sessionId);
|
|
248
|
+
}
|
|
249
|
+
return sessionId;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const isBrowser = () => typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
|
253
|
+
const getLocalStorageItem = (key) => {
|
|
254
|
+
if (!isBrowser())
|
|
255
|
+
return null;
|
|
256
|
+
try {
|
|
257
|
+
return window.localStorage.getItem(key);
|
|
258
|
+
}
|
|
259
|
+
catch (_a) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const setLocalStorageItem = (key, value) => {
|
|
264
|
+
if (!isBrowser())
|
|
265
|
+
return;
|
|
266
|
+
try {
|
|
267
|
+
window.localStorage.setItem(key, value);
|
|
268
|
+
}
|
|
269
|
+
catch (_a) {
|
|
270
|
+
// ignore
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const removeLocalStorageItem = (key) => {
|
|
274
|
+
if (!isBrowser())
|
|
275
|
+
return;
|
|
276
|
+
try {
|
|
277
|
+
window.localStorage.removeItem(key);
|
|
278
|
+
}
|
|
279
|
+
catch (_a) {
|
|
280
|
+
// ignore
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const getOrCreateUserId = () => {
|
|
285
|
+
let userId = localStorage.getItem(EXPERIMENT_STORAGE_KEYS.USER_ID);
|
|
286
|
+
if (!userId) {
|
|
287
|
+
userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
288
|
+
localStorage.setItem(EXPERIMENT_STORAGE_KEYS.USER_ID, userId);
|
|
289
|
+
}
|
|
290
|
+
return userId;
|
|
291
|
+
};
|
|
292
|
+
const hashUserId = (userId) => {
|
|
293
|
+
let hash = 0;
|
|
294
|
+
for (let i = 0; i < userId.length; i++) {
|
|
295
|
+
const char = userId.charCodeAt(i);
|
|
296
|
+
hash = (hash << 5) - hash + char;
|
|
297
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
298
|
+
}
|
|
299
|
+
return Math.abs(hash);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* GxExperimentWrapper - Framework-Agnostic A/B Testing Web Component
|
|
304
|
+
*
|
|
305
|
+
* This is a comprehensive Web Component that contains ALL A/B testing logic,
|
|
306
|
+
* making it truly framework-agnostic. It can be used in React, Vue, Angular,
|
|
307
|
+
* Svelte, or vanilla JavaScript without any framework-specific dependencies.
|
|
308
|
+
*/
|
|
309
|
+
function registerExperimentWebComponent() {
|
|
310
|
+
if (typeof window === 'undefined')
|
|
311
|
+
return;
|
|
312
|
+
class GxExperimentWrapper extends HTMLElement {
|
|
313
|
+
static get observedAttributes() {
|
|
314
|
+
return [
|
|
315
|
+
EXPERIMENT_ATTRIBUTES.config,
|
|
316
|
+
EXPERIMENT_ATTRIBUTES.trackingEndpoint,
|
|
317
|
+
EXPERIMENT_ATTRIBUTES.gaTracking,
|
|
318
|
+
EXPERIMENT_ATTRIBUTES.autoTrack,
|
|
319
|
+
EXPERIMENT_ATTRIBUTES.fallbackVariant,
|
|
320
|
+
EXPERIMENT_ATTRIBUTES.onTrack,
|
|
321
|
+
EXPERIMENT_ATTRIBUTES.onVariantAssigned,
|
|
322
|
+
EXPERIMENT_ATTRIBUTES.onError,
|
|
323
|
+
EXPERIMENT_ATTRIBUTES.selectedVariant,
|
|
324
|
+
EXPERIMENT_ATTRIBUTES.debug,
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
constructor() {
|
|
328
|
+
super();
|
|
329
|
+
this.experimentConfig = null;
|
|
330
|
+
this.currentVariant = null;
|
|
331
|
+
this.isInitialized = false;
|
|
332
|
+
this.impressionTracked = false;
|
|
333
|
+
this.retryCount = 0;
|
|
334
|
+
this.maxRetries = 3;
|
|
335
|
+
this.initializationTimeout = null;
|
|
336
|
+
this.attachShadow({ mode: 'open' });
|
|
337
|
+
this.sessionId = getOrCreateSessionId();
|
|
338
|
+
this.userId = getOrCreateUserId();
|
|
339
|
+
}
|
|
340
|
+
connectedCallback() {
|
|
341
|
+
this.initializeExperiment();
|
|
342
|
+
}
|
|
343
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
344
|
+
if (oldValue !== newValue) {
|
|
345
|
+
// Debounce initialization to prevent multiple calls when setting multiple attributes
|
|
346
|
+
if (this.initializationTimeout) {
|
|
347
|
+
clearTimeout(this.initializationTimeout);
|
|
348
|
+
}
|
|
349
|
+
this.initializationTimeout = window.setTimeout(() => {
|
|
350
|
+
this.initializeExperiment();
|
|
351
|
+
this.initializationTimeout = null;
|
|
352
|
+
}, 0);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Initialize the experiment with all configuration
|
|
357
|
+
*/
|
|
358
|
+
initializeExperiment() {
|
|
359
|
+
try {
|
|
360
|
+
this.parseConfiguration();
|
|
361
|
+
if (!this.experimentConfig)
|
|
362
|
+
return;
|
|
363
|
+
if (this.isInitialized && this.currentVariant)
|
|
364
|
+
return;
|
|
365
|
+
// Check targeting rules
|
|
366
|
+
if (!this.shouldRunExperiment()) {
|
|
367
|
+
this.currentVariant = this.getFallbackVariant();
|
|
368
|
+
this.render();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
// Get or assign variant
|
|
372
|
+
this.currentVariant = this.getExperimentVariant();
|
|
373
|
+
// Mark as initialized
|
|
374
|
+
this.isInitialized = true;
|
|
375
|
+
// Render with selected variant
|
|
376
|
+
this.render();
|
|
377
|
+
// Auto-track impression if enabled
|
|
378
|
+
if (this.isAutoTrackEnabled()) {
|
|
379
|
+
this.trackImpression();
|
|
380
|
+
}
|
|
381
|
+
// Emit variant assigned event
|
|
382
|
+
this.dispatchEvent(new CustomEvent(EXPERIMENT_EVENTS.VARIANT_ASSIGNED, {
|
|
383
|
+
detail: {
|
|
384
|
+
experimentId: this.experimentConfig.experimentId,
|
|
385
|
+
variant: this.currentVariant,
|
|
386
|
+
timestamp: Date.now(),
|
|
387
|
+
},
|
|
388
|
+
bubbles: true,
|
|
389
|
+
}));
|
|
390
|
+
if (this.isDebugEnabled()) {
|
|
391
|
+
console.log('🚀 ~ GxExperimentWrapper ~ Experiment Initialized:', {
|
|
392
|
+
experimentId: this.experimentConfig.experimentId,
|
|
393
|
+
variant: this.currentVariant,
|
|
394
|
+
config: this.experimentConfig,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
this.handleError(error);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
parseConfiguration() {
|
|
403
|
+
const configAttr = this.getAttribute(EXPERIMENT_ATTRIBUTES.config);
|
|
404
|
+
if (configAttr) {
|
|
405
|
+
try {
|
|
406
|
+
this.experimentConfig = JSON.parse(configAttr);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
console.warn('Failed to parse config attribute:', error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check if experiment should run based on targeting rules
|
|
415
|
+
*/
|
|
416
|
+
shouldRunExperiment() {
|
|
417
|
+
var _a;
|
|
418
|
+
const targeting = (_a = this.experimentConfig) === null || _a === void 0 ? void 0 : _a.targeting;
|
|
419
|
+
if (!targeting)
|
|
420
|
+
return true;
|
|
421
|
+
// Traffic percentage check
|
|
422
|
+
if (targeting.trafficPercentage && targeting.trafficPercentage < 100) {
|
|
423
|
+
const hash = hashUserId(this.userId);
|
|
424
|
+
const percentage = (hash % 100) + 1;
|
|
425
|
+
if (percentage > targeting.trafficPercentage) {
|
|
426
|
+
if (this.isDebugEnabled()) {
|
|
427
|
+
console.log('🚀 ~ GxExperimentWrapper ~ User excluded from experiment due to traffic percentage');
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Device type check
|
|
433
|
+
if (targeting.deviceType) {
|
|
434
|
+
const deviceType = getDeviceType();
|
|
435
|
+
if (deviceType !== targeting.deviceType) {
|
|
436
|
+
if (this.isDebugEnabled()) {
|
|
437
|
+
console.log('🚀 ~ GxExperimentWrapper ~ User excluded from experiment due to device type');
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Add more targeting rules as needed
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
getExperimentVariant() {
|
|
446
|
+
if (!this.experimentConfig)
|
|
447
|
+
return this.getFallbackVariant();
|
|
448
|
+
const selectedVariant = this.getAttribute(EXPERIMENT_ATTRIBUTES.selectedVariant);
|
|
449
|
+
if (selectedVariant) {
|
|
450
|
+
const key = getExperimentKey(this.experimentConfig.experimentId);
|
|
451
|
+
setCookie(key, selectedVariant);
|
|
452
|
+
return selectedVariant;
|
|
453
|
+
}
|
|
454
|
+
return getVariant(this.experimentConfig);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Render the component with CSS variant visibility
|
|
458
|
+
*/
|
|
459
|
+
render() {
|
|
460
|
+
const variant = this.currentVariant || this.getFallbackVariant();
|
|
461
|
+
console.log('🚀 ~ GxExperimentWrapper ~ render ~ variant:', variant);
|
|
462
|
+
const style = `
|
|
463
|
+
<style>
|
|
464
|
+
:host {
|
|
465
|
+
display: block;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* Hide all variants by default */
|
|
469
|
+
::slotted([data-variant]) {
|
|
470
|
+
display: none !important;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Show only the selected variant */
|
|
474
|
+
::slotted([data-variant="${variant}"]) {
|
|
475
|
+
display: block !important;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Fallback: if no data-variant specified, show all content */
|
|
479
|
+
::slotted(:not([data-variant])) {
|
|
480
|
+
display: block !important;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* Loading state */
|
|
484
|
+
:host([loading="true"]) {
|
|
485
|
+
opacity: 0.7;
|
|
486
|
+
pointer-events: none;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Error state */
|
|
490
|
+
:host([error="true"]) {
|
|
491
|
+
border: 2px solid #ef4444;
|
|
492
|
+
background-color: #fef2f2;
|
|
493
|
+
padding: 1rem;
|
|
494
|
+
border-radius: 0.5rem;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* Debug styles */
|
|
498
|
+
:host([debug="true"]) {
|
|
499
|
+
border: 2px dashed #3b82f6;
|
|
500
|
+
position: relative;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
:host([debug="true"])::before {
|
|
504
|
+
content: "🧪 " attr(experiment-id) " → " attr(variant);
|
|
505
|
+
position: absolute;
|
|
506
|
+
top: -1.5rem;
|
|
507
|
+
left: 0;
|
|
508
|
+
background: #3b82f6;
|
|
509
|
+
color: white;
|
|
510
|
+
padding: 0.25rem 0.5rem;
|
|
511
|
+
border-radius: 0.25rem;
|
|
512
|
+
font-size: 0.75rem;
|
|
513
|
+
font-family: monospace;
|
|
514
|
+
z-index: 1000;
|
|
515
|
+
}
|
|
516
|
+
</style>
|
|
517
|
+
`;
|
|
518
|
+
this.shadowRoot.innerHTML = `${style}<slot></slot>`;
|
|
519
|
+
if (!this.isInitialized) {
|
|
520
|
+
this.setAttribute(EXPERIMENT_ATTRIBUTES.loading, '');
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
this.removeAttribute(EXPERIMENT_ATTRIBUTES.loading);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Track impression event
|
|
528
|
+
*/
|
|
529
|
+
trackImpression() {
|
|
530
|
+
if (this.impressionTracked || !this.experimentConfig || !this.currentVariant) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.impressionTracked = true;
|
|
534
|
+
const trackingData = {
|
|
535
|
+
event: 'impression',
|
|
536
|
+
experimentId: this.experimentConfig.experimentId,
|
|
537
|
+
variant: this.currentVariant,
|
|
538
|
+
timestamp: Date.now(),
|
|
539
|
+
sessionId: this.sessionId,
|
|
540
|
+
userId: this.userId,
|
|
541
|
+
description: this.experimentConfig.description,
|
|
542
|
+
userAgent: navigator.userAgent,
|
|
543
|
+
referrer: document.referrer,
|
|
544
|
+
url: window.location.href,
|
|
545
|
+
deviceType: getDeviceType(),
|
|
546
|
+
};
|
|
547
|
+
this.track('impression', trackingData);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Track custom events
|
|
551
|
+
*/
|
|
552
|
+
trackEvent(event, data = {}) {
|
|
553
|
+
if (!this.experimentConfig || !this.currentVariant) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const trackingData = Object.assign({ event, experimentId: this.experimentConfig.experimentId, variant: this.currentVariant, timestamp: Date.now(), sessionId: this.sessionId, userId: this.userId }, data);
|
|
557
|
+
this.track(event, trackingData);
|
|
558
|
+
}
|
|
559
|
+
track(event, data) {
|
|
560
|
+
this.dispatchEvent(new CustomEvent(EXPERIMENT_EVENTS.TRACK, {
|
|
561
|
+
detail: data,
|
|
562
|
+
bubbles: true,
|
|
563
|
+
}));
|
|
564
|
+
// Google Analytics tracking
|
|
565
|
+
if (this.isGATrackingEnabled() && typeof window.gtag === 'function') {
|
|
566
|
+
window.gtag('event', event, data);
|
|
567
|
+
}
|
|
568
|
+
// Custom tracking endpoint
|
|
569
|
+
const trackingEndpoint = this.getAttribute(EXPERIMENT_ATTRIBUTES.trackingEndpoint);
|
|
570
|
+
if (trackingEndpoint) {
|
|
571
|
+
fetch(trackingEndpoint, {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
headers: { 'Content-Type': 'application/json' },
|
|
574
|
+
body: JSON.stringify(data),
|
|
575
|
+
}).catch((error) => {
|
|
576
|
+
console.warn('Failed to send tracking data:', error);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
// Debug logging
|
|
580
|
+
if (this.isDebugEnabled()) {
|
|
581
|
+
console.log('🚀 ~ GxExperimentWrapper ~ Tracked Event:', data);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Handle errors with retry logic
|
|
586
|
+
*/
|
|
587
|
+
handleError(error) {
|
|
588
|
+
var _a, _b;
|
|
589
|
+
console.error('🚀 ~ GxExperimentWrapper ~ Experiment Error:', error);
|
|
590
|
+
// Set error state
|
|
591
|
+
this.setAttribute('error', '');
|
|
592
|
+
// Try fallback variant
|
|
593
|
+
this.currentVariant = this.getFallbackVariant();
|
|
594
|
+
this.render();
|
|
595
|
+
// Emit error event
|
|
596
|
+
this.dispatchEvent(new CustomEvent(EXPERIMENT_EVENTS.ERROR, {
|
|
597
|
+
detail: {
|
|
598
|
+
error: error.message,
|
|
599
|
+
experimentId: (_a = this.experimentConfig) === null || _a === void 0 ? void 0 : _a.experimentId,
|
|
600
|
+
},
|
|
601
|
+
bubbles: true,
|
|
602
|
+
}));
|
|
603
|
+
// Retry logic
|
|
604
|
+
if (this.retryCount < this.maxRetries) {
|
|
605
|
+
this.retryCount++;
|
|
606
|
+
setTimeout(() => {
|
|
607
|
+
this.removeAttribute('error');
|
|
608
|
+
this.initializeExperiment();
|
|
609
|
+
}, 1000 * this.retryCount);
|
|
610
|
+
}
|
|
611
|
+
// Track error
|
|
612
|
+
if (typeof window.gtag === 'function') {
|
|
613
|
+
window.gtag('event', 'exception', {
|
|
614
|
+
description: error.message,
|
|
615
|
+
fatal: false,
|
|
616
|
+
experiment_error: true,
|
|
617
|
+
experiment_id: ((_b = this.experimentConfig) === null || _b === void 0 ? void 0 : _b.experimentId) || 'unknown',
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Helper Methods
|
|
622
|
+
getFallbackVariant() {
|
|
623
|
+
var _a, _b;
|
|
624
|
+
return (this.getAttribute(EXPERIMENT_ATTRIBUTES.fallbackVariant) ||
|
|
625
|
+
((_b = (_a = this.experimentConfig) === null || _a === void 0 ? void 0 : _a.variants[0]) === null || _b === void 0 ? void 0 : _b.name) ||
|
|
626
|
+
'control');
|
|
627
|
+
}
|
|
628
|
+
isAutoTrackEnabled() {
|
|
629
|
+
const autoTrack = this.getAttribute(EXPERIMENT_ATTRIBUTES.autoTrack);
|
|
630
|
+
return autoTrack !== 'false';
|
|
631
|
+
}
|
|
632
|
+
isGATrackingEnabled() {
|
|
633
|
+
const gaTracking = this.getAttribute(EXPERIMENT_ATTRIBUTES.gaTracking);
|
|
634
|
+
return gaTracking === 'true';
|
|
635
|
+
}
|
|
636
|
+
isDebugEnabled() {
|
|
637
|
+
const debug = this.getAttribute(EXPERIMENT_ATTRIBUTES.debug);
|
|
638
|
+
return debug === 'true';
|
|
639
|
+
}
|
|
640
|
+
// Public API Methods
|
|
641
|
+
/**
|
|
642
|
+
* Get current variant
|
|
643
|
+
*/
|
|
644
|
+
getCurrentVariant() {
|
|
645
|
+
return this.currentVariant;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Get experiment status
|
|
649
|
+
*/
|
|
650
|
+
getExperimentStatus() {
|
|
651
|
+
var _a;
|
|
652
|
+
return {
|
|
653
|
+
experimentId: (_a = this.experimentConfig) === null || _a === void 0 ? void 0 : _a.experimentId,
|
|
654
|
+
variant: this.currentVariant,
|
|
655
|
+
isInitialized: this.isInitialized,
|
|
656
|
+
sessionId: this.sessionId,
|
|
657
|
+
userId: this.userId,
|
|
658
|
+
config: this.experimentConfig,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Force re-assignment of variant (useful for testing)
|
|
663
|
+
*/
|
|
664
|
+
reassignVariant() {
|
|
665
|
+
if (this.experimentConfig) {
|
|
666
|
+
// Clear stored variant
|
|
667
|
+
removeCookie(`${EXPERIMENT_STORAGE_KEYS.PREFIX}${this.experimentConfig.experimentId}`);
|
|
668
|
+
// Re-initialize
|
|
669
|
+
this.impressionTracked = false;
|
|
670
|
+
this.initializeExperiment();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Check if current variant matches target
|
|
675
|
+
*/
|
|
676
|
+
isVariant(targetVariant) {
|
|
677
|
+
return this.currentVariant === targetVariant;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!customElements.get('gx-experiment-wrapper')) {
|
|
681
|
+
customElements.define('gx-experiment-wrapper', GxExperimentWrapper);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export { EXPERIMENT_ATTRIBUTES, EXPERIMENT_EVENTS, EXPERIMENT_STORAGE_KEYS, IDeviceType, getCookie, getDeviceType, getExperimentKey, getLocalStorageItem, getOrCreateSessionId, getOrCreateUserId, getVariant, hashUserId, parseCookies, registerExperimentWebComponent, removeCookie, removeLocalStorageItem, setCookie, setExperimentWrapperAttributes, setLocalStorageItem, setupExperimentWrapper, setupExperimentWrapperEventListeners };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
interface TrackData {
|
|
2
|
+
event: string;
|
|
3
|
+
variant: string;
|
|
4
|
+
experimentId: string;
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
}
|
|
7
|
+
export declare class Tracker {
|
|
8
|
+
private callback?;
|
|
9
|
+
constructor(callback?: (event: string, data: TrackData) => void);
|
|
10
|
+
trackEvent(event: string, data: TrackData): void;
|
|
11
|
+
}
|
|
12
|
+
export {};
|