Chrome Extension Extension Ab Testing — Best Practices
6 min readA/B Testing Patterns for Chrome Extensions
Overview
A/B testing (or experimentation) enables data-driven feature decisions by comparing user responses to different variants. Unlike feature flags that toggle on/off, experiments assign users to cohorts and measure outcomes. This pattern covers client-side experimentation with consistent bucketing, remote configuration, and analytics integration.
See also: Feature Flags, Feature Flags Implementation, Analytics and Telemetry, Remote Config
Consistent User Bucketing
Assign users to variants consistently using deterministic hashing. The user’s ID (extension install ID or anonymous token) combined with experiment ID produces a stable bucket.
// utils/experiment-hash.js
export function getBucket(userId, experimentId, variantCount = 2) {
const input = `${userId}:${experimentId}`;
let hash = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash) % variantCount;
}
This ensures the same user always lands in the same variant. Never use PII (email, name) as input—use only anonymous identifiers.
Experiment Manager Class
Centralized manager to load experiments, assign variants, and persist assignments:
// experiments/manager.js
import { getBucket } from '../utils/experiment-hash.js';
const EXPERIMENT_KEY = 'experiments';
class ExperimentManager {
constructor() {
this.experiments = {};
this.assignments = {};
}
async init() {
const stored = await chrome.storage.local.get(EXPERIMENT_KEY);
this.assignments = stored[EXPERIMENT_KEY] || {};
await this.syncRemote();
}
async syncRemote() {
try {
const resp = await fetch('https://api.example.com/experiments');
const data = await resp.json();
this.experiments = data.experiments || {};
await chrome.storage.local.set({
experimentsConfig: this.experiments,
experimentsTimestamp: Date.now()
});
} catch (e) {
const cached = await chrome.storage.local.get('experimentsConfig');
this.experiments = cached.experimentsConfig || {};
}
}
getVariant(experimentId, userId) {
if (this.assignments[experimentId]) {
return this.assignments[experimentId];
}
const exp = this.experiments[experimentId];
if (!exp || !exp.active) return 'control';
const bucket = getBucket(userId, experimentId, exp.variants.length);
const variant = exp.variants[bucket];
this.assignments[experimentId] = variant;
chrome.storage.local.set({ [EXPERIMENT_KEY]: this.assignments });
return variant;
}
}
export const experimentManager = new ExperimentManager();
Variant Renderer
Apply variants to UI consistently in content scripts or popup:
// experiments/renderer.js
import { experimentManager } from './manager.js';
export async function renderWithVariant(rootElement, userId) {
const variant = experimentManager.getVariant('new-onboarding-flow', userId);
if (variant === 'control') {
rootElement.innerHTML = '<div>Legacy onboarding</div>';
} else if (variant === 'variant-a') {
rootElement.innerHTML = '<div>New onboarding flow A</div>';
} else if (variant === 'variant-b') {
rootElement.innerHTML = '<div>New onboarding flow B</div>';
}
// Track exposure
await analytics.track('experiment_exposure', {
experiment: 'new-onboarding-flow',
variant
});
}
Analytics Integration
Track experiment outcomes alongside variant assignment:
// experiments/analytics.js
export async function trackConversion(userId, experimentId, metric) {
const variant = experimentManager.getVariant(experimentId, userId);
await analytics.track('experiment_conversion', {
experiment: experimentId,
variant,
metric,
timestamp: Date.now()
});
}
Always include the variant in conversion events to enable segmented analysis.
Gradual Rollouts
Use percentage-based gates for staged rollouts:
getVariant(experimentId, userId) {
const exp = this.experiments[experimentId];
if (!exp || !exp.active) return 'control';
const bucket = getBucket(userId, experimentId, 100);
if (bucket >= (exp.rolloutPercent || 100)) {
return 'control';
}
const variantBucket = getBucket(userId, experimentId, exp.variants.length);
return exp.variants[variantBucket];
}
Start at 5-10%, monitor metrics, then increase. Always keep control group.
Experiment Lifecycle
| Phase | Actions |
|---|---|
| Design | Define hypothesis, metrics, sample size |
| Launch | Deploy with remote config, small % |
| Measure | Monitor variant performance, check for regressions |
| Conclude | Roll out winner, archive experiment, clean storage |
Archive concluded experiments and clear their assignments from storage to prevent stale data.
Privacy Considerations
- Never bucket by PII; use install ID or anonymous token
- Store assignments locally only; never send to analytics raw
- Provide opt-out in extension settings
- Minimum viable data: experiment ID, variant, timestamp only
Related Patterns
- Feature Flags - Toggle features independently
- Extension Feature Flags Implementation - Detailed flag patterns
- Analytics and Telemetry - Privacy-first analytics
- Remote Config - Server-side configuration delivery
- Crash Reporting - Monitor experiment errors
-
Storage API - Persist experiment assignments
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.