@elevateab/sdk 1.0.1 → 1.1.1
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 +52 -44
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -34
- package/dist/index.d.ts +74 -34
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,61 +8,68 @@ Elevate AB Testing SDK for Hydrogen and Remix frameworks.
|
|
|
8
8
|
npm install @elevateab/sdk
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
**Latest version:** 1.0.0
|
|
12
|
-
|
|
13
11
|
## Usage
|
|
14
12
|
|
|
15
|
-
###
|
|
13
|
+
### Automatic Config Loading
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
import { useExperiment, ExperimentConfig } from '@elevateab/sdk';
|
|
19
|
-
|
|
20
|
-
const config: ExperimentConfig = {
|
|
21
|
-
id: 'price-test-001',
|
|
22
|
-
name: 'Pricing Experiment',
|
|
23
|
-
enabled: true,
|
|
24
|
-
variants: [
|
|
25
|
-
{ id: 'control', name: 'Control', weight: 50 },
|
|
26
|
-
{ id: 'variant-a', name: 'Variant A', weight: 50 },
|
|
27
|
-
],
|
|
28
|
-
};
|
|
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:
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
```typescript
|
|
18
|
+
import { ElevateProvider, Experiment, VariantDisplay } from "@elevateab/sdk";
|
|
32
19
|
|
|
33
|
-
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<ElevateProvider storeId="mystore.myshopify.com">
|
|
23
|
+
<ProductPage />
|
|
24
|
+
</ElevateProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
34
27
|
|
|
28
|
+
function ProductPage() {
|
|
35
29
|
return (
|
|
36
30
|
<div>
|
|
37
|
-
{variant
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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>
|
|
42
50
|
</div>
|
|
43
51
|
);
|
|
44
52
|
}
|
|
45
53
|
```
|
|
46
54
|
|
|
47
|
-
### Using the
|
|
55
|
+
### Using the Hook
|
|
48
56
|
|
|
49
57
|
```typescript
|
|
50
|
-
import {
|
|
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>;
|
|
51
64
|
|
|
52
|
-
function ProductPage() {
|
|
53
65
|
return (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</VariantDisplay>
|
|
62
|
-
<VariantDisplay variantId="variant-a">
|
|
63
|
-
<h1>New Headline</h1>
|
|
64
|
-
</VariantDisplay>
|
|
65
|
-
</Experiment>
|
|
66
|
+
<div>
|
|
67
|
+
{variant?.id === "control" ? (
|
|
68
|
+
<Price amount={99.99} />
|
|
69
|
+
) : (
|
|
70
|
+
<Price amount={89.99} />
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
66
73
|
);
|
|
67
74
|
}
|
|
68
75
|
```
|
|
@@ -75,23 +82,23 @@ import {
|
|
|
75
82
|
validateConfig,
|
|
76
83
|
generateExperimentId,
|
|
77
84
|
calculateRevenueLift,
|
|
78
|
-
} from
|
|
85
|
+
} from "@elevateab/sdk";
|
|
79
86
|
|
|
80
87
|
// Assign a variant based on weighted distribution
|
|
81
|
-
const variant = assignVariant(config.variants,
|
|
88
|
+
const variant = assignVariant(config.variants, "user-123");
|
|
82
89
|
|
|
83
90
|
// Validate experiment configuration
|
|
84
91
|
const isValid = validateConfig(config);
|
|
85
92
|
|
|
86
93
|
// Generate unique experiment ID
|
|
87
|
-
const expId = generateExperimentId(
|
|
94
|
+
const expId = generateExperimentId("My Test");
|
|
88
95
|
|
|
89
96
|
// Calculate revenue lift
|
|
90
97
|
const { lift, confidence } = calculateRevenueLift(
|
|
91
98
|
10000, // control revenue
|
|
92
99
|
12000, // variant revenue
|
|
93
|
-
500,
|
|
94
|
-
500
|
|
100
|
+
500, // control sample size
|
|
101
|
+
500 // variant sample size
|
|
95
102
|
);
|
|
96
103
|
```
|
|
97
104
|
|
|
@@ -124,9 +131,11 @@ const { lift, confidence } = calculateRevenueLift(
|
|
|
124
131
|
|
|
125
132
|
### Components
|
|
126
133
|
|
|
134
|
+
- `ElevateProvider`: Provider component that fetches configs from CDN
|
|
127
135
|
- `Experiment`: Main experiment wrapper component
|
|
128
136
|
- `VariantDisplay`: Conditional rendering based on variant
|
|
129
137
|
- `useExperiment`: React hook for experiment logic
|
|
138
|
+
- `useElevateConfig`: Hook to access config from context
|
|
130
139
|
|
|
131
140
|
### Utilities
|
|
132
141
|
|
|
@@ -143,4 +152,3 @@ MIT
|
|
|
143
152
|
## Support
|
|
144
153
|
|
|
145
154
|
For issues and questions, please visit [GitHub Issues](https://github.com/elevateab/sdk/issues).
|
|
146
|
-
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
4
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
87
|
+
testId: string;
|
|
49
88
|
userId: string;
|
|
50
|
-
onVariantAssigned?: (variant:
|
|
89
|
+
onVariantAssigned?: (variant: Variation) => void;
|
|
51
90
|
children?: React.ReactNode;
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
93
|
* React component for A/B test experiments
|
|
55
|
-
*
|
|
94
|
+
* Fetches config from ElevateProvider context based on testId
|
|
56
95
|
*/
|
|
57
|
-
declare function Experiment({
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
4
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
87
|
+
testId: string;
|
|
49
88
|
userId: string;
|
|
50
|
-
onVariantAssigned?: (variant:
|
|
89
|
+
onVariantAssigned?: (variant: Variation) => void;
|
|
51
90
|
children?: React.ReactNode;
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
93
|
* React component for A/B test experiments
|
|
55
|
-
*
|
|
94
|
+
* Fetches config from ElevateProvider context based on testId
|
|
56
95
|
*/
|
|
57
|
-
declare function Experiment({
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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":"AAKO,SAASA,EAAcC,EAAqBC,EAAyB,CAC1E,IAAMC,EAAcF,EAAS,OAAO,CAACG,EAAKC,IAAMD,EAAMC,EAAE,OAAQ,CAAC,EAE3DC,EADOC,EAAWL,CAAM,EACJC,EAEtBK,EAAa,EACjB,QAAWC,KAAWR,EAEpB,GADAO,GAAcC,EAAQ,OAClBH,EAAaE,EACf,OAAOC,EAIX,OAAOR,EAAS,CAAC,CACnB,CAKO,SAASM,EAAWG,EAAqB,CAC9C,IAAIC,EAAO,EACX,QAASC,EAAI,EAAGA,EAAIF,EAAI,OAAQE,IAAK,CACnC,IAAMC,EAAOH,EAAI,WAAWE,CAAC,EAC7BD,GAASA,GAAQ,GAAKA,EAAQE,EAC9BF,EAAOA,EAAOA,CAChB,CACA,OAAO,KAAK,IAAIA,CAAI,CACtB,CAKO,SAASG,EAAeC,EAAmC,CAEhE,MADI,CAACA,EAAO,IAAM,CAACA,EAAO,MACtB,CAACA,EAAO,UAAYA,EAAO,SAAS,SAAW,EAAU,GAEzCA,EAAO,SAAS,OAAO,CAACX,EAAKC,IAAMD,EAAMC,EAAE,OAAQ,CAAC,IACjD,GACzB,CAKO,SAASW,EAAqBC,EAAsB,CACzD,IAAMC,EAAY,KAAK,IAAI,EACrBC,EAAS,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,EAAG,CAAC,EAExD,MAAO,OADUF,EAAK,YAAY,EAAE,QAAQ,aAAc,GAAG,CACvC,IAAIC,CAAS,IAAIC,CAAM,EAC/C,CAKO,SAASC,EACdC,EACAC,EACAC,EACAC,EACsC,CACtC,IAAMC,EAAcJ,EAAiBE,EAC/BG,EAAcJ,EAAiBE,EAE/BG,GAASD,EAAcD,GAAeA,EAAe,IAMrDG,EAHe,KAAK,MACvBP,EAAiBC,IAAmBC,EAAoBC,EAC3D,EACqC,KAAK,KACxC,EAAID,EAAoB,EAAIC,CAC9B,EACMK,EAAS,KAAK,IAAIH,EAAcD,CAAW,EAAIG,EAC/CE,EAAa,KAAK,IAAI,KAAMD,EAAS,EAAE,EAE7C,MAAO,CAAE,KAAAF,EAAM,WAAAG,CAAW,CAC5B,CChFA,OAAOC,MAAW,QAeX,SAASC,EAAW,CAAE,OAAAC,EAAQ,OAAAC,EAAQ,kBAAAC,EAAmB,SAAAC,CAAS,EAAoB,CAC3F,GAAM,CAACC,EAAiBC,CAAkB,EAAIC,EAAM,SAAyB,IAAI,EAUjF,OARAA,EAAM,UAAU,IAAM,CACpB,GAAIN,EAAO,SAAWA,EAAO,SAAS,OAAS,EAAG,CAChD,IAAMO,EAAUC,EAAcR,EAAO,SAAUC,CAAM,EACrDI,EAAmBE,CAAO,EAC1BL,IAAoBK,CAAO,CAC7B,CACF,EAAG,CAACP,EAAQC,EAAQC,CAAiB,CAAC,EAElC,CAACF,EAAO,SAAW,CAACI,EACf,KAIPE,EAAA,cAAC,OAAI,qBAAoBN,EAAO,GAAI,kBAAiBI,EAAgB,IAClED,CACH,CAEJ,CAUO,SAASM,EAAe,CAAE,UAAAC,EAAW,SAAAP,CAAS,EAAwB,CAC3E,OACEG,EAAA,cAAC,OAAI,eAAcI,EAAW,MAAO,CAAE,QAAS,UAAW,GACxDP,CACH,CAEJ,CAKO,SAASQ,EAAcX,EAA0BC,EAAgB,CACtE,GAAM,CAACM,EAASK,CAAU,EAAIN,EAAM,SAAyB,IAAI,EAC3D,CAACO,EAAWC,CAAY,EAAIR,EAAM,SAAS,EAAI,EAErD,OAAAA,EAAM,UAAU,IAAM,CACpB,GAAI,CAACN,EAAO,QAAS,CACnBc,EAAa,EAAK,EAClB,MACF,CAEA,IAAMC,EAAWP,EAAcR,EAAO,SAAUC,CAAM,EACtDW,EAAWG,CAAQ,EACnBD,EAAa,EAAK,CACpB,EAAG,CAACd,EAAQC,CAAM,CAAC,EAEZ,CAAE,QAAAM,EAAS,UAAAM,CAAU,CAC9B,CCjDO,IAAMG,EAAU,QAGVC,EAAiB,CAC5B,QAAS,GACT,iBAAkB,wCAClB,cAAe,IACjB","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"]}
|