@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.
Files changed (39) hide show
  1. package/README.md +464 -0
  2. package/dist/esm/components/experiment-wrapper.d.ts +16 -0
  3. package/dist/esm/components/index.d.ts +1 -0
  4. package/dist/esm/constants/index.d.ts +12 -0
  5. package/dist/esm/core/ab-testing.d.ts +3 -0
  6. package/dist/esm/core/index.d.ts +1 -0
  7. package/dist/esm/index.d.ts +5 -0
  8. package/dist/esm/index.js +685 -0
  9. package/dist/esm/index.mjs +685 -0
  10. package/dist/esm/tracking/tracker.d.ts +12 -0
  11. package/dist/esm/types/devices.d.ts +5 -0
  12. package/dist/esm/types/experiments.d.ts +82 -0
  13. package/dist/esm/types/index.d.ts +2 -0
  14. package/dist/esm/utils/cookie.d.ts +21 -0
  15. package/dist/esm/utils/devices.d.ts +2 -0
  16. package/dist/esm/utils/experiment-wrapper-helpers.d.ts +21 -0
  17. package/dist/esm/utils/index.d.ts +6 -0
  18. package/dist/esm/utils/session.d.ts +1 -0
  19. package/dist/esm/utils/storage.d.ts +3 -0
  20. package/dist/esm/utils/user.d.ts +2 -0
  21. package/dist/umd/components/experiment-wrapper.d.ts +16 -0
  22. package/dist/umd/components/index.d.ts +1 -0
  23. package/dist/umd/constants/index.d.ts +12 -0
  24. package/dist/umd/core/ab-testing.d.ts +3 -0
  25. package/dist/umd/core/index.d.ts +1 -0
  26. package/dist/umd/index.d.ts +5 -0
  27. package/dist/umd/index.js +1 -0
  28. package/dist/umd/tracking/tracker.d.ts +12 -0
  29. package/dist/umd/types/devices.d.ts +5 -0
  30. package/dist/umd/types/experiments.d.ts +82 -0
  31. package/dist/umd/types/index.d.ts +2 -0
  32. package/dist/umd/utils/cookie.d.ts +21 -0
  33. package/dist/umd/utils/devices.d.ts +2 -0
  34. package/dist/umd/utils/experiment-wrapper-helpers.d.ts +21 -0
  35. package/dist/umd/utils/index.d.ts +6 -0
  36. package/dist/umd/utils/session.d.ts +1 -0
  37. package/dist/umd/utils/storage.d.ts +3 -0
  38. package/dist/umd/utils/user.d.ts +2 -0
  39. 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 {};
@@ -0,0 +1,5 @@
1
+ export declare enum IDeviceType {
2
+ MOBILE = "mobile",
3
+ DESKTOP = "desktop",
4
+ TABLET = "tablet"
5
+ }