@levi123/experiment-react 5.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 ADDED
@@ -0,0 +1 @@
1
+ # `@levi123/experiment-react`
@@ -0,0 +1,3 @@
1
+ import type { ExperimentWrapperProps } from '@levi123/experiment';
2
+ export declare const GxExperimentWrapper: React.FC<ExperimentWrapperProps>;
3
+ //# sourceMappingURL=GxExperimentWrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GxExperimentWrapper.d.ts","sourceRoot":"","sources":["../../src/components/GxExperimentWrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAsB,MAAM,qBAAqB,CAAC;AAMtF,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmEhE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './GxExperimentWrapper';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { ExperimentConfig } from '@levi123/experiment';
2
+ import type { ReactNode } from 'react';
3
+ interface ExperimentContextType {
4
+ value: string;
5
+ }
6
+ interface ExperimentProviderProps {
7
+ children: ReactNode;
8
+ config: ExperimentConfig;
9
+ server?: {
10
+ responseHeaders: Headers;
11
+ cookieHeader: string;
12
+ };
13
+ }
14
+ export declare const ExperimentProvider: React.FC<ExperimentProviderProps>;
15
+ export declare const useExperiment: () => ExperimentContextType;
16
+ export {};
17
+ //# sourceMappingURL=experiment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment.d.ts","sourceRoot":"","sources":["../../src/contexts/experiment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAID,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,CAAC,EAAE;QACP,eAAe,EAAE,OAAO,CAAC;QACzB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAsBhE,CAAC;AAEF,eAAO,MAAM,aAAa,QAAO,qBAMhC,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './experiment';
2
+ export * from './tracking';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contexts/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { IExperimentWrapper } from '@levi123/experiment';
2
+ import type { ReactNode, RefObject } from 'react';
3
+ interface TrackingContextType {
4
+ trackEvent: (event: string, data?: Record<string, unknown>) => void;
5
+ }
6
+ interface TrackingProviderProps {
7
+ children: ReactNode;
8
+ wrapperRef: RefObject<IExperimentWrapper | null>;
9
+ }
10
+ export declare const TrackingProvider: React.FC<TrackingProviderProps>;
11
+ export declare const useTracking: () => TrackingContextType;
12
+ export {};
13
+ //# sourceMappingURL=tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../../src/contexts/tracking.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlD,UAAU,mBAAmB;IAC3B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACrE;AAID,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,SAAS,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAU5D,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,mBAM9B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './components';
2
+ export * from './contexts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,99 @@
1
+ "use client"
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { registerExperimentWebComponent, setupExperimentWrapper, getVariant, getExperimentKey } from '@levi123/experiment';
4
+ import { createContext, useContext, useRef, useState, useEffect } from 'react';
5
+
6
+ const TrackingContext = createContext(undefined);
7
+ const TrackingProvider = ({ children, wrapperRef }) => {
8
+ const trackEvent = (event, data) => {
9
+ if (!wrapperRef.current) {
10
+ console.warn('ExperimentWrapper not initialized yet');
11
+ return;
12
+ }
13
+ wrapperRef.current.trackEvent(event, data);
14
+ };
15
+ return jsx(TrackingContext.Provider, { value: { trackEvent }, children: children });
16
+ };
17
+ const useTracking = () => {
18
+ const context = useContext(TrackingContext);
19
+ if (context === undefined) {
20
+ throw new Error('useTracking must be used within a TrackingProvider');
21
+ }
22
+ return context;
23
+ };
24
+
25
+ const GxExperimentWrapper = ({ config, trackingEndpoint, gaTracking = false, autoTrack = true, fallbackVariant, debug = false, loading = false, onTrack, onVariantAssigned, onError, children, selectedVariant, }) => {
26
+ const wrapperRef = useRef(null);
27
+ const [isLoadingExperimentWC, setIsLoadingExperimentWC] = useState(true);
28
+ useEffect(() => {
29
+ registerExperimentWebComponent();
30
+ setIsLoadingExperimentWC(false);
31
+ }, []);
32
+ // Setup the experiment wrapper using the helper function
33
+ useEffect(() => {
34
+ if (!wrapperRef.current || isLoadingExperimentWC)
35
+ return;
36
+ const cleanup = setupExperimentWrapper(wrapperRef.current, {
37
+ config,
38
+ trackingEndpoint,
39
+ gaTracking,
40
+ autoTrack,
41
+ fallbackVariant,
42
+ debug,
43
+ loading,
44
+ selectedVariant,
45
+ callbacks: {
46
+ onTrack,
47
+ onVariantAssigned,
48
+ onError,
49
+ },
50
+ });
51
+ return cleanup;
52
+ }, [
53
+ isLoadingExperimentWC,
54
+ autoTrack,
55
+ config,
56
+ debug,
57
+ fallbackVariant,
58
+ gaTracking,
59
+ loading,
60
+ trackingEndpoint,
61
+ selectedVariant,
62
+ onTrack,
63
+ onVariantAssigned,
64
+ onError,
65
+ ]);
66
+ if (isLoadingExperimentWC || !wrapperRef) {
67
+ return null;
68
+ }
69
+ return (jsx(TrackingProvider, { wrapperRef: wrapperRef, children: jsx("gx-experiment-wrapper", { ref: wrapperRef, children: children }) }));
70
+ };
71
+
72
+ const ExperimentContext = createContext(undefined);
73
+ const ExperimentProvider = ({ children, server, config }) => {
74
+ let value = '';
75
+ if (server && typeof window === 'undefined') {
76
+ const { responseHeaders, cookieHeader } = server;
77
+ const variant = getVariant(config, cookieHeader);
78
+ const experimentKey = getExperimentKey(config.experimentId);
79
+ value = variant;
80
+ const hasExperimentCookie = new RegExp(`(?:^|;\\s*)${experimentKey}=`).test(cookieHeader);
81
+ if (!hasExperimentCookie) {
82
+ const maxAge = 30 * 24 * 60 * 60;
83
+ responseHeaders.append('Set-Cookie', `${experimentKey}=${encodeURIComponent(variant)}; Path=/; Max-Age=${maxAge}; SameSite=None; Secure`);
84
+ }
85
+ }
86
+ else {
87
+ value = getVariant(config);
88
+ }
89
+ return jsx(ExperimentContext.Provider, { value: { value }, children: children });
90
+ };
91
+ const useExperiment = () => {
92
+ const context = useContext(ExperimentContext);
93
+ if (context === undefined) {
94
+ throw new Error('useExperiment must be used within an ExperimentProvider');
95
+ }
96
+ return context;
97
+ };
98
+
99
+ export { ExperimentProvider, GxExperimentWrapper, TrackingProvider, useExperiment, useTracking };
@@ -0,0 +1,99 @@
1
+ "use client"
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { registerExperimentWebComponent, setupExperimentWrapper, getVariant, getExperimentKey } from '@levi123/experiment';
4
+ import { createContext, useContext, useRef, useState, useEffect } from 'react';
5
+
6
+ const TrackingContext = createContext(undefined);
7
+ const TrackingProvider = ({ children, wrapperRef }) => {
8
+ const trackEvent = (event, data) => {
9
+ if (!wrapperRef.current) {
10
+ console.warn('ExperimentWrapper not initialized yet');
11
+ return;
12
+ }
13
+ wrapperRef.current.trackEvent(event, data);
14
+ };
15
+ return jsx(TrackingContext.Provider, { value: { trackEvent }, children: children });
16
+ };
17
+ const useTracking = () => {
18
+ const context = useContext(TrackingContext);
19
+ if (context === undefined) {
20
+ throw new Error('useTracking must be used within a TrackingProvider');
21
+ }
22
+ return context;
23
+ };
24
+
25
+ const GxExperimentWrapper = ({ config, trackingEndpoint, gaTracking = false, autoTrack = true, fallbackVariant, debug = false, loading = false, onTrack, onVariantAssigned, onError, children, selectedVariant, }) => {
26
+ const wrapperRef = useRef(null);
27
+ const [isLoadingExperimentWC, setIsLoadingExperimentWC] = useState(true);
28
+ useEffect(() => {
29
+ registerExperimentWebComponent();
30
+ setIsLoadingExperimentWC(false);
31
+ }, []);
32
+ // Setup the experiment wrapper using the helper function
33
+ useEffect(() => {
34
+ if (!wrapperRef.current || isLoadingExperimentWC)
35
+ return;
36
+ const cleanup = setupExperimentWrapper(wrapperRef.current, {
37
+ config,
38
+ trackingEndpoint,
39
+ gaTracking,
40
+ autoTrack,
41
+ fallbackVariant,
42
+ debug,
43
+ loading,
44
+ selectedVariant,
45
+ callbacks: {
46
+ onTrack,
47
+ onVariantAssigned,
48
+ onError,
49
+ },
50
+ });
51
+ return cleanup;
52
+ }, [
53
+ isLoadingExperimentWC,
54
+ autoTrack,
55
+ config,
56
+ debug,
57
+ fallbackVariant,
58
+ gaTracking,
59
+ loading,
60
+ trackingEndpoint,
61
+ selectedVariant,
62
+ onTrack,
63
+ onVariantAssigned,
64
+ onError,
65
+ ]);
66
+ if (isLoadingExperimentWC || !wrapperRef) {
67
+ return null;
68
+ }
69
+ return (jsx(TrackingProvider, { wrapperRef: wrapperRef, children: jsx("gx-experiment-wrapper", { ref: wrapperRef, children: children }) }));
70
+ };
71
+
72
+ const ExperimentContext = createContext(undefined);
73
+ const ExperimentProvider = ({ children, server, config }) => {
74
+ let value = '';
75
+ if (server && typeof window === 'undefined') {
76
+ const { responseHeaders, cookieHeader } = server;
77
+ const variant = getVariant(config, cookieHeader);
78
+ const experimentKey = getExperimentKey(config.experimentId);
79
+ value = variant;
80
+ const hasExperimentCookie = new RegExp(`(?:^|;\\s*)${experimentKey}=`).test(cookieHeader);
81
+ if (!hasExperimentCookie) {
82
+ const maxAge = 30 * 24 * 60 * 60;
83
+ responseHeaders.append('Set-Cookie', `${experimentKey}=${encodeURIComponent(variant)}; Path=/; Max-Age=${maxAge}; SameSite=None; Secure`);
84
+ }
85
+ }
86
+ else {
87
+ value = getVariant(config);
88
+ }
89
+ return jsx(ExperimentContext.Provider, { value: { value }, children: children });
90
+ };
91
+ const useExperiment = () => {
92
+ const context = useContext(ExperimentContext);
93
+ if (context === undefined) {
94
+ throw new Error('useExperiment must be used within an ExperimentProvider');
95
+ }
96
+ return context;
97
+ };
98
+
99
+ export { ExperimentProvider, GxExperimentWrapper, TrackingProvider, useExperiment, useTracking };
@@ -0,0 +1,3 @@
1
+ import type { ExperimentWrapperProps } from '@levi123/experiment';
2
+ export declare const GxExperimentWrapper: React.FC<ExperimentWrapperProps>;
3
+ //# sourceMappingURL=GxExperimentWrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GxExperimentWrapper.d.ts","sourceRoot":"","sources":["../../src/components/GxExperimentWrapper.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAsB,MAAM,qBAAqB,CAAC;AAMtF,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAmEhE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './GxExperimentWrapper';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { ExperimentConfig } from '@levi123/experiment';
2
+ import type { ReactNode } from 'react';
3
+ interface ExperimentContextType {
4
+ value: string;
5
+ }
6
+ interface ExperimentProviderProps {
7
+ children: ReactNode;
8
+ config: ExperimentConfig;
9
+ server?: {
10
+ responseHeaders: Headers;
11
+ cookieHeader: string;
12
+ };
13
+ }
14
+ export declare const ExperimentProvider: React.FC<ExperimentProviderProps>;
15
+ export declare const useExperiment: () => ExperimentContextType;
16
+ export {};
17
+ //# sourceMappingURL=experiment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment.d.ts","sourceRoot":"","sources":["../../src/contexts/experiment.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf;AAID,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,EAAE,gBAAgB,CAAC;IACzB,MAAM,CAAC,EAAE;QACP,eAAe,EAAE,OAAO,CAAC;QACzB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAsBhE,CAAC;AAEF,eAAO,MAAM,aAAa,QAAO,qBAMhC,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './experiment';
2
+ export * from './tracking';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/contexts/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { IExperimentWrapper } from '@levi123/experiment';
2
+ import type { ReactNode, RefObject } from 'react';
3
+ interface TrackingContextType {
4
+ trackEvent: (event: string, data?: Record<string, unknown>) => void;
5
+ }
6
+ interface TrackingProviderProps {
7
+ children: ReactNode;
8
+ wrapperRef: RefObject<IExperimentWrapper | null>;
9
+ }
10
+ export declare const TrackingProvider: React.FC<TrackingProviderProps>;
11
+ export declare const useTracking: () => TrackingContextType;
12
+ export {};
13
+ //# sourceMappingURL=tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../../src/contexts/tracking.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlD,UAAU,mBAAmB;IAC3B,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACrE;AAID,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,SAAS,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAU5D,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,mBAM9B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './components';
2
+ export * from './contexts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react/jsx-runtime"),require("react")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","react"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).ReactFlow={},e.jsxRuntime,e.React)}(this,function(e,t,n){"use strict";const i="gemx-ab-test:",r="gemx-ab-session-id",a="gemx-ab-user-id",o="config",s="tracking-endpoint",l="ga-tracking",d="auto-track",u="fallback-variant",c="debug",p="loading",h="on-track",m="on-variant-assigned",g="on-error",f="selected-variant",x="variant-assigned",v="track",b="error",E=()=>"undefined"!=typeof window,k=(e="")=>e.split(";").map(e=>e.trim()).filter(Boolean).reduce((e,t)=>{const[n,...i]=t.split("=");return n&&(e[n]=decodeURIComponent(i.join("="))),e},{}),w=(e,t)=>{if(E()){return k(document.cookie)[e]||null}if(t){return k(t)[e]||null}return null},I=(e,t,n=30)=>{if(!E())return;if(w(e))return;const i=new Date;i.setTime(i.getTime()+24*n*60*60*1e3),document.cookie=`${e}=${encodeURIComponent(t)}; expires=${i.toUTCString()}; path=/; SameSite=None; Secure`};function T(e){return`${i}${e}`}function C(e,t){var n,i,r,a,o,s,l,d;const{experimentId:u,variants:c,weights:p}=e;if(!u||!c||0===c.length)return"control";const h=T(u);try{let e=w(h,t);if(!e){let t;t=p&&p.length===c.length?p:c.map(e=>void 0!==e.weight?e.weight:1/c.length);const l=t.reduce((e,t)=>e+t,0),d=Math.random()*l;let u=0;for(let o=0;o<c.length;o++)if(u+=t[o]||0,d<u){e=null!==(a=null!==(i=null===(n=c[o])||void 0===n?void 0:n.name)&&void 0!==i?i:null===(r=c[0])||void 0===r?void 0:r.name)&&void 0!==a?a:"control";break}e||(e=null!==(s=null===(o=c[0])||void 0===o?void 0:o.name)&&void 0!==s?s:"control"),I(h,e)}return e||(null===(l=c[0])||void 0===l?void 0:l.name)||"control"}catch(e){return(null===(d=c[0])||void 0===d?void 0:d.name)||"control"}}var y;!function(e){e.MOBILE="mobile",e.DESKTOP="desktop",e.TABLET="tablet"}(y||(y={}));const V=e=>{const t=null!=e?e:navigator.userAgent;return/tablet|ipad|playbook|silk/i.test(t)?y.TABLET:/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(t)?y.MOBILE:y.DESKTOP},A=(e,t)=>e?(((e,t)=>{var n,i,r,a;if(!e)return;const h=[];h.push([o,JSON.stringify(t.config)]),h.push([l,(null!==(n=t.gaTracking)&&void 0!==n&&n).toString()]),h.push([d,(null===(i=t.autoTrack)||void 0===i||i).toString()]),h.push([c,(null!==(r=t.debug)&&void 0!==r&&r).toString()]),h.push([p,(null!==(a=t.loading)&&void 0!==a&&a).toString()]),t.trackingEndpoint&&h.push([s,t.trackingEndpoint]),t.fallbackVariant&&h.push([u,t.fallbackVariant]),t.selectedVariant&&h.push([f,t.selectedVariant]),h.length>0&&requestAnimationFrame(()=>{h.forEach(([t,n])=>{e.setAttribute(t,n)})})})(e,t),((e,t={})=>{if(!e)return()=>{};const n=new AbortController;return e.addEventListener(x,e=>{var n;const i=e,{variant:r,experimentId:a}=i.detail;console.log("๐Ÿš€ ~ Experiment Wrapper: Variant assigned",{variant:r,experimentId:a}),null===(n=t.onVariantAssigned)||void 0===n||n.call(t,r,a)},{signal:n.signal}),e.addEventListener(v,e=>{var n;const i=e;console.log("๐Ÿš€ ~ Experiment Wrapper: Track event",i.detail),null===(n=t.onTrack)||void 0===n||n.call(t,i.detail.event,i.detail)},{signal:n.signal}),e.addEventListener(b,e=>{var n;const i=e,{error:r,experimentId:a}=i.detail;console.log("๐Ÿš€ ~ Experiment Wrapper: Error event",{error:r,experimentId:a}),null===(n=t.onError)||void 0===n||n.call(t,r,a)},{signal:n.signal}),()=>{n.abort()}})(e,t.callbacks)):()=>{};function S(){if("undefined"!=typeof window){class e extends HTMLElement{static get observedAttributes(){return[o,s,l,d,u,h,m,g,f,c]}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=(()=>{let e=sessionStorage.getItem(r);return e||(e="session_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),sessionStorage.setItem(r,e)),e})(),this.userId=(()=>{let e=localStorage.getItem(a);return e||(e="user_"+Date.now()+"_"+Math.random().toString(36).substr(2,9),localStorage.setItem(a,e)),e})()}connectedCallback(){this.initializeExperiment()}attributeChangedCallback(e,t,n){t!==n&&(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(x,{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(o);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((e=>{let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return Math.abs(t)})(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(V()!==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(f);if(e){const t=T(this.experimentConfig.experimentId);return I(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(p):this.setAttribute(p,"")}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:V()};this.track("impression",e)}trackEvent(e,t={}){if(!this.experimentConfig||!this.currentVariant)return;const n=Object.assign({event:e,experimentId:this.experimentConfig.experimentId,variant:this.currentVariant,timestamp:Date.now(),sessionId:this.sessionId,userId:this.userId},t);this.track(e,n)}track(e,t){this.dispatchEvent(new CustomEvent(v,{detail:t,bubbles:!0})),this.isGATrackingEnabled()&&"function"==typeof window.gtag&&window.gtag("event",e,t);const n=this.getAttribute(s);n&&fetch(n,{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,n;console.error("๐Ÿš€ ~ GxExperimentWrapper ~ Experiment Error:",e),this.setAttribute("error",""),this.currentVariant=this.getFallbackVariant(),this.render(),this.dispatchEvent(new CustomEvent(b,{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===(n=this.experimentConfig)||void 0===n?void 0:n.experimentId)||"unknown"})}getFallbackVariant(){var e,t;return this.getAttribute(u)||(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(d)}isGATrackingEnabled(){return"true"===this.getAttribute(l)}isDebugEnabled(){return"true"===this.getAttribute(c)}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(){var e;this.experimentConfig&&(e=`${i}${this.experimentConfig.experimentId}`,E()&&(document.cookie=`${e}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`),this.impressionTracked=!1,this.initializeExperiment())}isVariant(e){return this.currentVariant===e}}customElements.get("gx-experiment-wrapper")||customElements.define("gx-experiment-wrapper",e)}}const z=n.createContext(void 0),R=({children:e,wrapperRef:n})=>t.jsx(z.Provider,{value:{trackEvent:(e,t)=>{n.current?n.current.trackEvent(e,t):console.warn("ExperimentWrapper not initialized yet")}},children:e}),D=n.createContext(void 0);e.ExperimentProvider=({children:e,server:n,config:i})=>{let r="";if(n&&"undefined"==typeof window){const{responseHeaders:e,cookieHeader:t}=n,a=C(i,t),o=T(i.experimentId);r=a;if(!new RegExp(`(?:^|;\\s*)${o}=`).test(t)){const t=2592e3;e.append("Set-Cookie",`${o}=${encodeURIComponent(a)}; Path=/; Max-Age=${t}; SameSite=None; Secure`)}}else r=C(i);return t.jsx(D.Provider,{value:{value:r},children:e})},e.GxExperimentWrapper=({config:e,trackingEndpoint:i,gaTracking:r=!1,autoTrack:a=!0,fallbackVariant:o,debug:s=!1,loading:l=!1,onTrack:d,onVariantAssigned:u,onError:c,children:p,selectedVariant:h})=>{const m=n.useRef(null),[g,f]=n.useState(!0);return n.useEffect(()=>{S(),f(!1)},[]),n.useEffect(()=>{if(!m.current||g)return;return A(m.current,{config:e,trackingEndpoint:i,gaTracking:r,autoTrack:a,fallbackVariant:o,debug:s,loading:l,selectedVariant:h,callbacks:{onTrack:d,onVariantAssigned:u,onError:c}})},[g,a,e,s,o,r,l,i,h,d,u,c]),g||!m?null:t.jsx(R,{wrapperRef:m,children:t.jsx("gx-experiment-wrapper",{ref:m,children:p})})},e.TrackingProvider=R,e.useExperiment=()=>{const e=n.useContext(D);if(void 0===e)throw new Error("useExperiment must be used within an ExperimentProvider");return e},e.useTracking=()=>{const e=n.useContext(z);if(void 0===e)throw new Error("useTracking must be used within a TrackingProvider");return e}});
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@levi123/experiment-react",
3
+ "version": "5.0.0-dev.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "source": "src/index.ts",
7
+ "scripts": {
8
+ "build": "rollup --config node:@levi123/rollup-config --environment NODE_ENV:production",
9
+ "clean": "rm -rf dist",
10
+ "format": "prettier --write \"**/*.{ts,tsx,md}\"",
11
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
12
+ "lint:all": "yarn lint && yarn lint:tsc",
13
+ "lint:tsc": "tsc --noEmit",
14
+ "post:publish": "node ../../scripts/convert-publish.js",
15
+ "pre:publish": "node ../../scripts/convert-publish.js -p",
16
+ "test": "jest --runInBand"
17
+ },
18
+ "peerDependencies": {
19
+ "react": ">=17",
20
+ "react-dom": ">=17"
21
+ },
22
+ "dependencies": {
23
+ "@levi123/experiment": "4.0.0-dev.0",
24
+ "react": "18.3.1",
25
+ "tsup": "8.5.0"
26
+ },
27
+ "devDependencies": {
28
+ "eslint": "9.36.0",
29
+ "rollup": "3.29.5"
30
+ },
31
+ "main": "dist/umd/index.js",
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "module": "dist/esm/index.js",
36
+ "types": "dist/esm/index.d.ts",
37
+ "exports": {
38
+ "./package.json": "./package.json",
39
+ ".": {
40
+ "node": {
41
+ "types": "./dist/esm/index.d.ts",
42
+ "module": "./dist/esm/index.js",
43
+ "require": "./dist/umd/index.js",
44
+ "import": "./dist/esm/index.mjs"
45
+ },
46
+ "browser": {
47
+ "import": "./dist/esm/index.js",
48
+ "require": "./dist/umd/index.js"
49
+ },
50
+ "default": "./dist/esm/index.js"
51
+ }
52
+ }
53
+ }