@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,82 @@
|
|
|
1
|
+
import type { IExperimentWrapper } from '../components/experiment-wrapper';
|
|
2
|
+
export interface Variant {
|
|
3
|
+
name: string;
|
|
4
|
+
weight?: number;
|
|
5
|
+
component?: any;
|
|
6
|
+
metadata?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
export interface ExperimentConfig {
|
|
9
|
+
experimentId: string;
|
|
10
|
+
variants: Variant[];
|
|
11
|
+
weights?: number[];
|
|
12
|
+
description?: string;
|
|
13
|
+
targeting?: TargetingConfig;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
export interface TargetingConfig {
|
|
17
|
+
trafficPercentage?: number;
|
|
18
|
+
deviceType?: 'mobile' | 'desktop' | 'tablet';
|
|
19
|
+
country?: string;
|
|
20
|
+
userSegment?: string;
|
|
21
|
+
customRules?: Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
export interface TrackingEvent {
|
|
24
|
+
event: string;
|
|
25
|
+
experimentId: string;
|
|
26
|
+
variant: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
userId?: string;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
export interface ExperimentStatus {
|
|
33
|
+
experimentId?: string;
|
|
34
|
+
variant: string | null;
|
|
35
|
+
isInitialized: boolean;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
config: ExperimentConfig | null;
|
|
39
|
+
}
|
|
40
|
+
export interface ExperimentWrapperConfigProps {
|
|
41
|
+
config: ExperimentConfig;
|
|
42
|
+
selectedVariant?: string;
|
|
43
|
+
trackingEndpoint?: string;
|
|
44
|
+
gaTracking?: boolean;
|
|
45
|
+
autoTrack?: boolean;
|
|
46
|
+
fallbackVariant?: string;
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
onTrack?: (event: string, data: TrackingEvent) => void;
|
|
50
|
+
onVariantAssigned?: (variant: string, experimentId: string) => void;
|
|
51
|
+
onError?: (error: string, experimentId?: string) => void;
|
|
52
|
+
}
|
|
53
|
+
export interface ExperimentWrapperProps extends ExperimentWrapperConfigProps {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
export interface ABTestHookResult {
|
|
57
|
+
variant: string | null;
|
|
58
|
+
isLoading: boolean;
|
|
59
|
+
error: Error | null;
|
|
60
|
+
experimentConfig: ExperimentConfig | null;
|
|
61
|
+
track: (event: string, data?: Record<string, any>) => void;
|
|
62
|
+
isVariant: (targetVariant: string) => boolean;
|
|
63
|
+
reassignVariant: () => void;
|
|
64
|
+
getStatus: () => ExperimentStatus;
|
|
65
|
+
}
|
|
66
|
+
export interface VariantAssignedEvent extends CustomEvent {
|
|
67
|
+
detail: {
|
|
68
|
+
experimentId: string;
|
|
69
|
+
variant: string;
|
|
70
|
+
timestamp: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface TrackEvent extends CustomEvent {
|
|
74
|
+
detail: TrackingEvent;
|
|
75
|
+
}
|
|
76
|
+
export interface ExperimentErrorEvent extends CustomEvent {
|
|
77
|
+
detail: {
|
|
78
|
+
error: string;
|
|
79
|
+
experimentId?: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export type GxExperimentWrapperElement = IExperimentWrapper;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse cookie string into key-value pairs
|
|
3
|
+
*/
|
|
4
|
+
export declare const parseCookies: (cookieString?: string) => Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Get cookie value (works on both client and server)
|
|
7
|
+
* @param key - Cookie name
|
|
8
|
+
* @param cookieString - Optional cookie string from server (e.g., from request headers)
|
|
9
|
+
*/
|
|
10
|
+
export declare const getCookie: (key: string, cookieString?: string) => string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Set cookie (client-side only)
|
|
13
|
+
* @param key - Cookie name
|
|
14
|
+
* @param value - Cookie value
|
|
15
|
+
* @param days - Number of days until expiration (default: 365)
|
|
16
|
+
*/
|
|
17
|
+
export declare const setCookie: (key: string, value: string, days?: number) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Remove cookie (client-side only)
|
|
20
|
+
*/
|
|
21
|
+
export declare const removeCookie: (key: string) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExperimentWrapperConfigProps, TrackingEvent } from '../types';
|
|
2
|
+
export interface ExperimentWrapperCallbacks {
|
|
3
|
+
onTrack?: (event: string, data: TrackingEvent) => void;
|
|
4
|
+
onVariantAssigned?: (variant: string, experimentId: string) => void;
|
|
5
|
+
onError?: (error: string, experimentId?: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ExperimentWrapperConfig extends Omit<ExperimentWrapperConfigProps, 'onTrack' | 'onVariantAssigned' | 'onError'> {
|
|
8
|
+
callbacks?: ExperimentWrapperCallbacks;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sets all attributes on the gx-experiment-wrapper element
|
|
12
|
+
*/
|
|
13
|
+
export declare const setExperimentWrapperAttributes: (element: any, config: ExperimentWrapperConfig) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Sets up event listeners on the gx-experiment-wrapper element
|
|
16
|
+
*/
|
|
17
|
+
export declare const setupExperimentWrapperEventListeners: (element: any, callbacks?: ExperimentWrapperCallbacks) => (() => void);
|
|
18
|
+
/**
|
|
19
|
+
* Complete setup function that configures both attributes and event listeners
|
|
20
|
+
*/
|
|
21
|
+
export declare const setupExperimentWrapper: (element: any, config: ExperimentWrapperConfig) => (() => void);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getOrCreateSessionId: () => string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GxExperimentWrapper - Framework-Agnostic A/B Testing Web Component
|
|
3
|
+
*
|
|
4
|
+
* This is a comprehensive Web Component that contains ALL A/B testing logic,
|
|
5
|
+
* making it truly framework-agnostic. It can be used in React, Vue, Angular,
|
|
6
|
+
* Svelte, or vanilla JavaScript without any framework-specific dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExperimentStatus } from '../types';
|
|
9
|
+
export interface IExperimentWrapper extends HTMLElement {
|
|
10
|
+
getCurrentVariant(): string | null;
|
|
11
|
+
getExperimentStatus(): ExperimentStatus;
|
|
12
|
+
reassignVariant(): void;
|
|
13
|
+
isVariant(targetVariant: string): boolean;
|
|
14
|
+
trackEvent(event: string, data?: Record<string, any>): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function registerExperimentWebComponent(): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './experiment-wrapper';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExperimentWrapperProps } from '../types';
|
|
2
|
+
export declare const EXPERIMENT_STORAGE_KEYS: {
|
|
3
|
+
PREFIX: string;
|
|
4
|
+
SESSION_ID: string;
|
|
5
|
+
USER_ID: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const EXPERIMENT_ATTRIBUTES: Record<keyof Omit<ExperimentWrapperProps, 'children'>, string>;
|
|
8
|
+
export declare const EXPERIMENT_EVENTS: {
|
|
9
|
+
VARIANT_ASSIGNED: string;
|
|
10
|
+
TRACK: string;
|
|
11
|
+
ERROR: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ab-testing';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ReactFlow={})}(this,function(e){"use strict";const t={PREFIX:"gemx-ab-test:",SESSION_ID:"gemx-ab-session-id",USER_ID:"gemx-ab-user-id"},i={config:"config",trackingEndpoint:"tracking-endpoint",gaTracking:"ga-tracking",autoTrack:"auto-track",fallbackVariant:"fallback-variant",debug:"debug",loading:"loading",onTrack:"on-track",onVariantAssigned:"on-variant-assigned",onError:"on-error",selectedVariant:"selected-variant"},n={VARIANT_ASSIGNED:"variant-assigned",TRACK:"track",ERROR:"error"},r=()=>"undefined"!=typeof window,a=(e="")=>e.split(";").map(e=>e.trim()).filter(Boolean).reduce((e,t)=>{const[i,...n]=t.split("=");return i&&(e[i]=decodeURIComponent(n.join("="))),e},{}),o=(e,t)=>{if(r()){return a(document.cookie)[e]||null}if(t){return a(t)[e]||null}return null},s=(e,t,i=30)=>{if(!r())return;if(o(e))return;const n=new Date;n.setTime(n.getTime()+24*i*60*60*1e3),document.cookie=`${e}=${encodeURIComponent(t)}; expires=${n.toUTCString()}; path=/; SameSite=None; Secure`},l=e=>{r()&&(document.cookie=`${e}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`)};function d(e){return`${t.PREFIX}${e}`}function c(e,t){var i,n,r,a,l,c,u,p;const{experimentId:g,variants:m,weights:h}=e;if(!g||!m||0===m.length)return"control";const f=d(g);try{let e=o(f,t);if(!e){let t;t=h&&h.length===m.length?h:m.map(e=>void 0!==e.weight?e.weight:1/m.length);const o=t.reduce((e,t)=>e+t,0),d=Math.random()*o;let u=0;for(let o=0;o<m.length;o++)if(u+=t[o]||0,d<u){e=null!==(a=null!==(n=null===(i=m[o])||void 0===i?void 0:i.name)&&void 0!==n?n:null===(r=m[0])||void 0===r?void 0:r.name)&&void 0!==a?a:"control";break}e||(e=null!==(c=null===(l=m[0])||void 0===l?void 0:l.name)&&void 0!==c?c:"control"),s(f,e)}return e||(null===(u=m[0])||void 0===u?void 0:u.name)||"control"}catch(e){return(null===(p=m[0])||void 0===p?void 0:p.name)||"control"}}var u;e.IDeviceType=void 0,(u=e.IDeviceType||(e.IDeviceType={})).MOBILE="mobile",u.DESKTOP="desktop",u.TABLET="tablet";const p=t=>{const i=null!=t?t:navigator.userAgent;return/tablet|ipad|playbook|silk/i.test(i)?e.IDeviceType.TABLET:/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(i)?e.IDeviceType.MOBILE:e.IDeviceType.DESKTOP},g=(e,t)=>{var n,r,a,o;if(!e)return;const s=[];s.push([i.config,JSON.stringify(t.config)]),s.push([i.gaTracking,(null!==(n=t.gaTracking)&&void 0!==n&&n).toString()]),s.push([i.autoTrack,(null===(r=t.autoTrack)||void 0===r||r).toString()]),s.push([i.debug,(null!==(a=t.debug)&&void 0!==a&&a).toString()]),s.push([i.loading,(null!==(o=t.loading)&&void 0!==o&&o).toString()]),t.trackingEndpoint&&s.push([i.trackingEndpoint,t.trackingEndpoint]),t.fallbackVariant&&s.push([i.fallbackVariant,t.fallbackVariant]),t.selectedVariant&&s.push([i.selectedVariant,t.selectedVariant]),s.length>0&&requestAnimationFrame(()=>{s.forEach(([t,i])=>{e.setAttribute(t,i)})})},m=(e,t={})=>{if(!e)return()=>{};const i=new AbortController;return e.addEventListener(n.VARIANT_ASSIGNED,e=>{var i;const n=e,{variant:r,experimentId:a}=n.detail;console.log("🚀 ~ Experiment Wrapper: Variant assigned",{variant:r,experimentId:a}),null===(i=t.onVariantAssigned)||void 0===i||i.call(t,r,a)},{signal:i.signal}),e.addEventListener(n.TRACK,e=>{var i;const n=e;console.log("🚀 ~ Experiment Wrapper: Track event",n.detail),null===(i=t.onTrack)||void 0===i||i.call(t,n.detail.event,n.detail)},{signal:i.signal}),e.addEventListener(n.ERROR,e=>{var i;const n=e,{error:r,experimentId:a}=n.detail;console.log("🚀 ~ Experiment Wrapper: Error event",{error:r,experimentId:a}),null===(i=t.onError)||void 0===i||i.call(t,r,a)},{signal:i.signal}),()=>{i.abort()}},h=()=>{let e=sessionStorage.getItem(t.SESSION_ID);return e||(e="session_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),sessionStorage.setItem(t.SESSION_ID,e)),e},f=()=>"undefined"!=typeof window&&void 0!==window.localStorage,v=()=>{let e=localStorage.getItem(t.USER_ID);return e||(e="user_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),localStorage.setItem(t.USER_ID,e)),e},b=e=>{let t=0;for(let i=0;i<e.length;i++){t=(t<<5)-t+e.charCodeAt(i),t&=t}return Math.abs(t)};e.EXPERIMENT_ATTRIBUTES=i,e.EXPERIMENT_EVENTS=n,e.EXPERIMENT_STORAGE_KEYS=t,e.getCookie=o,e.getDeviceType=p,e.getExperimentKey=d,e.getLocalStorageItem=e=>{if(!f())return null;try{return window.localStorage.getItem(e)}catch(e){return null}},e.getOrCreateSessionId=h,e.getOrCreateUserId=v,e.getVariant=c,e.hashUserId=b,e.parseCookies=a,e.registerExperimentWebComponent=function(){if("undefined"!=typeof window){class e extends HTMLElement{static get observedAttributes(){return[i.config,i.trackingEndpoint,i.gaTracking,i.autoTrack,i.fallbackVariant,i.onTrack,i.onVariantAssigned,i.onError,i.selectedVariant,i.debug]}constructor(){super(),this.experimentConfig=null,this.currentVariant=null,this.isInitialized=!1,this.impressionTracked=!1,this.retryCount=0,this.maxRetries=3,this.initializationTimeout=null,this.attachShadow({mode:"open"}),this.sessionId=h(),this.userId=v()}connectedCallback(){this.initializeExperiment()}attributeChangedCallback(e,t,i){t!==i&&(this.initializationTimeout&&clearTimeout(this.initializationTimeout),this.initializationTimeout=window.setTimeout(()=>{this.initializeExperiment(),this.initializationTimeout=null},0))}initializeExperiment(){try{if(this.parseConfiguration(),!this.experimentConfig)return;if(this.isInitialized&&this.currentVariant)return;if(!this.shouldRunExperiment())return this.currentVariant=this.getFallbackVariant(),void this.render();this.currentVariant=this.getExperimentVariant(),this.isInitialized=!0,this.render(),this.isAutoTrackEnabled()&&this.trackImpression(),this.dispatchEvent(new CustomEvent(n.VARIANT_ASSIGNED,{detail:{experimentId:this.experimentConfig.experimentId,variant:this.currentVariant,timestamp:Date.now()},bubbles:!0})),this.isDebugEnabled()&&console.log("🚀 ~ GxExperimentWrapper ~ Experiment Initialized:",{experimentId:this.experimentConfig.experimentId,variant:this.currentVariant,config:this.experimentConfig})}catch(e){this.handleError(e)}}parseConfiguration(){const e=this.getAttribute(i.config);if(e)try{this.experimentConfig=JSON.parse(e)}catch(e){console.warn("Failed to parse config attribute:",e)}}shouldRunExperiment(){var e;const t=null===(e=this.experimentConfig)||void 0===e?void 0:e.targeting;if(!t)return!0;if(t.trafficPercentage&&t.trafficPercentage<100){if(b(this.userId)%100+1>t.trafficPercentage)return this.isDebugEnabled()&&console.log("🚀 ~ GxExperimentWrapper ~ User excluded from experiment due to traffic percentage"),!1}if(t.deviceType){if(p()!==t.deviceType)return this.isDebugEnabled()&&console.log("🚀 ~ GxExperimentWrapper ~ User excluded from experiment due to device type"),!1}return!0}getExperimentVariant(){if(!this.experimentConfig)return this.getFallbackVariant();const e=this.getAttribute(i.selectedVariant);if(e){const t=d(this.experimentConfig.experimentId);return s(t,e),e}return c(this.experimentConfig)}render(){const e=this.currentVariant||this.getFallbackVariant();console.log("🚀 ~ GxExperimentWrapper ~ render ~ variant:",e);const t=`\n <style>\n :host {\n display: block;\n }\n\n /* Hide all variants by default */\n ::slotted([data-variant]) {\n display: none !important;\n }\n\n /* Show only the selected variant */\n ::slotted([data-variant="${e}"]) {\n display: block !important;\n }\n\n /* Fallback: if no data-variant specified, show all content */\n ::slotted(:not([data-variant])) {\n display: block !important;\n }\n\n /* Loading state */\n :host([loading="true"]) {\n opacity: 0.7;\n pointer-events: none;\n }\n\n /* Error state */\n :host([error="true"]) {\n border: 2px solid #ef4444;\n background-color: #fef2f2;\n padding: 1rem;\n border-radius: 0.5rem;\n }\n\n /* Debug styles */\n :host([debug="true"]) {\n border: 2px dashed #3b82f6;\n position: relative;\n }\n\n :host([debug="true"])::before {\n content: "🧪 " attr(experiment-id) " → " attr(variant);\n position: absolute;\n top: -1.5rem;\n left: 0;\n background: #3b82f6;\n color: white;\n padding: 0.25rem 0.5rem;\n border-radius: 0.25rem;\n font-size: 0.75rem;\n font-family: monospace;\n z-index: 1000;\n }\n </style>\n `;this.shadowRoot.innerHTML=`${t}<slot></slot>`,this.isInitialized?this.removeAttribute(i.loading):this.setAttribute(i.loading,"")}trackImpression(){if(this.impressionTracked||!this.experimentConfig||!this.currentVariant)return;this.impressionTracked=!0;const e={event:"impression",experimentId:this.experimentConfig.experimentId,variant:this.currentVariant,timestamp:Date.now(),sessionId:this.sessionId,userId:this.userId,description:this.experimentConfig.description,userAgent:navigator.userAgent,referrer:document.referrer,url:window.location.href,deviceType:p()};this.track("impression",e)}trackEvent(e,t={}){if(!this.experimentConfig||!this.currentVariant)return;const i=Object.assign({event:e,experimentId:this.experimentConfig.experimentId,variant:this.currentVariant,timestamp:Date.now(),sessionId:this.sessionId,userId:this.userId},t);this.track(e,i)}track(e,t){this.dispatchEvent(new CustomEvent(n.TRACK,{detail:t,bubbles:!0})),this.isGATrackingEnabled()&&"function"==typeof window.gtag&&window.gtag("event",e,t);const r=this.getAttribute(i.trackingEndpoint);r&&fetch(r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}).catch(e=>{console.warn("Failed to send tracking data:",e)}),this.isDebugEnabled()&&console.log("🚀 ~ GxExperimentWrapper ~ Tracked Event:",t)}handleError(e){var t,i;console.error("🚀 ~ GxExperimentWrapper ~ Experiment Error:",e),this.setAttribute("error",""),this.currentVariant=this.getFallbackVariant(),this.render(),this.dispatchEvent(new CustomEvent(n.ERROR,{detail:{error:e.message,experimentId:null===(t=this.experimentConfig)||void 0===t?void 0:t.experimentId},bubbles:!0})),this.retryCount<this.maxRetries&&(this.retryCount++,setTimeout(()=>{this.removeAttribute("error"),this.initializeExperiment()},1e3*this.retryCount)),"function"==typeof window.gtag&&window.gtag("event","exception",{description:e.message,fatal:!1,experiment_error:!0,experiment_id:(null===(i=this.experimentConfig)||void 0===i?void 0:i.experimentId)||"unknown"})}getFallbackVariant(){var e,t;return this.getAttribute(i.fallbackVariant)||(null===(t=null===(e=this.experimentConfig)||void 0===e?void 0:e.variants[0])||void 0===t?void 0:t.name)||"control"}isAutoTrackEnabled(){return"false"!==this.getAttribute(i.autoTrack)}isGATrackingEnabled(){return"true"===this.getAttribute(i.gaTracking)}isDebugEnabled(){return"true"===this.getAttribute(i.debug)}getCurrentVariant(){return this.currentVariant}getExperimentStatus(){var e;return{experimentId:null===(e=this.experimentConfig)||void 0===e?void 0:e.experimentId,variant:this.currentVariant,isInitialized:this.isInitialized,sessionId:this.sessionId,userId:this.userId,config:this.experimentConfig}}reassignVariant(){this.experimentConfig&&(l(`${t.PREFIX}${this.experimentConfig.experimentId}`),this.impressionTracked=!1,this.initializeExperiment())}isVariant(e){return this.currentVariant===e}}customElements.get("gx-experiment-wrapper")||customElements.define("gx-experiment-wrapper",e)}},e.removeCookie=l,e.removeLocalStorageItem=e=>{if(f())try{window.localStorage.removeItem(e)}catch(e){}},e.setCookie=s,e.setExperimentWrapperAttributes=g,e.setLocalStorageItem=(e,t)=>{if(f())try{window.localStorage.setItem(e,t)}catch(e){}},e.setupExperimentWrapper=(e,t)=>e?(g(e,t),m(e,t.callbacks)):()=>{},e.setupExperimentWrapperEventListeners=m});
|
|
@@ -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,82 @@
|
|
|
1
|
+
import type { IExperimentWrapper } from '../components/experiment-wrapper';
|
|
2
|
+
export interface Variant {
|
|
3
|
+
name: string;
|
|
4
|
+
weight?: number;
|
|
5
|
+
component?: any;
|
|
6
|
+
metadata?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
export interface ExperimentConfig {
|
|
9
|
+
experimentId: string;
|
|
10
|
+
variants: Variant[];
|
|
11
|
+
weights?: number[];
|
|
12
|
+
description?: string;
|
|
13
|
+
targeting?: TargetingConfig;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
export interface TargetingConfig {
|
|
17
|
+
trafficPercentage?: number;
|
|
18
|
+
deviceType?: 'mobile' | 'desktop' | 'tablet';
|
|
19
|
+
country?: string;
|
|
20
|
+
userSegment?: string;
|
|
21
|
+
customRules?: Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
export interface TrackingEvent {
|
|
24
|
+
event: string;
|
|
25
|
+
experimentId: string;
|
|
26
|
+
variant: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
userId?: string;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
export interface ExperimentStatus {
|
|
33
|
+
experimentId?: string;
|
|
34
|
+
variant: string | null;
|
|
35
|
+
isInitialized: boolean;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
config: ExperimentConfig | null;
|
|
39
|
+
}
|
|
40
|
+
export interface ExperimentWrapperConfigProps {
|
|
41
|
+
config: ExperimentConfig;
|
|
42
|
+
selectedVariant?: string;
|
|
43
|
+
trackingEndpoint?: string;
|
|
44
|
+
gaTracking?: boolean;
|
|
45
|
+
autoTrack?: boolean;
|
|
46
|
+
fallbackVariant?: string;
|
|
47
|
+
debug?: boolean;
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
onTrack?: (event: string, data: TrackingEvent) => void;
|
|
50
|
+
onVariantAssigned?: (variant: string, experimentId: string) => void;
|
|
51
|
+
onError?: (error: string, experimentId?: string) => void;
|
|
52
|
+
}
|
|
53
|
+
export interface ExperimentWrapperProps extends ExperimentWrapperConfigProps {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
export interface ABTestHookResult {
|
|
57
|
+
variant: string | null;
|
|
58
|
+
isLoading: boolean;
|
|
59
|
+
error: Error | null;
|
|
60
|
+
experimentConfig: ExperimentConfig | null;
|
|
61
|
+
track: (event: string, data?: Record<string, any>) => void;
|
|
62
|
+
isVariant: (targetVariant: string) => boolean;
|
|
63
|
+
reassignVariant: () => void;
|
|
64
|
+
getStatus: () => ExperimentStatus;
|
|
65
|
+
}
|
|
66
|
+
export interface VariantAssignedEvent extends CustomEvent {
|
|
67
|
+
detail: {
|
|
68
|
+
experimentId: string;
|
|
69
|
+
variant: string;
|
|
70
|
+
timestamp: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface TrackEvent extends CustomEvent {
|
|
74
|
+
detail: TrackingEvent;
|
|
75
|
+
}
|
|
76
|
+
export interface ExperimentErrorEvent extends CustomEvent {
|
|
77
|
+
detail: {
|
|
78
|
+
error: string;
|
|
79
|
+
experimentId?: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export type GxExperimentWrapperElement = IExperimentWrapper;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse cookie string into key-value pairs
|
|
3
|
+
*/
|
|
4
|
+
export declare const parseCookies: (cookieString?: string) => Record<string, string>;
|
|
5
|
+
/**
|
|
6
|
+
* Get cookie value (works on both client and server)
|
|
7
|
+
* @param key - Cookie name
|
|
8
|
+
* @param cookieString - Optional cookie string from server (e.g., from request headers)
|
|
9
|
+
*/
|
|
10
|
+
export declare const getCookie: (key: string, cookieString?: string) => string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Set cookie (client-side only)
|
|
13
|
+
* @param key - Cookie name
|
|
14
|
+
* @param value - Cookie value
|
|
15
|
+
* @param days - Number of days until expiration (default: 365)
|
|
16
|
+
*/
|
|
17
|
+
export declare const setCookie: (key: string, value: string, days?: number) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Remove cookie (client-side only)
|
|
20
|
+
*/
|
|
21
|
+
export declare const removeCookie: (key: string) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExperimentWrapperConfigProps, TrackingEvent } from '../types';
|
|
2
|
+
export interface ExperimentWrapperCallbacks {
|
|
3
|
+
onTrack?: (event: string, data: TrackingEvent) => void;
|
|
4
|
+
onVariantAssigned?: (variant: string, experimentId: string) => void;
|
|
5
|
+
onError?: (error: string, experimentId?: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ExperimentWrapperConfig extends Omit<ExperimentWrapperConfigProps, 'onTrack' | 'onVariantAssigned' | 'onError'> {
|
|
8
|
+
callbacks?: ExperimentWrapperCallbacks;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sets all attributes on the gx-experiment-wrapper element
|
|
12
|
+
*/
|
|
13
|
+
export declare const setExperimentWrapperAttributes: (element: any, config: ExperimentWrapperConfig) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Sets up event listeners on the gx-experiment-wrapper element
|
|
16
|
+
*/
|
|
17
|
+
export declare const setupExperimentWrapperEventListeners: (element: any, callbacks?: ExperimentWrapperCallbacks) => (() => void);
|
|
18
|
+
/**
|
|
19
|
+
* Complete setup function that configures both attributes and event listeners
|
|
20
|
+
*/
|
|
21
|
+
export declare const setupExperimentWrapper: (element: any, config: ExperimentWrapperConfig) => (() => void);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getOrCreateSessionId: () => string;
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@levi123/experiment",
|
|
3
|
+
"version": "4.0.0-dev.0",
|
|
4
|
+
"description": "AB Testing Section Block Components for multiple frameworks with tracking",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ab-testing",
|
|
7
|
+
"web-components",
|
|
8
|
+
"react",
|
|
9
|
+
"vue",
|
|
10
|
+
"tracking"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "gemx-dev",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"source": "src/index.ts",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "rollup --config node:@levi123/rollup-config --environment NODE_ENV:production",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"dev": "echo 'Add dev script here'",
|
|
20
|
+
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
21
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
22
|
+
"lint:all": "yarn lint && yarn lint:tsc",
|
|
23
|
+
"lint:tsc": "tsc --noEmit",
|
|
24
|
+
"post:publish": "node ../../scripts/convert-publish.js",
|
|
25
|
+
"pre:publish": "node ../../scripts/convert-publish.js -p",
|
|
26
|
+
"test": "jest --runInBand"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"tsup": "8.5.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"eslint": "9.36.0",
|
|
33
|
+
"rollup": "3.29.5"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=17",
|
|
37
|
+
"react-dom": ">=17"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"main": "dist/umd/index.js",
|
|
43
|
+
"files": [
|
|
44
|
+
"dist"
|
|
45
|
+
],
|
|
46
|
+
"module": "dist/esm/index.js",
|
|
47
|
+
"types": "dist/esm/index.d.ts",
|
|
48
|
+
"exports": {
|
|
49
|
+
"./package.json": "./package.json",
|
|
50
|
+
".": {
|
|
51
|
+
"node": {
|
|
52
|
+
"types": "./dist/esm/index.d.ts",
|
|
53
|
+
"module": "./dist/esm/index.js",
|
|
54
|
+
"require": "./dist/umd/index.js",
|
|
55
|
+
"import": "./dist/esm/index.mjs"
|
|
56
|
+
},
|
|
57
|
+
"browser": {
|
|
58
|
+
"import": "./dist/esm/index.js",
|
|
59
|
+
"require": "./dist/umd/index.js"
|
|
60
|
+
},
|
|
61
|
+
"default": "./dist/esm/index.js"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|