@elevateab/sdk 1.0.0 → 1.1.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 CHANGED
@@ -10,57 +10,66 @@ npm install @elevateab/sdk
10
10
 
11
11
  ## Usage
12
12
 
13
- ### Basic Example
13
+ ### Automatic Config Loading
14
14
 
15
- ```typescript
16
- import { useExperiment, ExperimentConfig } from '@elevateab/sdk';
17
-
18
- const config: ExperimentConfig = {
19
- id: 'price-test-001',
20
- name: 'Pricing Experiment',
21
- enabled: true,
22
- variants: [
23
- { id: 'control', name: 'Control', weight: 50 },
24
- { id: 'variant-a', name: 'Variant A', weight: 50 },
25
- ],
26
- };
15
+ The easiest way to use the SDK is with `ElevateProvider`. It automatically fetches all active experiments from the CDN and makes them available throughout your app:
27
16
 
28
- function MyComponent() {
29
- const { variant, isLoading } = useExperiment(config, 'user-123');
17
+ ```typescript
18
+ import { ElevateProvider, Experiment, VariantDisplay } from "@elevateab/sdk";
30
19
 
31
- if (isLoading) return <div>Loading...</div>;
20
+ function App() {
21
+ return (
22
+ <ElevateProvider storeId="mystore.myshopify.com">
23
+ <ProductPage />
24
+ </ElevateProvider>
25
+ );
26
+ }
32
27
 
28
+ function ProductPage() {
33
29
  return (
34
30
  <div>
35
- {variant?.id === 'control' ? (
36
- <Price amount={99.99} />
37
- ) : (
38
- <Price amount={89.99} />
39
- )}
31
+ {/* Price test - Only the assigned variant will render */}
32
+ <Experiment testId="exp-123" userId="user-123">
33
+ <VariantDisplay variantId="control">
34
+ <Price amount={99.99} />
35
+ </VariantDisplay>
36
+ <VariantDisplay variantId="variant-a">
37
+ <Price amount={89.99} />
38
+ </VariantDisplay>
39
+ </Experiment>
40
+
41
+ {/* Headline test */}
42
+ <Experiment testId="exp-456" userId="user-123">
43
+ <VariantDisplay variantId="control">
44
+ <h1>Original Headline</h1>
45
+ </VariantDisplay>
46
+ <VariantDisplay variantId="variant-b">
47
+ <h1>New Headline</h1>
48
+ </VariantDisplay>
49
+ </Experiment>
40
50
  </div>
41
51
  );
42
52
  }
43
53
  ```
44
54
 
45
- ### Using the Experiment Component
55
+ ### Using the Hook
46
56
 
47
57
  ```typescript
48
- import { Experiment, VariantDisplay } from '@elevateab/sdk';
58
+ import { ElevateProvider, useExperiment } from "@elevateab/sdk";
59
+
60
+ function MyComponent() {
61
+ const { variant, isLoading } = useExperiment("exp-123", "user-123");
62
+
63
+ if (isLoading) return <div>Loading...</div>;
49
64
 
50
- function ProductPage() {
51
65
  return (
52
- <Experiment
53
- config={config}
54
- userId="user-123"
55
- onVariantAssigned={(variant) => console.log('Assigned:', variant)}
56
- >
57
- <VariantDisplay variantId="control">
58
- <h1>Original Headline</h1>
59
- </VariantDisplay>
60
- <VariantDisplay variantId="variant-a">
61
- <h1>New Headline</h1>
62
- </VariantDisplay>
63
- </Experiment>
66
+ <div>
67
+ {variant?.id === "control" ? (
68
+ <Price amount={99.99} />
69
+ ) : (
70
+ <Price amount={89.99} />
71
+ )}
72
+ </div>
64
73
  );
65
74
  }
66
75
  ```
@@ -73,23 +82,23 @@ import {
73
82
  validateConfig,
74
83
  generateExperimentId,
75
84
  calculateRevenueLift,
76
- } from '@elevateab/sdk';
85
+ } from "@elevateab/sdk";
77
86
 
78
87
  // Assign a variant based on weighted distribution
79
- const variant = assignVariant(config.variants, 'user-123');
88
+ const variant = assignVariant(config.variants, "user-123");
80
89
 
81
90
  // Validate experiment configuration
82
91
  const isValid = validateConfig(config);
83
92
 
84
93
  // Generate unique experiment ID
85
- const expId = generateExperimentId('My Test');
94
+ const expId = generateExperimentId("My Test");
86
95
 
87
96
  // Calculate revenue lift
88
97
  const { lift, confidence } = calculateRevenueLift(
89
98
  10000, // control revenue
90
99
  12000, // variant revenue
91
- 500, // control sample size
92
- 500 // variant sample size
100
+ 500, // control sample size
101
+ 500 // variant sample size
93
102
  );
94
103
  ```
95
104
 
@@ -122,9 +131,11 @@ const { lift, confidence } = calculateRevenueLift(
122
131
 
123
132
  ### Components
124
133
 
134
+ - `ElevateProvider`: Provider component that fetches configs from CDN
125
135
  - `Experiment`: Main experiment wrapper component
126
136
  - `VariantDisplay`: Conditional rendering based on variant
127
137
  - `useExperiment`: React hook for experiment logic
138
+ - `useElevateConfig`: Hook to access config from context
128
139
 
129
140
  ### Utilities
130
141
 
@@ -141,4 +152,3 @@ MIT
141
152
  ## Support
142
153
 
143
154
  For issues and questions, please visit [GitHub Issues](https://github.com/elevateab/sdk/issues).
144
-
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var y=Object.create;var p=Object.defineProperty;var C=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var M=Object.getPrototypeOf,L=Object.prototype.hasOwnProperty;var N=(t,e)=>{for(var n in e)p(t,n,{get:e[n],enumerable:!0})},l=(t,e,n,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of D(e))!L.call(t,r)&&r!==n&&p(t,r,{get:()=>e[r],enumerable:!(a=C(e,r))||a.enumerable});return t};var w=(t,e,n)=>(n=t!=null?y(M(t)):{},l(e||!t||!t.__esModule?p(n,"default",{value:t,enumerable:!0}):n,t)),I=t=>l(p({},"__esModule",{value:!0}),t);var k={};N(k,{DEFAULT_CONFIG:()=>R,Experiment:()=>h,VERSION:()=>P,VariantDisplay:()=>x,assignVariant:()=>c,calculateRevenueLift:()=>g,generateExperimentId:()=>f,hashString:()=>u,useExperiment:()=>E,validateConfig:()=>m});module.exports=I(k);function c(t,e){let n=t.reduce((i,d)=>i+d.weight,0),r=u(e)%n,s=0;for(let i of t)if(s+=i.weight,r<s)return i;return t[0]}function u(t){let e=0;for(let n=0;n<t.length;n++){let a=t.charCodeAt(n);e=(e<<5)-e+a,e=e&e}return Math.abs(e)}function m(t){return!t.id||!t.name||!t.variants||t.variants.length===0?!1:t.variants.reduce((n,a)=>n+a.weight,0)===100}function f(t){let e=Date.now(),n=Math.random().toString(36).substring(2,9);return`exp-${t.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${e}-${n}`}function g(t,e,n,a){let r=t/n,s=e/a,i=(s-r)/r*100,v=Math.sqrt((t+e)/(n+a))*Math.sqrt(1/n+1/a),V=Math.abs(s-r)/v,b=Math.min(99.9,V*34);return{lift:i,confidence:b}}var o=w(require("react"),1);function h({config:t,userId:e,onVariantAssigned:n,children:a}){let[r,s]=o.default.useState(null);return o.default.useEffect(()=>{if(t.enabled&&t.variants.length>0){let i=c(t.variants,e);s(i),n?.(i)}},[t,e,n]),!t.enabled||!r?null:o.default.createElement("div",{"data-experiment-id":t.id,"data-variant-id":r.id},a)}function x({variantId:t,children:e}){return o.default.createElement("div",{"data-variant":t,style:{display:"contents"}},e)}function E(t,e){let[n,a]=o.default.useState(null),[r,s]=o.default.useState(!0);return o.default.useEffect(()=>{if(!t.enabled){s(!1);return}let i=c(t.variants,e);a(i),s(!1)},[t,e]),{variant:n,isLoading:r}}var P="1.0.0",R={enabled:!0,trackingEndpoint:"https://analytics.elevateab.com/track",cacheDuration:3600};0&&(module.exports={DEFAULT_CONFIG,Experiment,VERSION,VariantDisplay,assignVariant,calculateRevenueLift,generateExperimentId,hashString,useExperiment,validateConfig});
1
+ "use strict";var T=Object.create;var p=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var L=Object.getPrototypeOf,N=Object.prototype.hasOwnProperty;var $=(t,e)=>{for(var n in e)p(t,n,{get:e[n],enumerable:!0})},h=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of I(e))!N.call(t,a)&&a!==n&&p(t,a,{get:()=>e[a],enumerable:!(o=D(e,a))||o.enumerable});return t};var m=(t,e,n)=>(n=t!=null?T(L(t)):{},h(e||!t||!t.__esModule?p(n,"default",{value:t,enumerable:!0}):n,t)),A=t=>h(p({},"__esModule",{value:!0}),t);var S={};$(S,{DEFAULT_CONFIG:()=>O,ElevateProvider:()=>B,Experiment:()=>P,VERSION:()=>F,VariantDisplay:()=>k,assignVariant:()=>f,calculateRevenueLift:()=>b,generateExperimentId:()=>V,hashString:()=>v,useElevateConfig:()=>u,useExperiment:()=>w,validateConfig:()=>C});module.exports=A(S);function f(t,e){let n=t.reduce((i,r)=>i+r.weight,0),a=v(e)%n,s=0;for(let i of t)if(s+=i.weight,a<s)return i;return t[0]}function v(t){let e=0;for(let n=0;n<t.length;n++){let o=t.charCodeAt(n);e=(e<<5)-e+o,e=e&e}return Math.abs(e)}function C(t){return!t.testId||!t.name||!t.variations||t.variations.length===0?!1:t.variations.reduce((n,o)=>n+o.weight,0)===100}function V(t){let e=Date.now(),n=Math.random().toString(36).substring(2,9);return`exp-${t.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${e}-${n}`}function b(t,e,n,o){let a=t/n,s=e/o,i=(s-a)/a*100,c=Math.sqrt((t+e)/(n+o))*Math.sqrt(1/n+1/o),g=Math.abs(s-a)/c,M=Math.min(99.9,g*34);return{lift:i,confidence:M}}function y(t){let e=[];for(let[n,o]of Object.entries(t.allTests)){if(!o.data.isLive)continue;let a=[];for(let[s,i]of Object.entries(o)){if(s==="data")continue;let r=i;typeof r=="object"&&r!==null&&"variationName"in r&&a.push({id:s,name:r.variationName,weight:r.trafficPercentage,isControl:r.isControl,productId:r.id,handle:r.handle,price:r.price})}a.length>0&&e.push({testId:n,name:o.data.name,enabled:o.data.isLive,type:o.data.type,variations:a})}return{tests:e,selectors:t.selectors}}var l=m(require("react"),1);var x=m(require("react"),1),E=x.default.createContext(null);function u(){let t=x.default.useContext(E);if(t===null)throw new Error("useElevateConfig must be used within ElevateProvider");return t}var j=l.default.createContext({assignedVariantId:null});function P({testId:t,userId:e,onVariantAssigned:n,children:o}){let{config:a}=u(),[s,i]=l.default.useState(null),r=l.default.useMemo(()=>a&&a.tests.find(c=>c.testId===t)||null,[a,t]);return l.default.useEffect(()=>{if(r&&r.enabled&&r.variations.length>0){let c=f(r.variations,e);i(c),n?.(c)}},[r,e,n]),!r||!r.enabled||!s?null:l.default.createElement("div",{"data-experiment-id":r.testId,"data-variant-id":s.id},o)}function k({variantId:t,children:e}){let{assignedVariantId:n}=l.default.useContext(j);return n?n!==t?null:l.default.createElement(l.default.Fragment,null,e):l.default.createElement(l.default.Fragment,null,e)}function w(t,e){let{config:n}=u(),[o,a]=l.default.useState(null),[s,i]=l.default.useState(!0),r=l.default.useMemo(()=>n&&n.tests.find(c=>c.testId===t)||null,[n,t]);return l.default.useEffect(()=>{if(!r){console.error(`[ElevateAB] Test not found: ${t}`),i(!1);return}if(!r.enabled){console.error(`[ElevateAB] Test disabled: ${t}`),i(!1);return}let c=f(r.variations,e);a(c),i(!1)},[r,e,t]),{variant:o,isLoading:s}}var d=m(require("react"),1);function B({storeId:t,children:e}){let[n,o]=d.default.useState(null);d.default.useEffect(()=>{async function s(){try{let i=`https://configs.elevateab.com/config/${t}.json`,r=await fetch(i);if(r.status===404){o({tests:[],selectors:void 0});return}if(!r.ok)throw new Error(`Failed to fetch config: ${r.status} ${r.statusText}`);let c=await r.json(),g=y(c);o(g)}catch(i){console.error("[ElevateAB] Failed to load config:",i),o({tests:[],selectors:void 0})}}s()},[t]);let a=d.default.useMemo(()=>({config:n}),[n]);return d.default.createElement(E.Provider,{value:a},e)}var F="1.1.0",O={enabled:!0,trackingEndpoint:"https://analytics.elevateab.com/track",cacheDuration:3600};0&&(module.exports={DEFAULT_CONFIG,ElevateProvider,Experiment,VERSION,VariantDisplay,assignVariant,calculateRevenueLift,generateExperimentId,hashString,useElevateConfig,useExperiment,validateConfig});
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/utils.ts","../src/components/Experiment.tsx"],"sourcesContent":["// Main entry point for the Elevate AB Testing NPM package\nexport type {\n ExperimentConfig,\n Variant,\n TrackingEvent,\n ExperimentStatus,\n} from './types';\n\nexport {\n assignVariant,\n hashString,\n validateConfig,\n generateExperimentId,\n calculateRevenueLift,\n} from './utils';\n\nexport {\n Experiment,\n VariantDisplay,\n useExperiment,\n} from './components/Experiment';\n\n// Package version\nexport const VERSION = '1.0.0';\n\n// Default configuration\nexport const DEFAULT_CONFIG = {\n enabled: true,\n trackingEndpoint: 'https://analytics.elevateab.com/track',\n cacheDuration: 3600,\n};\n\n","import type { Variant, ExperimentConfig } from './types';\n\n/**\n * Assigns a variant based on weighted distribution\n */\nexport function assignVariant(variants: Variant[], userId: string): Variant {\n const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);\n const hash = hashString(userId);\n const normalized = hash % totalWeight;\n \n let cumulative = 0;\n for (const variant of variants) {\n cumulative += variant.weight;\n if (normalized < cumulative) {\n return variant;\n }\n }\n \n return variants[0];\n}\n\n/**\n * Simple hash function for user ID\n */\nexport function hashString(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return Math.abs(hash);\n}\n\n/**\n * Validates experiment configuration\n */\nexport function validateConfig(config: ExperimentConfig): boolean {\n if (!config.id || !config.name) return false;\n if (!config.variants || config.variants.length === 0) return false;\n \n const totalWeight = config.variants.reduce((sum, v) => sum + v.weight, 0);\n return totalWeight === 100;\n}\n\n/**\n * Generates a unique experiment ID\n */\nexport function generateExperimentId(name: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 9);\n const safeName = name.toLowerCase().replace(/[^a-z0-9]/g, '-');\n return `exp-${safeName}-${timestamp}-${random}`;\n}\n\n/**\n * Sensitive business logic that should be minified/obfuscated\n */\nexport function calculateRevenueLift(\n controlRevenue: number,\n variantRevenue: number,\n controlSampleSize: number,\n variantSampleSize: number\n): { lift: number; confidence: number } {\n const controlMean = controlRevenue / controlSampleSize;\n const variantMean = variantRevenue / variantSampleSize;\n \n const lift = ((variantMean - controlMean) / controlMean) * 100;\n \n // Simplified confidence calculation\n const pooledStdDev = Math.sqrt(\n (controlRevenue + variantRevenue) / (controlSampleSize + variantSampleSize)\n );\n const standardError = pooledStdDev * Math.sqrt(\n 1 / controlSampleSize + 1 / variantSampleSize\n );\n const zScore = Math.abs(variantMean - controlMean) / standardError;\n const confidence = Math.min(99.9, zScore * 34); // Simplified\n \n return { lift, confidence };\n}\n\n","import React from 'react';\nimport type { ExperimentConfig, Variant } from '../types';\nimport { assignVariant } from '../utils';\n\ninterface ExperimentProps {\n config: ExperimentConfig;\n userId: string;\n onVariantAssigned?: (variant: Variant) => void;\n children?: React.ReactNode;\n}\n\n/**\n * React component for A/B test experiments\n * Tests React/JSX compatibility with bundlers\n */\nexport function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps) {\n const [assignedVariant, setAssignedVariant] = React.useState<Variant | null>(null);\n \n React.useEffect(() => {\n if (config.enabled && config.variants.length > 0) {\n const variant = assignVariant(config.variants, userId);\n setAssignedVariant(variant);\n onVariantAssigned?.(variant);\n }\n }, [config, userId, onVariantAssigned]);\n \n if (!config.enabled || !assignedVariant) {\n return null;\n }\n \n return (\n <div data-experiment-id={config.id} data-variant-id={assignedVariant.id}>\n {children}\n </div>\n );\n}\n\ninterface VariantDisplayProps {\n variantId: string;\n children: React.ReactNode;\n}\n\n/**\n * Conditionally renders content based on variant\n */\nexport function VariantDisplay({ variantId, children }: VariantDisplayProps) {\n return (\n <div data-variant={variantId} style={{ display: 'contents' }}>\n {children}\n </div>\n );\n}\n\n/**\n * Hook for using experiments in functional components\n */\nexport function useExperiment(config: ExperimentConfig, userId: string) {\n const [variant, setVariant] = React.useState<Variant | null>(null);\n const [isLoading, setIsLoading] = React.useState(true);\n \n React.useEffect(() => {\n if (!config.enabled) {\n setIsLoading(false);\n return;\n }\n \n const assigned = assignVariant(config.variants, userId);\n setVariant(assigned);\n setIsLoading(false);\n }, [config, userId]);\n \n return { variant, isLoading };\n}\n\n"],"mappings":"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","names":["index_exports","__export","DEFAULT_CONFIG","Experiment","VERSION","VariantDisplay","assignVariant","calculateRevenueLift","generateExperimentId","hashString","useExperiment","validateConfig","__toCommonJS","assignVariant","variants","userId","totalWeight","sum","v","normalized","hashString","cumulative","variant","str","hash","i","char","validateConfig","config","generateExperimentId","name","timestamp","random","calculateRevenueLift","controlRevenue","variantRevenue","controlSampleSize","variantSampleSize","controlMean","variantMean","lift","standardError","zScore","confidence","import_react","Experiment","config","userId","onVariantAssigned","children","assignedVariant","setAssignedVariant","React","variant","assignVariant","VariantDisplay","variantId","useExperiment","setVariant","isLoading","setIsLoading","assigned","VERSION","DEFAULT_CONFIG"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/utils.ts","../src/components/Experiment.tsx","../src/contexts/ElevateContext.tsx","../src/components/ElevateProvider.tsx"],"sourcesContent":["// Main entry point for the Elevate AB Testing NPM package\nexport type {\n Test,\n Variation,\n TrackingEvent,\n ExperimentStatus,\n ElevateConfig,\n ElevateProviderProps,\n ElevateContextValue,\n BackendConfig,\n} from \"./types\";\n\nexport {\n assignVariant,\n hashString,\n validateConfig,\n generateExperimentId,\n calculateRevenueLift,\n} from \"./utils\";\n\nexport {\n Experiment,\n VariantDisplay,\n useExperiment,\n} from \"./components/Experiment\";\n\nexport { ElevateProvider } from \"./components/ElevateProvider\";\nexport { useElevateConfig } from \"./contexts/ElevateContext\";\n\n// Package version\nexport const VERSION = \"1.1.0\";\n\n// Default configuration\nexport const DEFAULT_CONFIG = {\n enabled: true,\n trackingEndpoint: \"https://analytics.elevateab.com/track\",\n cacheDuration: 3600,\n};\n","import type { Variation, Test, BackendConfig, ElevateConfig } from \"./types\";\n\n/**\n * Assigns a variant based on weighted distribution\n */\nexport function assignVariant(\n variations: Variation[],\n userId: string\n): Variation {\n const totalWeight = variations.reduce((sum, v) => sum + v.weight, 0);\n const hash = hashString(userId);\n const normalized = hash % totalWeight;\n\n let cumulative = 0;\n for (const variation of variations) {\n cumulative += variation.weight;\n if (normalized < cumulative) {\n return variation;\n }\n }\n\n return variations[0];\n}\n\nexport function hashString(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash;\n }\n return Math.abs(hash);\n}\n\nexport function validateConfig(config: Test): boolean {\n if (!config.testId || !config.name) return false;\n if (!config.variations || config.variations.length === 0) return false;\n\n const totalWeight = config.variations.reduce((sum, v) => sum + v.weight, 0);\n return totalWeight === 100;\n}\n\nexport function generateExperimentId(name: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 9);\n const safeName = name.toLowerCase().replace(/[^a-z0-9]/g, \"-\");\n return `exp-${safeName}-${timestamp}-${random}`;\n}\n\nexport function calculateRevenueLift(\n controlRevenue: number,\n variantRevenue: number,\n controlSampleSize: number,\n variantSampleSize: number\n): { lift: number; confidence: number } {\n const controlMean = controlRevenue / controlSampleSize;\n const variantMean = variantRevenue / variantSampleSize;\n\n const lift = ((variantMean - controlMean) / controlMean) * 100;\n\n const pooledStdDev = Math.sqrt(\n (controlRevenue + variantRevenue) / (controlSampleSize + variantSampleSize)\n );\n const standardError =\n pooledStdDev * Math.sqrt(1 / controlSampleSize + 1 / variantSampleSize);\n const zScore = Math.abs(variantMean - controlMean) / standardError;\n const confidence = Math.min(99.9, zScore * 34);\n\n return { lift, confidence };\n}\n\n/**\n * Parse backend config format to SDK normalized format\n */\nexport function parseBackendConfig(backendData: BackendConfig): ElevateConfig {\n const tests: Test[] = [];\n\n for (const [testId, testData] of Object.entries(backendData.allTests)) {\n // Skip if not live\n if (!testData.data.isLive) continue;\n\n // Extract variations (all keys except 'data')\n const variations: Variation[] = [];\n for (const [key, value] of Object.entries(testData)) {\n if (key === \"data\") continue;\n\n const variation = value as BackendConfig[\"allTests\"][string][string];\n if (\n typeof variation === \"object\" &&\n variation !== null &&\n \"variationName\" in variation\n ) {\n variations.push({\n id: key,\n name: variation.variationName,\n weight: variation.trafficPercentage,\n isControl: variation.isControl,\n productId: variation.id,\n handle: variation.handle,\n price: variation.price,\n });\n }\n }\n\n // Only include tests with variations\n if (variations.length > 0) {\n tests.push({\n testId,\n name: testData.data.name,\n enabled: testData.data.isLive,\n type: testData.data.type,\n variations,\n });\n }\n }\n\n return {\n tests,\n selectors: backendData.selectors,\n };\n}\n","import React from \"react\";\nimport type { Variation, ExperimentContextValue } from \"../types\";\nimport { assignVariant } from \"../utils\";\nimport { useElevateConfig } from \"../contexts/ElevateContext\";\n\n// Internal context to pass assigned variant to VariantDisplay components\nconst ExperimentContext = React.createContext<ExperimentContextValue>({\n assignedVariantId: null,\n});\n\ninterface ExperimentProps {\n testId: string;\n userId: string;\n onVariantAssigned?: (variant: Variation) => void;\n children?: React.ReactNode;\n}\n\n/**\n * React component for A/B test experiments\n * Fetches config from ElevateProvider context based on testId\n */\nexport function Experiment({\n testId,\n userId,\n onVariantAssigned,\n children,\n}: ExperimentProps) {\n const { config } = useElevateConfig();\n const [assignedVariant, setAssignedVariant] =\n React.useState<Variation | null>(null);\n\n const testConfig = React.useMemo(() => {\n if (!config) return null;\n return config.tests.find((test) => test.testId === testId) || null;\n }, [config, testId]);\n\n React.useEffect(() => {\n if (testConfig && testConfig.enabled && testConfig.variations.length > 0) {\n const variant = assignVariant(testConfig.variations, userId);\n setAssignedVariant(variant);\n onVariantAssigned?.(variant);\n }\n }, [testConfig, userId, onVariantAssigned]);\n\n if (!testConfig || !testConfig.enabled || !assignedVariant) {\n return null;\n }\n\n return (\n <div\n data-experiment-id={testConfig.testId}\n data-variant-id={assignedVariant.id}\n >\n {children}\n </div>\n );\n}\n\ninterface VariantDisplayProps {\n variantId: string;\n children: React.ReactNode;\n}\n\nexport function VariantDisplay({ variantId, children }: VariantDisplayProps) {\n const { assignedVariantId } = React.useContext(ExperimentContext);\n\n if (!assignedVariantId) {\n return <>{children}</>;\n }\n\n if (assignedVariantId !== variantId) {\n return null;\n }\n\n return <>{children}</>;\n}\n\nexport function useExperiment(testId: string, userId: string) {\n const { config } = useElevateConfig();\n const [variant, setVariant] = React.useState<Variation | null>(null);\n const [isLoading, setIsLoading] = React.useState(true);\n\n const testConfig = React.useMemo(() => {\n if (!config) return null;\n return config.tests.find((test) => test.testId === testId) || null;\n }, [config, testId]);\n\n React.useEffect(() => {\n if (!testConfig) {\n console.error(`[ElevateAB] Test not found: ${testId}`);\n setIsLoading(false);\n return;\n }\n\n if (!testConfig.enabled) {\n console.error(`[ElevateAB] Test disabled: ${testId}`);\n setIsLoading(false);\n return;\n }\n\n const assigned = assignVariant(testConfig.variations, userId);\n setVariant(assigned);\n setIsLoading(false);\n }, [testConfig, userId, testId]);\n\n return { variant, isLoading };\n}\n","import React from \"react\";\nimport type { ElevateContextValue } from \"../types\";\n\nexport const ElevateContext = React.createContext<ElevateContextValue | null>(\n null\n);\n\n/**\n * Hook to access Elevate config from context\n */\nexport function useElevateConfig(): ElevateContextValue {\n const context = React.useContext(ElevateContext);\n\n if (context === null) {\n throw new Error(\"useElevateConfig must be used within ElevateProvider\");\n }\n\n return context;\n}\n","import React from \"react\";\nimport type {\n ElevateConfig,\n ElevateProviderProps,\n BackendConfig,\n} from \"../types\";\nimport { ElevateContext } from \"../contexts/ElevateContext\";\nimport { parseBackendConfig } from \"../utils\";\n\nexport function ElevateProvider({ storeId, children }: ElevateProviderProps) {\n const [config, setConfig] = React.useState<ElevateConfig | null>(null);\n\n React.useEffect(() => {\n async function fetchConfig() {\n try {\n const url = `https://configs.elevateab.com/config/${storeId}.json`;\n const response = await fetch(url);\n\n if (response.status === 404) {\n setConfig({ tests: [], selectors: undefined });\n return;\n }\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch config: ${response.status} ${response.statusText}`\n );\n }\n\n const backendData: BackendConfig = await response.json();\n const parsedConfig = parseBackendConfig(backendData);\n setConfig(parsedConfig);\n } catch (err) {\n console.error(\"[ElevateAB] Failed to load config:\", err);\n setConfig({ tests: [], selectors: undefined });\n }\n }\n\n fetchConfig();\n }, [storeId]);\n\n const value = React.useMemo(() => ({ config }), [config]);\n\n return (\n <ElevateContext.Provider value={value}>{children}</ElevateContext.Provider>\n );\n}\n"],"mappings":"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","names":["index_exports","__export","DEFAULT_CONFIG","ElevateProvider","Experiment","VERSION","VariantDisplay","assignVariant","calculateRevenueLift","generateExperimentId","hashString","useElevateConfig","useExperiment","validateConfig","__toCommonJS","assignVariant","variations","userId","totalWeight","sum","v","normalized","hashString","cumulative","variation","str","hash","i","char","validateConfig","config","generateExperimentId","name","timestamp","random","calculateRevenueLift","controlRevenue","variantRevenue","controlSampleSize","variantSampleSize","controlMean","variantMean","lift","standardError","zScore","confidence","parseBackendConfig","backendData","tests","testId","testData","key","value","import_react","import_react","ElevateContext","React","useElevateConfig","context","ExperimentContext","React","Experiment","testId","userId","onVariantAssigned","children","config","useElevateConfig","assignedVariant","setAssignedVariant","testConfig","test","variant","assignVariant","VariantDisplay","variantId","assignedVariantId","useExperiment","setVariant","isLoading","setIsLoading","assigned","import_react","ElevateProvider","storeId","children","config","setConfig","React","fetchConfig","url","response","backendData","parsedConfig","parseBackendConfig","err","value","ElevateContext","VERSION","DEFAULT_CONFIG"]}
package/dist/index.d.cts CHANGED
@@ -1,15 +1,66 @@
1
1
  import React from 'react';
2
2
 
3
- interface ExperimentConfig {
4
- id: string;
3
+ interface BackendConfig {
4
+ allTests: {
5
+ [testId: string]: BackendTest;
6
+ };
7
+ selectors?: {
8
+ selectorsV2?: unknown[];
9
+ };
10
+ }
11
+ interface BackendTest {
12
+ data: {
13
+ name: string;
14
+ isLive: boolean;
15
+ settings?: Record<string, unknown>;
16
+ type: string;
17
+ filters?: unknown[];
18
+ isPersonalization?: boolean;
19
+ testTrafficPercentage?: number;
20
+ handles?: string[];
21
+ productIds?: number[];
22
+ defaultVariants?: Record<string, number>;
23
+ };
24
+ [variationId: string]: BackendVariation | BackendTest["data"];
25
+ }
26
+ interface BackendVariation {
27
+ variationName: string;
28
+ trafficPercentage: number;
29
+ isDone?: boolean;
30
+ isControl?: boolean;
31
+ id?: number;
32
+ productTitle?: string;
33
+ handle?: string;
34
+ price?: string;
35
+ link?: string;
36
+ productVariants?: unknown[];
37
+ }
38
+ interface Test {
39
+ testId: string;
5
40
  name: string;
6
- variants: Variant[];
7
41
  enabled: boolean;
42
+ type: string;
43
+ variations: Variation[];
8
44
  }
9
- interface Variant {
45
+ interface Variation {
10
46
  id: string;
11
47
  name: string;
12
48
  weight: number;
49
+ isControl?: boolean;
50
+ productId?: number;
51
+ handle?: string;
52
+ price?: string;
53
+ }
54
+ interface ElevateConfig {
55
+ tests: Test[];
56
+ selectors?: unknown;
57
+ }
58
+ interface ElevateProviderProps {
59
+ storeId: string;
60
+ children: React.ReactNode;
61
+ }
62
+ interface ElevateContextValue {
63
+ config: ElevateConfig | null;
13
64
  }
14
65
  interface TrackingEvent {
15
66
  experimentId: string;
@@ -18,64 +69,53 @@ interface TrackingEvent {
18
69
  timestamp: number;
19
70
  metadata?: Record<string, unknown>;
20
71
  }
21
- type ExperimentStatus = 'draft' | 'running' | 'paused' | 'completed';
72
+ type ExperimentStatus = "draft" | "running" | "paused" | "completed";
22
73
 
23
74
  /**
24
75
  * Assigns a variant based on weighted distribution
25
76
  */
26
- declare function assignVariant(variants: Variant[], userId: string): Variant;
27
- /**
28
- * Simple hash function for user ID
29
- */
77
+ declare function assignVariant(variations: Variation[], userId: string): Variation;
30
78
  declare function hashString(str: string): number;
31
- /**
32
- * Validates experiment configuration
33
- */
34
- declare function validateConfig(config: ExperimentConfig): boolean;
35
- /**
36
- * Generates a unique experiment ID
37
- */
79
+ declare function validateConfig(config: Test): boolean;
38
80
  declare function generateExperimentId(name: string): string;
39
- /**
40
- * Sensitive business logic that should be minified/obfuscated
41
- */
42
81
  declare function calculateRevenueLift(controlRevenue: number, variantRevenue: number, controlSampleSize: number, variantSampleSize: number): {
43
82
  lift: number;
44
83
  confidence: number;
45
84
  };
46
85
 
47
86
  interface ExperimentProps {
48
- config: ExperimentConfig;
87
+ testId: string;
49
88
  userId: string;
50
- onVariantAssigned?: (variant: Variant) => void;
89
+ onVariantAssigned?: (variant: Variation) => void;
51
90
  children?: React.ReactNode;
52
91
  }
53
92
  /**
54
93
  * React component for A/B test experiments
55
- * Tests React/JSX compatibility with bundlers
94
+ * Fetches config from ElevateProvider context based on testId
56
95
  */
57
- declare function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps): React.JSX.Element | null;
96
+ declare function Experiment({ testId, userId, onVariantAssigned, children, }: ExperimentProps): React.JSX.Element | null;
58
97
  interface VariantDisplayProps {
59
98
  variantId: string;
60
99
  children: React.ReactNode;
61
100
  }
62
- /**
63
- * Conditionally renders content based on variant
64
- */
65
- declare function VariantDisplay({ variantId, children }: VariantDisplayProps): React.JSX.Element;
66
- /**
67
- * Hook for using experiments in functional components
68
- */
69
- declare function useExperiment(config: ExperimentConfig, userId: string): {
70
- variant: Variant | null;
101
+ declare function VariantDisplay({ variantId, children }: VariantDisplayProps): React.JSX.Element | null;
102
+ declare function useExperiment(testId: string, userId: string): {
103
+ variant: Variation | null;
71
104
  isLoading: boolean;
72
105
  };
73
106
 
74
- declare const VERSION = "1.0.0";
107
+ declare function ElevateProvider({ storeId, children }: ElevateProviderProps): React.JSX.Element;
108
+
109
+ /**
110
+ * Hook to access Elevate config from context
111
+ */
112
+ declare function useElevateConfig(): ElevateContextValue;
113
+
114
+ declare const VERSION = "1.1.0";
75
115
  declare const DEFAULT_CONFIG: {
76
116
  enabled: boolean;
77
117
  trackingEndpoint: string;
78
118
  cacheDuration: number;
79
119
  };
80
120
 
81
- export { DEFAULT_CONFIG, Experiment, type ExperimentConfig, type ExperimentStatus, type TrackingEvent, VERSION, type Variant, VariantDisplay, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useExperiment, validateConfig };
121
+ export { type BackendConfig, DEFAULT_CONFIG, type ElevateConfig, type ElevateContextValue, ElevateProvider, type ElevateProviderProps, Experiment, type ExperimentStatus, type Test, type TrackingEvent, VERSION, VariantDisplay, type Variation, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useElevateConfig, useExperiment, validateConfig };
package/dist/index.d.ts CHANGED
@@ -1,15 +1,66 @@
1
1
  import React from 'react';
2
2
 
3
- interface ExperimentConfig {
4
- id: string;
3
+ interface BackendConfig {
4
+ allTests: {
5
+ [testId: string]: BackendTest;
6
+ };
7
+ selectors?: {
8
+ selectorsV2?: unknown[];
9
+ };
10
+ }
11
+ interface BackendTest {
12
+ data: {
13
+ name: string;
14
+ isLive: boolean;
15
+ settings?: Record<string, unknown>;
16
+ type: string;
17
+ filters?: unknown[];
18
+ isPersonalization?: boolean;
19
+ testTrafficPercentage?: number;
20
+ handles?: string[];
21
+ productIds?: number[];
22
+ defaultVariants?: Record<string, number>;
23
+ };
24
+ [variationId: string]: BackendVariation | BackendTest["data"];
25
+ }
26
+ interface BackendVariation {
27
+ variationName: string;
28
+ trafficPercentage: number;
29
+ isDone?: boolean;
30
+ isControl?: boolean;
31
+ id?: number;
32
+ productTitle?: string;
33
+ handle?: string;
34
+ price?: string;
35
+ link?: string;
36
+ productVariants?: unknown[];
37
+ }
38
+ interface Test {
39
+ testId: string;
5
40
  name: string;
6
- variants: Variant[];
7
41
  enabled: boolean;
42
+ type: string;
43
+ variations: Variation[];
8
44
  }
9
- interface Variant {
45
+ interface Variation {
10
46
  id: string;
11
47
  name: string;
12
48
  weight: number;
49
+ isControl?: boolean;
50
+ productId?: number;
51
+ handle?: string;
52
+ price?: string;
53
+ }
54
+ interface ElevateConfig {
55
+ tests: Test[];
56
+ selectors?: unknown;
57
+ }
58
+ interface ElevateProviderProps {
59
+ storeId: string;
60
+ children: React.ReactNode;
61
+ }
62
+ interface ElevateContextValue {
63
+ config: ElevateConfig | null;
13
64
  }
14
65
  interface TrackingEvent {
15
66
  experimentId: string;
@@ -18,64 +69,53 @@ interface TrackingEvent {
18
69
  timestamp: number;
19
70
  metadata?: Record<string, unknown>;
20
71
  }
21
- type ExperimentStatus = 'draft' | 'running' | 'paused' | 'completed';
72
+ type ExperimentStatus = "draft" | "running" | "paused" | "completed";
22
73
 
23
74
  /**
24
75
  * Assigns a variant based on weighted distribution
25
76
  */
26
- declare function assignVariant(variants: Variant[], userId: string): Variant;
27
- /**
28
- * Simple hash function for user ID
29
- */
77
+ declare function assignVariant(variations: Variation[], userId: string): Variation;
30
78
  declare function hashString(str: string): number;
31
- /**
32
- * Validates experiment configuration
33
- */
34
- declare function validateConfig(config: ExperimentConfig): boolean;
35
- /**
36
- * Generates a unique experiment ID
37
- */
79
+ declare function validateConfig(config: Test): boolean;
38
80
  declare function generateExperimentId(name: string): string;
39
- /**
40
- * Sensitive business logic that should be minified/obfuscated
41
- */
42
81
  declare function calculateRevenueLift(controlRevenue: number, variantRevenue: number, controlSampleSize: number, variantSampleSize: number): {
43
82
  lift: number;
44
83
  confidence: number;
45
84
  };
46
85
 
47
86
  interface ExperimentProps {
48
- config: ExperimentConfig;
87
+ testId: string;
49
88
  userId: string;
50
- onVariantAssigned?: (variant: Variant) => void;
89
+ onVariantAssigned?: (variant: Variation) => void;
51
90
  children?: React.ReactNode;
52
91
  }
53
92
  /**
54
93
  * React component for A/B test experiments
55
- * Tests React/JSX compatibility with bundlers
94
+ * Fetches config from ElevateProvider context based on testId
56
95
  */
57
- declare function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps): React.JSX.Element | null;
96
+ declare function Experiment({ testId, userId, onVariantAssigned, children, }: ExperimentProps): React.JSX.Element | null;
58
97
  interface VariantDisplayProps {
59
98
  variantId: string;
60
99
  children: React.ReactNode;
61
100
  }
62
- /**
63
- * Conditionally renders content based on variant
64
- */
65
- declare function VariantDisplay({ variantId, children }: VariantDisplayProps): React.JSX.Element;
66
- /**
67
- * Hook for using experiments in functional components
68
- */
69
- declare function useExperiment(config: ExperimentConfig, userId: string): {
70
- variant: Variant | null;
101
+ declare function VariantDisplay({ variantId, children }: VariantDisplayProps): React.JSX.Element | null;
102
+ declare function useExperiment(testId: string, userId: string): {
103
+ variant: Variation | null;
71
104
  isLoading: boolean;
72
105
  };
73
106
 
74
- declare const VERSION = "1.0.0";
107
+ declare function ElevateProvider({ storeId, children }: ElevateProviderProps): React.JSX.Element;
108
+
109
+ /**
110
+ * Hook to access Elevate config from context
111
+ */
112
+ declare function useElevateConfig(): ElevateContextValue;
113
+
114
+ declare const VERSION = "1.1.0";
75
115
  declare const DEFAULT_CONFIG: {
76
116
  enabled: boolean;
77
117
  trackingEndpoint: string;
78
118
  cacheDuration: number;
79
119
  };
80
120
 
81
- export { DEFAULT_CONFIG, Experiment, type ExperimentConfig, type ExperimentStatus, type TrackingEvent, VERSION, type Variant, VariantDisplay, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useExperiment, validateConfig };
121
+ export { type BackendConfig, DEFAULT_CONFIG, type ElevateConfig, type ElevateContextValue, ElevateProvider, type ElevateProviderProps, Experiment, type ExperimentStatus, type Test, type TrackingEvent, VERSION, VariantDisplay, type Variation, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useElevateConfig, useExperiment, validateConfig };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- function c(t,e){let n=t.reduce((r,p)=>r+p.weight,0),i=u(e)%n,s=0;for(let r of t)if(s+=r.weight,i<s)return r;return t[0]}function u(t){let e=0;for(let n=0;n<t.length;n++){let a=t.charCodeAt(n);e=(e<<5)-e+a,e=e&e}return Math.abs(e)}function f(t){return!t.id||!t.name||!t.variants||t.variants.length===0?!1:t.variants.reduce((n,a)=>n+a.weight,0)===100}function g(t){let e=Date.now(),n=Math.random().toString(36).substring(2,9);return`exp-${t.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${e}-${n}`}function h(t,e,n,a){let i=t/n,s=e/a,r=(s-i)/i*100,d=Math.sqrt((t+e)/(n+a))*Math.sqrt(1/n+1/a),l=Math.abs(s-i)/d,m=Math.min(99.9,l*34);return{lift:r,confidence:m}}import o from"react";function x({config:t,userId:e,onVariantAssigned:n,children:a}){let[i,s]=o.useState(null);return o.useEffect(()=>{if(t.enabled&&t.variants.length>0){let r=c(t.variants,e);s(r),n?.(r)}},[t,e,n]),!t.enabled||!i?null:o.createElement("div",{"data-experiment-id":t.id,"data-variant-id":i.id},a)}function E({variantId:t,children:e}){return o.createElement("div",{"data-variant":t,style:{display:"contents"}},e)}function v(t,e){let[n,a]=o.useState(null),[i,s]=o.useState(!0);return o.useEffect(()=>{if(!t.enabled){s(!1);return}let r=c(t.variants,e);a(r),s(!1)},[t,e]),{variant:n,isLoading:i}}var D="1.0.0",M={enabled:!0,trackingEndpoint:"https://analytics.elevateab.com/track",cacheDuration:3600};export{M as DEFAULT_CONFIG,x as Experiment,D as VERSION,E as VariantDisplay,c as assignVariant,h as calculateRevenueLift,g as generateExperimentId,u as hashString,v as useExperiment,f as validateConfig};
1
+ function f(t,n){let r=t.reduce((a,e)=>a+e.weight,0),i=m(n)%r,s=0;for(let a of t)if(s+=a.weight,i<s)return a;return t[0]}function m(t){let n=0;for(let r=0;r<t.length;r++){let o=t.charCodeAt(r);n=(n<<5)-n+o,n=n&n}return Math.abs(n)}function h(t){return!t.testId||!t.name||!t.variations||t.variations.length===0?!1:t.variations.reduce((r,o)=>r+o.weight,0)===100}function C(t){let n=Date.now(),r=Math.random().toString(36).substring(2,9);return`exp-${t.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${n}-${r}`}function V(t,n,r,o){let i=t/r,s=n/o,a=(s-i)/i*100,c=Math.sqrt((t+n)/(r+o))*Math.sqrt(1/r+1/o),p=Math.abs(s-i)/c,E=Math.min(99.9,p*34);return{lift:a,confidence:E}}function v(t){let n=[];for(let[r,o]of Object.entries(t.allTests)){if(!o.data.isLive)continue;let i=[];for(let[s,a]of Object.entries(o)){if(s==="data")continue;let e=a;typeof e=="object"&&e!==null&&"variationName"in e&&i.push({id:s,name:e.variationName,weight:e.trafficPercentage,isControl:e.isControl,productId:e.id,handle:e.handle,price:e.price})}i.length>0&&n.push({testId:r,name:o.data.name,enabled:o.data.isLive,type:o.data.type,variations:i})}return{tests:n,selectors:t.selectors}}import l from"react";import x from"react";var g=x.createContext(null);function u(){let t=x.useContext(g);if(t===null)throw new Error("useElevateConfig must be used within ElevateProvider");return t}var b=l.createContext({assignedVariantId:null});function y({testId:t,userId:n,onVariantAssigned:r,children:o}){let{config:i}=u(),[s,a]=l.useState(null),e=l.useMemo(()=>i&&i.tests.find(c=>c.testId===t)||null,[i,t]);return l.useEffect(()=>{if(e&&e.enabled&&e.variations.length>0){let c=f(e.variations,n);a(c),r?.(c)}},[e,n,r]),!e||!e.enabled||!s?null:l.createElement("div",{"data-experiment-id":e.testId,"data-variant-id":s.id},o)}function P({variantId:t,children:n}){let{assignedVariantId:r}=l.useContext(b);return r?r!==t?null:l.createElement(l.Fragment,null,n):l.createElement(l.Fragment,null,n)}function k(t,n){let{config:r}=u(),[o,i]=l.useState(null),[s,a]=l.useState(!0),e=l.useMemo(()=>r&&r.tests.find(c=>c.testId===t)||null,[r,t]);return l.useEffect(()=>{if(!e){console.error(`[ElevateAB] Test not found: ${t}`),a(!1);return}if(!e.enabled){console.error(`[ElevateAB] Test disabled: ${t}`),a(!1);return}let c=f(e.variations,n);i(c),a(!1)},[e,n,t]),{variant:o,isLoading:s}}import d from"react";function w({storeId:t,children:n}){let[r,o]=d.useState(null);d.useEffect(()=>{async function s(){try{let a=`https://configs.elevateab.com/config/${t}.json`,e=await fetch(a);if(e.status===404){o({tests:[],selectors:void 0});return}if(!e.ok)throw new Error(`Failed to fetch config: ${e.status} ${e.statusText}`);let c=await e.json(),p=v(c);o(p)}catch(a){console.error("[ElevateAB] Failed to load config:",a),o({tests:[],selectors:void 0})}}s()},[t]);let i=d.useMemo(()=>({config:r}),[r]);return d.createElement(g.Provider,{value:i},n)}var O="1.1.0",S={enabled:!0,trackingEndpoint:"https://analytics.elevateab.com/track",cacheDuration:3600};export{S as DEFAULT_CONFIG,w as ElevateProvider,y as Experiment,O as VERSION,P as VariantDisplay,f as assignVariant,V as calculateRevenueLift,C as generateExperimentId,m as hashString,u as useElevateConfig,k as useExperiment,h as validateConfig};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils.ts","../src/components/Experiment.tsx","../src/index.ts"],"sourcesContent":["import type { Variant, ExperimentConfig } from './types';\n\n/**\n * Assigns a variant based on weighted distribution\n */\nexport function assignVariant(variants: Variant[], userId: string): Variant {\n const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);\n const hash = hashString(userId);\n const normalized = hash % totalWeight;\n \n let cumulative = 0;\n for (const variant of variants) {\n cumulative += variant.weight;\n if (normalized < cumulative) {\n return variant;\n }\n }\n \n return variants[0];\n}\n\n/**\n * Simple hash function for user ID\n */\nexport function hashString(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return Math.abs(hash);\n}\n\n/**\n * Validates experiment configuration\n */\nexport function validateConfig(config: ExperimentConfig): boolean {\n if (!config.id || !config.name) return false;\n if (!config.variants || config.variants.length === 0) return false;\n \n const totalWeight = config.variants.reduce((sum, v) => sum + v.weight, 0);\n return totalWeight === 100;\n}\n\n/**\n * Generates a unique experiment ID\n */\nexport function generateExperimentId(name: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 9);\n const safeName = name.toLowerCase().replace(/[^a-z0-9]/g, '-');\n return `exp-${safeName}-${timestamp}-${random}`;\n}\n\n/**\n * Sensitive business logic that should be minified/obfuscated\n */\nexport function calculateRevenueLift(\n controlRevenue: number,\n variantRevenue: number,\n controlSampleSize: number,\n variantSampleSize: number\n): { lift: number; confidence: number } {\n const controlMean = controlRevenue / controlSampleSize;\n const variantMean = variantRevenue / variantSampleSize;\n \n const lift = ((variantMean - controlMean) / controlMean) * 100;\n \n // Simplified confidence calculation\n const pooledStdDev = Math.sqrt(\n (controlRevenue + variantRevenue) / (controlSampleSize + variantSampleSize)\n );\n const standardError = pooledStdDev * Math.sqrt(\n 1 / controlSampleSize + 1 / variantSampleSize\n );\n const zScore = Math.abs(variantMean - controlMean) / standardError;\n const confidence = Math.min(99.9, zScore * 34); // Simplified\n \n return { lift, confidence };\n}\n\n","import React from 'react';\nimport type { ExperimentConfig, Variant } from '../types';\nimport { assignVariant } from '../utils';\n\ninterface ExperimentProps {\n config: ExperimentConfig;\n userId: string;\n onVariantAssigned?: (variant: Variant) => void;\n children?: React.ReactNode;\n}\n\n/**\n * React component for A/B test experiments\n * Tests React/JSX compatibility with bundlers\n */\nexport function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps) {\n const [assignedVariant, setAssignedVariant] = React.useState<Variant | null>(null);\n \n React.useEffect(() => {\n if (config.enabled && config.variants.length > 0) {\n const variant = assignVariant(config.variants, userId);\n setAssignedVariant(variant);\n onVariantAssigned?.(variant);\n }\n }, [config, userId, onVariantAssigned]);\n \n if (!config.enabled || !assignedVariant) {\n return null;\n }\n \n return (\n <div data-experiment-id={config.id} data-variant-id={assignedVariant.id}>\n {children}\n </div>\n );\n}\n\ninterface VariantDisplayProps {\n variantId: string;\n children: React.ReactNode;\n}\n\n/**\n * Conditionally renders content based on variant\n */\nexport function VariantDisplay({ variantId, children }: VariantDisplayProps) {\n return (\n <div data-variant={variantId} style={{ display: 'contents' }}>\n {children}\n </div>\n );\n}\n\n/**\n * Hook for using experiments in functional components\n */\nexport function useExperiment(config: ExperimentConfig, userId: string) {\n const [variant, setVariant] = React.useState<Variant | null>(null);\n const [isLoading, setIsLoading] = React.useState(true);\n \n React.useEffect(() => {\n if (!config.enabled) {\n setIsLoading(false);\n return;\n }\n \n const assigned = assignVariant(config.variants, userId);\n setVariant(assigned);\n setIsLoading(false);\n }, [config, userId]);\n \n return { variant, isLoading };\n}\n\n","// Main entry point for the Elevate AB Testing NPM package\nexport type {\n ExperimentConfig,\n Variant,\n TrackingEvent,\n ExperimentStatus,\n} from './types';\n\nexport {\n assignVariant,\n hashString,\n validateConfig,\n generateExperimentId,\n calculateRevenueLift,\n} from './utils';\n\nexport {\n Experiment,\n VariantDisplay,\n useExperiment,\n} from './components/Experiment';\n\n// Package version\nexport const VERSION = '1.0.0';\n\n// Default configuration\nexport const DEFAULT_CONFIG = {\n enabled: true,\n trackingEndpoint: 'https://analytics.elevateab.com/track',\n cacheDuration: 3600,\n};\n\n"],"mappings":"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","names":["assignVariant","variants","userId","totalWeight","sum","v","normalized","hashString","cumulative","variant","str","hash","i","char","validateConfig","config","generateExperimentId","name","timestamp","random","calculateRevenueLift","controlRevenue","variantRevenue","controlSampleSize","variantSampleSize","controlMean","variantMean","lift","standardError","zScore","confidence","React","Experiment","config","userId","onVariantAssigned","children","assignedVariant","setAssignedVariant","React","variant","assignVariant","VariantDisplay","variantId","useExperiment","setVariant","isLoading","setIsLoading","assigned","VERSION","DEFAULT_CONFIG"]}
1
+ {"version":3,"sources":["../src/utils.ts","../src/components/Experiment.tsx","../src/contexts/ElevateContext.tsx","../src/components/ElevateProvider.tsx","../src/index.ts"],"sourcesContent":["import type { Variation, Test, BackendConfig, ElevateConfig } from \"./types\";\n\n/**\n * Assigns a variant based on weighted distribution\n */\nexport function assignVariant(\n variations: Variation[],\n userId: string\n): Variation {\n const totalWeight = variations.reduce((sum, v) => sum + v.weight, 0);\n const hash = hashString(userId);\n const normalized = hash % totalWeight;\n\n let cumulative = 0;\n for (const variation of variations) {\n cumulative += variation.weight;\n if (normalized < cumulative) {\n return variation;\n }\n }\n\n return variations[0];\n}\n\nexport function hashString(str: string): number {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash;\n }\n return Math.abs(hash);\n}\n\nexport function validateConfig(config: Test): boolean {\n if (!config.testId || !config.name) return false;\n if (!config.variations || config.variations.length === 0) return false;\n\n const totalWeight = config.variations.reduce((sum, v) => sum + v.weight, 0);\n return totalWeight === 100;\n}\n\nexport function generateExperimentId(name: string): string {\n const timestamp = Date.now();\n const random = Math.random().toString(36).substring(2, 9);\n const safeName = name.toLowerCase().replace(/[^a-z0-9]/g, \"-\");\n return `exp-${safeName}-${timestamp}-${random}`;\n}\n\nexport function calculateRevenueLift(\n controlRevenue: number,\n variantRevenue: number,\n controlSampleSize: number,\n variantSampleSize: number\n): { lift: number; confidence: number } {\n const controlMean = controlRevenue / controlSampleSize;\n const variantMean = variantRevenue / variantSampleSize;\n\n const lift = ((variantMean - controlMean) / controlMean) * 100;\n\n const pooledStdDev = Math.sqrt(\n (controlRevenue + variantRevenue) / (controlSampleSize + variantSampleSize)\n );\n const standardError =\n pooledStdDev * Math.sqrt(1 / controlSampleSize + 1 / variantSampleSize);\n const zScore = Math.abs(variantMean - controlMean) / standardError;\n const confidence = Math.min(99.9, zScore * 34);\n\n return { lift, confidence };\n}\n\n/**\n * Parse backend config format to SDK normalized format\n */\nexport function parseBackendConfig(backendData: BackendConfig): ElevateConfig {\n const tests: Test[] = [];\n\n for (const [testId, testData] of Object.entries(backendData.allTests)) {\n // Skip if not live\n if (!testData.data.isLive) continue;\n\n // Extract variations (all keys except 'data')\n const variations: Variation[] = [];\n for (const [key, value] of Object.entries(testData)) {\n if (key === \"data\") continue;\n\n const variation = value as BackendConfig[\"allTests\"][string][string];\n if (\n typeof variation === \"object\" &&\n variation !== null &&\n \"variationName\" in variation\n ) {\n variations.push({\n id: key,\n name: variation.variationName,\n weight: variation.trafficPercentage,\n isControl: variation.isControl,\n productId: variation.id,\n handle: variation.handle,\n price: variation.price,\n });\n }\n }\n\n // Only include tests with variations\n if (variations.length > 0) {\n tests.push({\n testId,\n name: testData.data.name,\n enabled: testData.data.isLive,\n type: testData.data.type,\n variations,\n });\n }\n }\n\n return {\n tests,\n selectors: backendData.selectors,\n };\n}\n","import React from \"react\";\nimport type { Variation, ExperimentContextValue } from \"../types\";\nimport { assignVariant } from \"../utils\";\nimport { useElevateConfig } from \"../contexts/ElevateContext\";\n\n// Internal context to pass assigned variant to VariantDisplay components\nconst ExperimentContext = React.createContext<ExperimentContextValue>({\n assignedVariantId: null,\n});\n\ninterface ExperimentProps {\n testId: string;\n userId: string;\n onVariantAssigned?: (variant: Variation) => void;\n children?: React.ReactNode;\n}\n\n/**\n * React component for A/B test experiments\n * Fetches config from ElevateProvider context based on testId\n */\nexport function Experiment({\n testId,\n userId,\n onVariantAssigned,\n children,\n}: ExperimentProps) {\n const { config } = useElevateConfig();\n const [assignedVariant, setAssignedVariant] =\n React.useState<Variation | null>(null);\n\n const testConfig = React.useMemo(() => {\n if (!config) return null;\n return config.tests.find((test) => test.testId === testId) || null;\n }, [config, testId]);\n\n React.useEffect(() => {\n if (testConfig && testConfig.enabled && testConfig.variations.length > 0) {\n const variant = assignVariant(testConfig.variations, userId);\n setAssignedVariant(variant);\n onVariantAssigned?.(variant);\n }\n }, [testConfig, userId, onVariantAssigned]);\n\n if (!testConfig || !testConfig.enabled || !assignedVariant) {\n return null;\n }\n\n return (\n <div\n data-experiment-id={testConfig.testId}\n data-variant-id={assignedVariant.id}\n >\n {children}\n </div>\n );\n}\n\ninterface VariantDisplayProps {\n variantId: string;\n children: React.ReactNode;\n}\n\nexport function VariantDisplay({ variantId, children }: VariantDisplayProps) {\n const { assignedVariantId } = React.useContext(ExperimentContext);\n\n if (!assignedVariantId) {\n return <>{children}</>;\n }\n\n if (assignedVariantId !== variantId) {\n return null;\n }\n\n return <>{children}</>;\n}\n\nexport function useExperiment(testId: string, userId: string) {\n const { config } = useElevateConfig();\n const [variant, setVariant] = React.useState<Variation | null>(null);\n const [isLoading, setIsLoading] = React.useState(true);\n\n const testConfig = React.useMemo(() => {\n if (!config) return null;\n return config.tests.find((test) => test.testId === testId) || null;\n }, [config, testId]);\n\n React.useEffect(() => {\n if (!testConfig) {\n console.error(`[ElevateAB] Test not found: ${testId}`);\n setIsLoading(false);\n return;\n }\n\n if (!testConfig.enabled) {\n console.error(`[ElevateAB] Test disabled: ${testId}`);\n setIsLoading(false);\n return;\n }\n\n const assigned = assignVariant(testConfig.variations, userId);\n setVariant(assigned);\n setIsLoading(false);\n }, [testConfig, userId, testId]);\n\n return { variant, isLoading };\n}\n","import React from \"react\";\nimport type { ElevateContextValue } from \"../types\";\n\nexport const ElevateContext = React.createContext<ElevateContextValue | null>(\n null\n);\n\n/**\n * Hook to access Elevate config from context\n */\nexport function useElevateConfig(): ElevateContextValue {\n const context = React.useContext(ElevateContext);\n\n if (context === null) {\n throw new Error(\"useElevateConfig must be used within ElevateProvider\");\n }\n\n return context;\n}\n","import React from \"react\";\nimport type {\n ElevateConfig,\n ElevateProviderProps,\n BackendConfig,\n} from \"../types\";\nimport { ElevateContext } from \"../contexts/ElevateContext\";\nimport { parseBackendConfig } from \"../utils\";\n\nexport function ElevateProvider({ storeId, children }: ElevateProviderProps) {\n const [config, setConfig] = React.useState<ElevateConfig | null>(null);\n\n React.useEffect(() => {\n async function fetchConfig() {\n try {\n const url = `https://configs.elevateab.com/config/${storeId}.json`;\n const response = await fetch(url);\n\n if (response.status === 404) {\n setConfig({ tests: [], selectors: undefined });\n return;\n }\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch config: ${response.status} ${response.statusText}`\n );\n }\n\n const backendData: BackendConfig = await response.json();\n const parsedConfig = parseBackendConfig(backendData);\n setConfig(parsedConfig);\n } catch (err) {\n console.error(\"[ElevateAB] Failed to load config:\", err);\n setConfig({ tests: [], selectors: undefined });\n }\n }\n\n fetchConfig();\n }, [storeId]);\n\n const value = React.useMemo(() => ({ config }), [config]);\n\n return (\n <ElevateContext.Provider value={value}>{children}</ElevateContext.Provider>\n );\n}\n","// Main entry point for the Elevate AB Testing NPM package\nexport type {\n Test,\n Variation,\n TrackingEvent,\n ExperimentStatus,\n ElevateConfig,\n ElevateProviderProps,\n ElevateContextValue,\n BackendConfig,\n} from \"./types\";\n\nexport {\n assignVariant,\n hashString,\n validateConfig,\n generateExperimentId,\n calculateRevenueLift,\n} from \"./utils\";\n\nexport {\n Experiment,\n VariantDisplay,\n useExperiment,\n} from \"./components/Experiment\";\n\nexport { ElevateProvider } from \"./components/ElevateProvider\";\nexport { useElevateConfig } from \"./contexts/ElevateContext\";\n\n// Package version\nexport const VERSION = \"1.1.0\";\n\n// Default configuration\nexport const DEFAULT_CONFIG = {\n enabled: true,\n trackingEndpoint: \"https://analytics.elevateab.com/track\",\n cacheDuration: 3600,\n};\n"],"mappings":"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","names":["assignVariant","variations","userId","totalWeight","sum","v","normalized","hashString","cumulative","variation","str","hash","i","char","validateConfig","config","generateExperimentId","name","timestamp","random","calculateRevenueLift","controlRevenue","variantRevenue","controlSampleSize","variantSampleSize","controlMean","variantMean","lift","standardError","zScore","confidence","parseBackendConfig","backendData","tests","testId","testData","key","value","React","React","ElevateContext","useElevateConfig","context","ExperimentContext","React","Experiment","testId","userId","onVariantAssigned","children","config","useElevateConfig","assignedVariant","setAssignedVariant","testConfig","test","variant","assignVariant","VariantDisplay","variantId","assignedVariantId","useExperiment","setVariant","isLoading","setIsLoading","assigned","React","ElevateProvider","storeId","children","config","setConfig","React","fetchConfig","url","response","backendData","parsedConfig","parseBackendConfig","err","value","ElevateContext","VERSION","DEFAULT_CONFIG"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevateab/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Elevate AB Testing SDK for Hydrogen and Remix frameworks",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",