A/B testing URLs enable experiment tracking, variant assignment, and conversion measurement. Proper URL design ensures consistent user experiences and accurate test results.
Key Takeaways
- 1Use cookies, not URLs, for variant assignment
- 2URL parameters can force specific variants for testing
- 3Track experiment ID in analytics for conversion attribution
- 4Canonical URLs prevent SEO issues with test variants
- 5Edge-side assignment enables server-side testing without flicker
“Statistical validity requires that users see the same variant throughout their experiment journey. URL-based assignment should only be used for preview and debugging, not production traffic.”
A/B Test URL Parameters
While production variant assignment should use cookies, URL parameters are useful for testing and previewing different variants. The table below shows common parameters used in A/B testing.
| Parameter | Purpose | Example |
|---|---|---|
| exp | Experiment ID | exp=pricing-2026 |
| var | Variant name | var=b |
| ab_test | Force variant | ab_test=new-cta |
| preview | Preview mode | preview=variant-b |
These parameters let QA teams and stakeholders preview specific variants without affecting the actual test. Use a preview key to prevent unauthorized access to unpublished variants.
Variant Assignment
Proper variant assignment is critical for statistical validity. Users must see the same variant consistently, which is why cookies are preferred over URL-based assignment for production traffic.
// Variant assignment - cookie-based (recommended)
// Don't assign based on URL alone - users share links
function getOrAssignVariant(experimentId, variants) {
const cookieName = `exp_${experimentId}`;
// Check for existing assignment
const existingVariant = getCookie(cookieName);
if (existingVariant && variants.includes(existingVariant)) {
return existingVariant;
}
// Check for URL override (for testing/preview)
const urlParams = new URLSearchParams(window.location.search);
const urlVariant = urlParams.get('var') || urlParams.get('ab_test');
if (urlVariant && variants.includes(urlVariant)) {
return urlVariant; // Don't persist URL overrides
}
// Assign new variant
const variant = assignVariant(variants);
setCookie(cookieName, variant, 30); // 30 days
// Track assignment
analytics.track('Experiment Assigned', {
experimentId,
variant,
timestamp: Date.now()
});
return variant;
}
function assignVariant(variants) {
// Simple random assignment
const index = Math.floor(Math.random() * variants.length);
return variants[index];
// Or weighted assignment
// return weightedRandom({ a: 0.5, b: 0.3, c: 0.2 });
}This function checks for an existing cookie assignment first, then allows URL overrides for testing, and finally assigns a new variant if needed. URL overrides are not persisted to cookies to prevent test pollution.
How you structure URLs for A/B tests affects SEO, shareability, and implementation complexity. Here are common approaches and their trade-offs.
URL Design for Tests
The cleanest approach serves all variants from the same URL, varying content based on the user's cookie. This avoids SEO issues and prevents users from accidentally sharing variant-specific links.
// Approach 1: Same URL, different content (recommended)
// /pricing - content varies based on cookie
// Clean URLs, no SEO issues, users share same URL
// Approach 2: Variant in URL path
// /pricing-a, /pricing-b
// For completely different pages
// Must handle canonical URLs
// Approach 3: Variant in query param
// /pricing?var=b
// Easy to track, but URL changes on variant switch
// Users may share variant-specific URLs
// Canonical URL handling
function getCanonicalUrl() {
const url = new URL(window.location.href);
// Remove test parameters
url.searchParams.delete('var');
url.searchParams.delete('ab_test');
url.searchParams.delete('exp');
url.searchParams.delete('preview');
return url.toString();
}
// In page head
<link rel="canonical" href={getCanonicalUrl()} />The getCanonicalUrl function strips test parameters so all variants point to the same canonical URL. This prevents search engines from indexing multiple versions of your test pages.
Client-side A/B testing can cause visible flicker as variants load. Edge-side testing assigns variants before the page renders, eliminating this issue.
Edge-Side A/B Testing
Edge middleware runs before your page renders, allowing you to assign variants and rewrite requests to variant-specific pages without any client-side flicker or layout shift.
// Cloudflare Worker / Vercel Edge Middleware
// Assign variant at edge, no client-side flicker
export async function middleware(request) {
const url = new URL(request.url);
// Skip non-experiment pages
if (!url.pathname.startsWith('/pricing')) {
return NextResponse.next();
}
const experimentId = 'pricing-2026';
const variants = ['control', 'variant-a', 'variant-b'];
// Get or assign variant
let variant = request.cookies.get(`exp_${experimentId}`)?.value;
// Check URL override
const urlVariant = url.searchParams.get('var');
if (urlVariant && variants.includes(urlVariant)) {
variant = urlVariant;
}
// Assign if needed
if (!variant || !variants.includes(variant)) {
variant = variants[Math.floor(Math.random() * variants.length)];
}
// Rewrite to variant page
url.pathname = `/pricing/${variant}`;
const response = NextResponse.rewrite(url);
// Set cookie for consistency
response.cookies.set(`exp_${experimentId}`, variant, {
maxAge: 30 * 24 * 60 * 60,
path: '/'
});
// Add header for analytics
response.headers.set('X-Experiment', `${experimentId}:${variant}`);
return response;
}The middleware rewrites requests internally to variant-specific pages while keeping the public URL unchanged. The X-Experiment header helps debugging and can be used for server-side analytics.
The point of A/B testing is measuring impact. You need to track which variants users see and attribute conversions back to those variants for analysis.
Conversion Tracking
Every conversion event should include the experiments and variants the user was exposed to. This data enables you to measure statistical significance and determine winning variants.
// Track conversions with experiment context
function trackConversion(eventName, properties = {}) {
// Get all active experiments
const experiments = getActiveExperiments();
analytics.track(eventName, {
...properties,
experiments: experiments.map(exp => ({
id: exp.id,
variant: exp.variant
})),
timestamp: Date.now()
});
}
function getActiveExperiments() {
const experiments = [];
const experimentIds = ['pricing-2026', 'cta-color', 'checkout-flow'];
experimentIds.forEach(id => {
const variant = getCookie(`exp_${id}`);
if (variant) {
experiments.push({ id, variant });
}
});
return experiments;
}
// In conversion handler
function handlePurchase(order) {
trackConversion('Purchase', {
orderId: order.id,
value: order.total,
products: order.items.map(i => i.sku)
});
}
// Google Analytics 4 integration
gtag('event', 'purchase', {
transaction_id: order.id,
value: order.total,
// Custom dimensions for experiments
experiment_id: 'pricing-2026',
experiment_variant: getVariant('pricing-2026')
});The trackConversion function automatically includes all active experiments with each event. Google Analytics custom dimensions capture the experiment context for analysis in your reporting tools.
Before launching a test, stakeholders need to preview variants. Debug tools help developers troubleshoot assignment and tracking issues during development.
Preview and Debug URLs
Preview URLs let product managers and designers see specific variants without polluting test data. Debug panels show developers which experiments are active and allow quick variant switching.
// Preview URLs for stakeholder review
// /pricing?preview=variant-b&preview_key=secret123
function handlePreviewMode(req) {
const { preview, preview_key } = req.query;
if (preview && preview_key === process.env.PREVIEW_KEY) {
// Force variant without affecting cookie
req.forcedVariant = preview;
return true;
}
return false;
}
// Debug panel URL
// /pricing?ab_debug=true
function renderDebugPanel() {
if (!url.searchParams.has('ab_debug')) return null;
const experiments = getActiveExperiments();
return (
<div className="ab-debug-panel">
<h3>A/B Test Debug</h3>
{experiments.map(exp => (
<div key={exp.id}>
<strong>{exp.id}</strong>: {exp.variant}
<div>
{['control', 'variant-a', 'variant-b'].map(v => (
<a
key={v}
href={`?var=${v}`}
className={v === exp.variant ? 'active' : ''}
>
{v}
</a>
))}
</div>
</div>
))}
</div>
);
}
// Clear experiment cookies
// /pricing?ab_reset=true
if (url.searchParams.has('ab_reset')) {
clearExperimentCookies();
window.location.href = url.pathname;
}The preview mode requires a secret key to prevent unauthorized access. The debug panel shows all active experiments and lets developers switch variants. The reset parameter clears all experiment cookies for fresh testing.