← back to Watches
analytics/ab-testing.js
342 lines
/**
* A/B Testing Framework
* Run controlled experiments to optimize user experience
*/
class ABTestingFramework {
constructor(analytics) {
this.analytics = analytics;
this.experiments = new Map();
this.userAssignments = this.loadAssignments();
this.setupExperiments();
}
/**
* Setup active experiments
*/
setupExperiments() {
// Example experiments - can be configured via backend
this.createExperiment('price_chart_layout', {
name: 'Price Chart Layout Test',
variants: [
{ id: 'control', name: 'Original Layout', weight: 50 },
{ id: 'variant_a', name: 'Side by Side Layout', weight: 50 }
],
startDate: '2025-01-01',
endDate: '2025-12-31',
metrics: ['chart_interaction_rate', 'time_on_page', 'comparison_rate']
});
this.createExperiment('watch_card_design', {
name: 'Watch Card Design Test',
variants: [
{ id: 'control', name: 'Original Design', weight: 50 },
{ id: 'variant_a', name: 'Enhanced Details', weight: 50 }
],
startDate: '2025-01-01',
endDate: '2025-12-31',
metrics: ['watch_view_rate', 'comparison_rate', 'engagement_time']
});
this.createExperiment('cta_button_color', {
name: 'CTA Button Color Test',
variants: [
{ id: 'control', name: 'Blue Button', weight: 33 },
{ id: 'variant_a', name: 'Green Button', weight: 33 },
{ id: 'variant_b', name: 'Orange Button', weight: 34 }
],
startDate: '2025-01-01',
endDate: '2025-12-31',
metrics: ['cta_click_rate', 'conversion_rate']
});
this.createExperiment('search_algorithm', {
name: 'Search Algorithm Test',
variants: [
{ id: 'control', name: 'Basic Search', weight: 50 },
{ id: 'variant_a', name: 'Fuzzy Search', weight: 50 }
],
startDate: '2025-01-01',
endDate: '2025-12-31',
metrics: ['search_success_rate', 'search_refinement_rate', 'results_clicked']
});
}
/**
* Create a new experiment
*/
createExperiment(experimentId, config) {
this.experiments.set(experimentId, {
id: experimentId,
...config,
active: this.isExperimentActive(config.startDate, config.endDate)
});
}
/**
* Check if experiment is active
*/
isExperimentActive(startDate, endDate) {
const now = new Date();
const start = new Date(startDate);
const end = new Date(endDate);
return now >= start && now <= end;
}
/**
* Get variant for user
*/
getVariant(experimentId) {
const experiment = this.experiments.get(experimentId);
if (!experiment || !experiment.active) {
return 'control';
}
// Check if user already assigned
if (this.userAssignments[experimentId]) {
return this.userAssignments[experimentId];
}
// Assign variant based on weights
const variant = this.assignVariant(experiment.variants);
this.userAssignments[experimentId] = variant;
this.saveAssignments();
// Track assignment
this.trackAssignment(experimentId, variant);
return variant;
}
/**
* Assign variant based on weights
*/
assignVariant(variants) {
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
let random = Math.random() * totalWeight;
for (const variant of variants) {
random -= variant.weight;
if (random <= 0) {
return variant.id;
}
}
return variants[0].id; // Fallback to first variant
}
/**
* Track variant assignment
*/
trackAssignment(experimentId, variantId) {
const experiment = this.experiments.get(experimentId);
this.analytics.trackEvent('ab_test_assignment', {
experiment_id: experimentId,
experiment_name: experiment.name,
variant_id: variantId,
variant_name: experiment.variants.find(v => v.id === variantId)?.name
});
this.sendAssignmentToBackend(experimentId, variantId);
}
/**
* Track experiment goal
*/
trackGoal(experimentId, goalName, goalValue = 1) {
const variantId = this.userAssignments[experimentId];
if (!variantId) return;
const experiment = this.experiments.get(experimentId);
this.analytics.trackEvent('ab_test_goal', {
experiment_id: experimentId,
experiment_name: experiment.name,
variant_id: variantId,
goal_name: goalName,
goal_value: goalValue
});
this.sendGoalToBackend(experimentId, variantId, goalName, goalValue);
}
/**
* Track metric for experiment
*/
trackMetric(experimentId, metricName, metricValue) {
const variantId = this.userAssignments[experimentId];
if (!variantId) return;
const experiment = this.experiments.get(experimentId);
this.analytics.trackEvent('ab_test_metric', {
experiment_id: experimentId,
experiment_name: experiment.name,
variant_id: variantId,
metric_name: metricName,
metric_value: metricValue
});
this.sendMetricToBackend(experimentId, variantId, metricName, metricValue);
}
/**
* Get active experiments for user
*/
getActiveExperiments() {
const activeExperiments = [];
for (const [experimentId, experiment] of this.experiments) {
if (experiment.active) {
activeExperiments.push({
id: experimentId,
name: experiment.name,
variant: this.getVariant(experimentId),
metrics: experiment.metrics
});
}
}
return activeExperiments;
}
/**
* Check if variant is active
*/
isVariant(experimentId, variantId) {
return this.getVariant(experimentId) === variantId;
}
/**
* Force variant (for testing)
*/
forceVariant(experimentId, variantId) {
this.userAssignments[experimentId] = variantId;
this.saveAssignments();
this.trackAssignment(experimentId, variantId);
}
/**
* Load assignments from storage
*/
loadAssignments() {
try {
const assignments = localStorage.getItem('ab_assignments');
return assignments ? JSON.parse(assignments) : {};
} catch (error) {
console.error('Failed to load AB assignments:', error);
return {};
}
}
/**
* Save assignments to storage
*/
saveAssignments() {
try {
localStorage.setItem('ab_assignments', JSON.stringify(this.userAssignments));
} catch (error) {
console.error('Failed to save AB assignments:', error);
}
}
/**
* Send assignment to backend
*/
async sendAssignmentToBackend(experimentId, variantId) {
try {
await fetch('/api/analytics/ab-tests/assignments', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: this.analytics.userId,
sessionId: this.analytics.sessionId,
experimentId,
variantId,
timestamp: Date.now()
})
});
} catch (error) {
console.error('Failed to send AB assignment:', error);
}
}
/**
* Send goal to backend
*/
async sendGoalToBackend(experimentId, variantId, goalName, goalValue) {
try {
await fetch('/api/analytics/ab-tests/goals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: this.analytics.userId,
sessionId: this.analytics.sessionId,
experimentId,
variantId,
goalName,
goalValue,
timestamp: Date.now()
})
});
} catch (error) {
console.error('Failed to send AB goal:', error);
}
}
/**
* Send metric to backend
*/
async sendMetricToBackend(experimentId, variantId, metricName, metricValue) {
try {
await fetch('/api/analytics/ab-tests/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: this.analytics.userId,
sessionId: this.analytics.sessionId,
experimentId,
variantId,
metricName,
metricValue,
timestamp: Date.now()
})
});
} catch (error) {
console.error('Failed to send AB metric:', error);
}
}
/**
* Get experiment results (from backend)
*/
async getExperimentResults(experimentId) {
try {
const response = await fetch(`/api/analytics/ab-tests/${experimentId}/results`);
return await response.json();
} catch (error) {
console.error('Failed to get experiment results:', error);
return null;
}
}
/**
* Export experiment data
*/
exportExperimentData() {
return {
assignments: this.userAssignments,
activeExperiments: this.getActiveExperiments(),
timestamp: Date.now()
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = ABTestingFramework;
} else {
window.ABTestingFramework = ABTestingFramework;
}