@elevateab/sdk 1.0.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 +144 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @elevateab/sdk
|
|
2
|
+
|
|
3
|
+
Elevate AB Testing SDK for Hydrogen and Remix frameworks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @elevateab/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Example
|
|
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
|
+
};
|
|
27
|
+
|
|
28
|
+
function MyComponent() {
|
|
29
|
+
const { variant, isLoading } = useExperiment(config, 'user-123');
|
|
30
|
+
|
|
31
|
+
if (isLoading) return <div>Loading...</div>;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
{variant?.id === 'control' ? (
|
|
36
|
+
<Price amount={99.99} />
|
|
37
|
+
) : (
|
|
38
|
+
<Price amount={89.99} />
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Using the Experiment Component
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Experiment, VariantDisplay } from '@elevateab/sdk';
|
|
49
|
+
|
|
50
|
+
function ProductPage() {
|
|
51
|
+
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>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Utility Functions
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import {
|
|
72
|
+
assignVariant,
|
|
73
|
+
validateConfig,
|
|
74
|
+
generateExperimentId,
|
|
75
|
+
calculateRevenueLift,
|
|
76
|
+
} from '@elevateab/sdk';
|
|
77
|
+
|
|
78
|
+
// Assign a variant based on weighted distribution
|
|
79
|
+
const variant = assignVariant(config.variants, 'user-123');
|
|
80
|
+
|
|
81
|
+
// Validate experiment configuration
|
|
82
|
+
const isValid = validateConfig(config);
|
|
83
|
+
|
|
84
|
+
// Generate unique experiment ID
|
|
85
|
+
const expId = generateExperimentId('My Test');
|
|
86
|
+
|
|
87
|
+
// Calculate revenue lift
|
|
88
|
+
const { lift, confidence } = calculateRevenueLift(
|
|
89
|
+
10000, // control revenue
|
|
90
|
+
12000, // variant revenue
|
|
91
|
+
500, // control sample size
|
|
92
|
+
500 // variant sample size
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Features
|
|
97
|
+
|
|
98
|
+
- ✅ Zero-config TypeScript support
|
|
99
|
+
- ✅ React Server Components compatible
|
|
100
|
+
- ✅ ESM and CJS dual format
|
|
101
|
+
- ✅ Tree-shakeable
|
|
102
|
+
- ✅ Minified and optimized
|
|
103
|
+
- ✅ Full type definitions
|
|
104
|
+
- ✅ < 1KB gzipped
|
|
105
|
+
|
|
106
|
+
## Compatibility
|
|
107
|
+
|
|
108
|
+
- **Shopify Hydrogen**: ✅ Full support
|
|
109
|
+
- **Remix**: ✅ Full support (ESM + CJS)
|
|
110
|
+
- **React**: 18.0.0+
|
|
111
|
+
- **TypeScript**: 5.0.0+
|
|
112
|
+
- **Node.js**: 18.0.0+
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
### Types
|
|
117
|
+
|
|
118
|
+
- `ExperimentConfig`: Configuration for an experiment
|
|
119
|
+
- `Variant`: Definition of a test variant
|
|
120
|
+
- `TrackingEvent`: Event tracking data structure
|
|
121
|
+
- `ExperimentStatus`: Experiment lifecycle status
|
|
122
|
+
|
|
123
|
+
### Components
|
|
124
|
+
|
|
125
|
+
- `Experiment`: Main experiment wrapper component
|
|
126
|
+
- `VariantDisplay`: Conditional rendering based on variant
|
|
127
|
+
- `useExperiment`: React hook for experiment logic
|
|
128
|
+
|
|
129
|
+
### Utilities
|
|
130
|
+
|
|
131
|
+
- `assignVariant()`: Assign variant based on distribution
|
|
132
|
+
- `hashString()`: Hash function for consistent assignment
|
|
133
|
+
- `validateConfig()`: Validate experiment configuration
|
|
134
|
+
- `generateExperimentId()`: Generate unique experiment IDs
|
|
135
|
+
- `calculateRevenueLift()`: Calculate A/B test metrics
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
140
|
+
|
|
141
|
+
## Support
|
|
142
|
+
|
|
143
|
+
For issues and questions, please visit [GitHub Issues](https://github.com/elevateab/sdk/issues).
|
|
144
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +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});
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ExperimentConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
variants: Variant[];
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface Variant {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
weight: number;
|
|
13
|
+
}
|
|
14
|
+
interface TrackingEvent {
|
|
15
|
+
experimentId: string;
|
|
16
|
+
variantId: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
type ExperimentStatus = 'draft' | 'running' | 'paused' | 'completed';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Assigns a variant based on weighted distribution
|
|
25
|
+
*/
|
|
26
|
+
declare function assignVariant(variants: Variant[], userId: string): Variant;
|
|
27
|
+
/**
|
|
28
|
+
* Simple hash function for user ID
|
|
29
|
+
*/
|
|
30
|
+
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
|
+
*/
|
|
38
|
+
declare function generateExperimentId(name: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Sensitive business logic that should be minified/obfuscated
|
|
41
|
+
*/
|
|
42
|
+
declare function calculateRevenueLift(controlRevenue: number, variantRevenue: number, controlSampleSize: number, variantSampleSize: number): {
|
|
43
|
+
lift: number;
|
|
44
|
+
confidence: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface ExperimentProps {
|
|
48
|
+
config: ExperimentConfig;
|
|
49
|
+
userId: string;
|
|
50
|
+
onVariantAssigned?: (variant: Variant) => void;
|
|
51
|
+
children?: React.ReactNode;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* React component for A/B test experiments
|
|
55
|
+
* Tests React/JSX compatibility with bundlers
|
|
56
|
+
*/
|
|
57
|
+
declare function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps): React.JSX.Element | null;
|
|
58
|
+
interface VariantDisplayProps {
|
|
59
|
+
variantId: string;
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}
|
|
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;
|
|
71
|
+
isLoading: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
declare const VERSION = "1.0.0";
|
|
75
|
+
declare const DEFAULT_CONFIG: {
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
trackingEndpoint: string;
|
|
78
|
+
cacheDuration: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { DEFAULT_CONFIG, Experiment, type ExperimentConfig, type ExperimentStatus, type TrackingEvent, VERSION, type Variant, VariantDisplay, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useExperiment, validateConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ExperimentConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
variants: Variant[];
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface Variant {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
weight: number;
|
|
13
|
+
}
|
|
14
|
+
interface TrackingEvent {
|
|
15
|
+
experimentId: string;
|
|
16
|
+
variantId: string;
|
|
17
|
+
userId: string;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
type ExperimentStatus = 'draft' | 'running' | 'paused' | 'completed';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Assigns a variant based on weighted distribution
|
|
25
|
+
*/
|
|
26
|
+
declare function assignVariant(variants: Variant[], userId: string): Variant;
|
|
27
|
+
/**
|
|
28
|
+
* Simple hash function for user ID
|
|
29
|
+
*/
|
|
30
|
+
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
|
+
*/
|
|
38
|
+
declare function generateExperimentId(name: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Sensitive business logic that should be minified/obfuscated
|
|
41
|
+
*/
|
|
42
|
+
declare function calculateRevenueLift(controlRevenue: number, variantRevenue: number, controlSampleSize: number, variantSampleSize: number): {
|
|
43
|
+
lift: number;
|
|
44
|
+
confidence: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface ExperimentProps {
|
|
48
|
+
config: ExperimentConfig;
|
|
49
|
+
userId: string;
|
|
50
|
+
onVariantAssigned?: (variant: Variant) => void;
|
|
51
|
+
children?: React.ReactNode;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* React component for A/B test experiments
|
|
55
|
+
* Tests React/JSX compatibility with bundlers
|
|
56
|
+
*/
|
|
57
|
+
declare function Experiment({ config, userId, onVariantAssigned, children }: ExperimentProps): React.JSX.Element | null;
|
|
58
|
+
interface VariantDisplayProps {
|
|
59
|
+
variantId: string;
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}
|
|
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;
|
|
71
|
+
isLoading: boolean;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
declare const VERSION = "1.0.0";
|
|
75
|
+
declare const DEFAULT_CONFIG: {
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
trackingEndpoint: string;
|
|
78
|
+
cacheDuration: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { DEFAULT_CONFIG, Experiment, type ExperimentConfig, type ExperimentStatus, type TrackingEvent, VERSION, type Variant, VariantDisplay, assignVariant, calculateRevenueLift, generateExperimentId, hashString, useExperiment, validateConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +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};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elevateab/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Elevate AB Testing SDK for Hydrogen and Remix frameworks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --minify --sourcemap",
|
|
21
|
+
"build:watch": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"ab-testing",
|
|
26
|
+
"experiments",
|
|
27
|
+
"hydrogen",
|
|
28
|
+
"remix",
|
|
29
|
+
"shopify",
|
|
30
|
+
"react"
|
|
31
|
+
],
|
|
32
|
+
"author": "Elevate AB",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/abshop/headless-npm.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/abshop/headless-npm#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/abshop/headless-npm/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react": "^18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/react": "^18.3.27",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|