xperiment
v1.0.2
Published
🧪 Xperiment - A/B testing, simplified
Maintainers
Readme
🧪 Xperiment: A/B testing, simplified
- Optimize like a pro. Intuition doesn't count, numbers do.
- Make data-driven decisions, not guesses.
Features
- 🎯 Simple API - Easy to integrate and use
- 💾 Persistent Storage - Uses DeepBase for automatic persistence
- 🎲 Configurable Probabilities - Set custom weight for each variant
- 📊 Built-in Analytics - Track hits/misses and generate effectiveness reports
- 🔄 Singleton Pattern - Ensures consistent user experience
- ⚡ Async/Await - Modern JavaScript API
Installation
npm install xperimentDemo Examples
Check out the demo folder for complete, runnable examples:
- basic.js - Simple A/B test with two variants
- multivariant.js - Testing 4 variants simultaneously (A/B/C/D)
- weighted-tracking.js - Using weighted scores for different actions
- score-usage.js - Using score() for engagement time tracking
- dashboard.js - Monitoring multiple experiments with a visual dashboard
- complete-flow.js - Multi-stage funnel testing for e-commerce
Run any example:
node demo/basic.js
node demo/score-usage.js
node demo/dashboard.jsQuick Start
Simple Usage (Single Experiment)
import Xperiment from 'xperiment';
// Create experiment directly with cases
const exp = new Xperiment('user123', {
cases: ['variant_a', 'variant_b']
// name is optional, defaults to 'default'
});
// Get assigned variant
const variant = await exp.case();
console.log(`User assigned to: ${variant}`);
// Track outcomes
await exp.hit();
await exp.miss();Production Usage (Multiple Users)
import Xperiment from 'xperiment';
// 1. Define experiment once (persists in database)
await Xperiment.define(['variant_a', 'variant_b'], 'homepage-test');
// Or with custom weights: { variant_a: 30, variant_b: 70 }
// 2. Get experiment instance for each user (loads config from DB)
const exp = await Xperiment.get('user123', 'homepage-test');
// 3. Get the assigned variant (persists automatically)
const variant = await exp.case();
// 4. Track outcomes
await exp.hit(5); // Add 5 points
await exp.miss(2); // Subtract 2 points
// 5. Generate effectiveness report
const report = await Xperiment.report('homepage-test');
console.log(`Best variant: ${report.bestCase}`);
// 6. Reset experiment (clears all data)
await Xperiment.reset('homepage-test');Inline Usage (Quick & Simple)
// Define and get in one step
const exp = await Xperiment.get('user123', 'my-test', ['option_a', 'option_b']);
const variant = await exp.case();API Reference
Static Method: define()
Define an experiment with its cases. Configuration is persisted in the database.
await Xperiment.define(cases, name = 'default')Parameters:
cases(Array|Object) - Case definitions- Array:
['option1', 'option2']- Equal probability (1/n each) - Object:
{option1: 30, option2: 70}- Custom weights
- Array:
name(string) - Experiment name (optional, defaults to'default')
Example:
// Equal distribution
await Xperiment.define(['headline_a', 'headline_b', 'headline_c'], 'headline-test');
// Custom weights
await Xperiment.define({ red: 30, blue: 70 }, 'button-test');
// Using default name (no need to specify)
await Xperiment.define(['option_a', 'option_b']);Constructor
Create an experiment instance directly. Ideal for simple use cases.
new Xperiment(id, options)Parameters:
id(string) - Unique user identifieroptions(object) - Configuration optionsname(string) - Experiment name (default:'default')cases(Array|Object) - Case definitions (optional if loading from DB)
Examples:
// Simple: just cases (uses 'default' name)
const exp1 = new Xperiment('user456', {
cases: ['red', 'blue']
});
// With custom name and weights
const exp2 = new Xperiment('user456', {
name: 'button-color-test',
cases: { red: 30, blue: 70 }
});
// Array with equal probability
const exp3 = new Xperiment('user456', {
name: 'headline-test',
cases: ['a', 'b', 'c', 'd'] // 25% each
});Static Method: get()
Get or create a singleton instance for a user/experiment combination. Automatically loads experiment configuration from database.
await Xperiment.get(id, nameOrOptions = 'default', cases = null)Parameters:
id(string) - Unique user identifiernameOrOptions(string|Object) - Experiment name or options object- As string:
'experiment-name' - As object:
{ name: 'experiment-name', cases: [...] }
- As string:
cases(Array|Object) - Optional: cases to define if experiment doesn't exist
Returns: Promise - Experiment instance
Examples:
// Load from DB (experiment must be defined first)
await Xperiment.define(['control', 'treatment'], 'my-test');
const exp1 = await Xperiment.get('user123', 'my-test');
// Inline definition
const exp2 = await Xperiment.get('user123', 'quick-test', ['a', 'b']);
// With options object
const exp3 = await Xperiment.get('user123', {
name: 'flex-test',
cases: ['x', 'y', 'z']
});
// Default experiment (no name needed)
const exp4 = await Xperiment.get('user123'); // uses 'default' nameInstance Method: case()
Get the assigned case for this user. Returns the same case on subsequent calls.
await exp.case()Returns: Promise - The assigned case
Example:
await Xperiment.define(['control', 'treatment'], 'my-test');
const exp = await Xperiment.get('user123', 'my-test');
const variant = await exp.case();
// Returns 'control' or 'treatment' based on configured probabilities
// Always returns the same value for this userInstance Method: hit()
Record a positive outcome (success).
await exp.hit(amount = 1)Parameters:
amount(number) - Points to add (default: 1)
Example:
await exp.hit(); // Add 1 point
await exp.hit(10); // Add 10 pointsInstance Method: miss()
Record a negative outcome (failure).
await exp.miss(amount = 1)Parameters:
amount(number) - Points to add (default: 1)
Example:
await exp.miss(); // Add 1 miss
await exp.miss(5); // Add 5 missesInstance Method: score()
Set a fixed score value for a user (non-incremental). Unlike hit() which adds to the total, score() sets a specific value that will be added to hits in calculations.
await exp.score(value = 1)Parameters:
value(number) - Fixed score value to set (default: 1)
Use cases:
- Engagement time (seconds/minutes)
- Scroll depth percentage (0-100)
- Revenue per user
- Any metric where you track a final accumulated value per user
Example:
// Track time spent on page
const engagementSeconds = 145;
await exp.score(engagementSeconds);
// Track scroll depth
const scrollPercentage = 87;
await exp.score(scrollPercentage);Note: Each call to score() replaces the previous value (not incremental). The score value is added to hits when generating reports.
Static Method: reset()
Reset an entire experiment, deleting all user data.
await Xperiment.reset(name = 'default')Parameters:
name(string) - Experiment name to reset (default:'default')
Example:
await Xperiment.reset('homepage-test');
await Xperiment.reset(); // Resets 'default' experimentStatic Method: report()
Generate an effectiveness report for an experiment.
await Xperiment.report(name = 'default')Parameters:
name(string) - Experiment name (default:'default')
Returns: Promise - Report with the following structure:
{
experiment: 'experiment-name',
totalUsers: 100,
cases: {
'variant_a': {
users: 50,
totalHits: 300,
totalMisses: 100,
netScore: 200,
successRate: 0.75
},
'variant_b': {
users: 50,
totalHits: 250,
totalMisses: 150,
netScore: 100,
successRate: 0.625
}
},
bestCase: 'variant_a',
effectiveness: 100
}Example:
const report = await Xperiment.report('homepage-test');
console.log(`Total users tested: ${report.totalUsers}`);
console.log(`Winner: ${report.bestCase}`);
console.log(`Success rate: ${report.cases[report.bestCase].successRate * 100}%`);Usage Examples
Simple Single Experiment
import Xperiment from 'xperiment';
// No need to define or name - just use it!
const exp = new Xperiment('user_alice', {
cases: ['old_checkout', 'new_checkout']
});
const variant = await exp.case();
// Show appropriate UI
if (variant === 'new_checkout') {
showNewCheckout();
} else {
showOldCheckout();
}
// Track conversion
if (userCompletesPurchase()) {
await exp.hit();
} else {
await exp.miss();
}Production with Multiple Users
import Xperiment from 'xperiment';
// Define experiment once (persists in database)
await Xperiment.define(['old_checkout', 'new_checkout'], 'checkout-flow');
async function testUserJourney(userId) {
// Get experiment instance for user (loads from DB)
const exp = await Xperiment.get(userId, 'checkout-flow');
const variant = await exp.case();
// Show appropriate UI based on variant
if (variant === 'new_checkout') {
showNewCheckout();
} else {
showOldCheckout();
}
// Track conversion
if (userCompletesPurchase()) {
await exp.hit();
} else {
await exp.miss();
}
}Weighted Distribution
// Give 80% of traffic to control, 20% to new feature
await Xperiment.define({ control: 80, new_feature: 20 }, 'feature-rollout');
const exp = await Xperiment.get('user789', 'feature-rollout');
const variant = await exp.case();Multi-variant Testing (Equal Distribution)
// Define with array for equal probability (25% each)
await Xperiment.define([
'headline_a',
'headline_b',
'headline_c',
'headline_d'
], 'landing-page-headline');
const exp = await Xperiment.get('user999', 'landing-page-headline');
const headline = await exp.case();Analytics Dashboard
async function showDashboard() {
const experiments = ['homepage-test', 'checkout-flow', 'pricing-test'];
for (const name of experiments) {
const report = await Xperiment.report(name);
console.log(`\n=== ${report.experiment} ===`);
console.log(`Total Users: ${report.totalUsers}`);
console.log(`Best Case: ${report.bestCase}`);
for (const [caseName, stats] of Object.entries(report.cases)) {
console.log(`\n${caseName}:`);
console.log(` Users: ${stats.users}`);
console.log(` Success Rate: ${(stats.successRate * 100).toFixed(2)}%`);
console.log(` Net Score: ${stats.netScore}`);
}
}
}Score-based Tracking
await Xperiment.define(['layout_a', 'layout_b'], 'engagement-test');
const exp = await Xperiment.get('user555', 'engagement-test');
const layout = await exp.case();
// Track different levels of engagement
if (userClicksButton()) {
await exp.hit(1);
}
if (userSharesContent()) {
await exp.hit(5);
}
if (userMakesPurchase()) {
await exp.hit(10);
}
if (userBounces()) {
await exp.miss(1);
}Testing
Run the test suite:
npm testThe library includes comprehensive tests covering:
- Constructor and singleton pattern
- Case assignment and persistence
- Metrics tracking
- Reset functionality
- Report generation
- Edge cases and error handling
How It Works
- Assignment: When a user first encounters an experiment, they're randomly assigned to a case based on configured probabilities
- Persistence: The assignment is immediately saved to DeepBase and will remain consistent for that user
- Tracking: As the user interacts with your application, you track positive (hit) and negative (miss) outcomes
- Analysis: Generate reports to see which variant performs best based on net score (hits - misses) and success rate
Best Practices
- Choose meaningful experiment names - Use descriptive names like
'homepage-hero-test'instead of'test1' - Track meaningful events - Use hits for conversions, not just clicks
- Use weighted scoring - Give more points to important actions (e.g., purchase = 10 points, signup = 5 points)
- Let tests run long enough - Collect sufficient data before making decisions
- Reset carefully - Resetting an experiment deletes ALL user data for that experiment
Data Structure
DeepBase stores data in the following structure:
config/
{experimentName}/
cases: ['variant_a', 'variant_b'] or { variant_a: 50, variant_b: 50 }
experiments/
{experimentName}/
{userId}/
case: 'variant_a'
hits: 25
misses: 10
score: 145 (optional, set via score() method)License
MIT
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
