# A/B Testing Implementation **Meta-Description:** A/B Testing für Web-Anwendungen. Feature Flags, statistische Signifikanz und Testing-Frameworks mit Next.js. **Keywords:** A/B Testing, Split Testing, Feature Flags, Experimentation, Statistical Significance, Conversion Optimization --- ## Einführung **A/B Testing** ist essentiell für datengetriebene Entscheidungen. Mit **Feature Flags**, **statistischer Auswertung** und modernen Testing-Tools können Hypothesen validiert und Conversions optimiert werden. Dieser Guide zeigt Implementation mit Next.js. --- ## A/B Testing Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ A/B TESTING ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Experiment Flow: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ User Visit │ │ │ │ ↓ │ │ │ │ Random Assignment (Hash-based) │ │ │ │ ↓ │ │ │ │ ┌──────────────┬──────────────┐ │ │ │ │ │ Control (A) │ Variant (B) │ │ │ │ │ │ 50% │ 50% │ │ │ │ │ └──────┬───────┴──────┬───────┘ │ │ │ │ ↓ ↓ │ │ │ │ Track Events Track Events │ │ │ │ ↓ ↓ │ │ │ │ ┌──────────────────────────┐ │ │ │ │ │ Statistical Analysis │ │ │ │ │ │ (Significance Test) │ │ │ │ │ └──────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Key Metrics: │ │ ├── Conversion Rate │ │ ├── Click-Through Rate (CTR) │ │ ├── Revenue per Visitor │ │ ├── Time on Page │ │ └── Bounce Rate │ │ │ │ Statistical Requirements: │ │ ├── Sample Size: Min 1000 per variant │ │ ├── Significance Level: 95% (p < 0.05) │ │ ├── Power: 80% minimum │ │ └── Test Duration: Min 1-2 weeks │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Custom A/B Testing System ```typescript // lib/ab-testing/types.ts export interface Experiment { id: string; name: string; description: string; variants: Variant[]; targetingRules?: TargetingRule[]; status: 'draft' | 'running' | 'paused' | 'completed'; startDate?: Date; endDate?: Date; metrics: string[]; } export interface Variant { id: string; name: string; weight: number; // 0-100 config: Record; } export interface TargetingRule { type: 'percentage' | 'userProperty' | 'url' | 'device'; operator: 'equals' | 'contains' | 'greaterThan' | 'lessThan'; value: string | number; } export interface ExperimentAssignment { experimentId: string; variantId: string; userId: string; assignedAt: Date; } export interface ExperimentEvent { experimentId: string; variantId: string; userId: string; eventName: string; eventValue?: number; timestamp: Date; metadata?: Record; } ``` ```typescript // lib/ab-testing/assignment.ts import { createHash } from 'crypto'; export function assignVariant( userId: string, experiment: Experiment ): Variant { // Deterministic hash-based assignment const hash = createHash('md5') .update(`${experiment.id}:${userId}`) .digest('hex'); // Convert first 8 chars to number (0-100) const hashValue = parseInt(hash.substring(0, 8), 16) % 100; // Assign based on weights let cumulativeWeight = 0; for (const variant of experiment.variants) { cumulativeWeight += variant.weight; if (hashValue < cumulativeWeight) { return variant; } } // Fallback to first variant return experiment.variants[0]; } // Check if user matches targeting rules export function matchesTargeting( user: { id: string; properties?: Record }, rules: TargetingRule[] ): boolean { if (!rules || rules.length === 0) return true; return rules.every(rule => { switch (rule.type) { case 'percentage': const hash = createHash('md5').update(user.id).digest('hex'); const value = parseInt(hash.substring(0, 8), 16) % 100; return value < (rule.value as number); case 'userProperty': const propValue = user.properties?.[rule.operator]; return propValue === rule.value; default: return true; } }); } ``` ```typescript // lib/ab-testing/client.ts import { Experiment, Variant, ExperimentEvent } from './types'; import { assignVariant, matchesTargeting } from './assignment'; class ABTestingClient { private experiments: Map = new Map(); private assignments: Map = new Map(); private userId: string; private eventQueue: ExperimentEvent[] = []; constructor(userId: string) { this.userId = userId; } async loadExperiments(): Promise { const response = await fetch('/api/experiments'); const experiments: Experiment[] = await response.json(); experiments .filter(exp => exp.status === 'running') .forEach(exp => this.experiments.set(exp.id, exp)); } getVariant(experimentId: string): Variant | null { // Check cache first const cached = this.assignments.get(experimentId); if (cached) return cached; const experiment = this.experiments.get(experimentId); if (!experiment) return null; // Check targeting if (!matchesTargeting({ id: this.userId }, experiment.targetingRules || [])) { return null; } // Assign variant const variant = assignVariant(this.userId, experiment); this.assignments.set(experimentId, variant); // Track assignment this.trackEvent(experimentId, 'experiment_viewed'); return variant; } trackEvent( experimentId: string, eventName: string, eventValue?: number, metadata?: Record ): void { const variant = this.assignments.get(experimentId); if (!variant) return; const event: ExperimentEvent = { experimentId, variantId: variant.id, userId: this.userId, eventName, eventValue, timestamp: new Date(), metadata }; this.eventQueue.push(event); this.flushEvents(); } private flushTimeout: NodeJS.Timeout | null = null; private flushEvents(): void { if (this.flushTimeout) return; this.flushTimeout = setTimeout(async () => { if (this.eventQueue.length === 0) return; const events = [...this.eventQueue]; this.eventQueue = []; try { await fetch('/api/experiments/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(events) }); } catch (error) { // Re-queue on failure this.eventQueue.unshift(...events); } this.flushTimeout = null; }, 1000); } } // Singleton let client: ABTestingClient | null = null; export function getABClient(userId: string): ABTestingClient { if (!client || client['userId'] !== userId) { client = new ABTestingClient(userId); } return client; } ``` --- ## React Integration ```tsx // contexts/ABTestingContext.tsx 'use client'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { getABClient } from '@/lib/ab-testing/client'; import { Variant } from '@/lib/ab-testing/types'; interface ABTestingContextValue { getVariant: (experimentId: string) => Variant | null; trackConversion: (experimentId: string, eventName: string, value?: number) => void; isReady: boolean; } const ABTestingContext = createContext(null); export function ABTestingProvider({ children, userId }: { children: ReactNode; userId: string; }) { const [isReady, setIsReady] = useState(false); const [client, setClient] = useState(() => getABClient(userId)); useEffect(() => { client.loadExperiments().then(() => setIsReady(true)); }, [client]); const getVariant = (experimentId: string) => { return client.getVariant(experimentId); }; const trackConversion = (experimentId: string, eventName: string, value?: number) => { client.trackEvent(experimentId, eventName, value); }; return ( {children} ); } export function useABTesting() { const context = useContext(ABTestingContext); if (!context) { throw new Error('useABTesting must be used within ABTestingProvider'); } return context; } // hooks/useExperiment.ts export function useExperiment( experimentId: string, defaultValue: T ): { variant: T; isLoading: boolean; trackConversion: (event: string, value?: number) => void } { const { getVariant, trackConversion, isReady } = useABTesting(); const variant = getVariant(experimentId); const value = variant?.config as T ?? defaultValue; return { variant: value, isLoading: !isReady, trackConversion: (event: string, value?: number) => trackConversion(experimentId, event, value) }; } ``` ```tsx // components/ABTest.tsx 'use client'; import { useExperiment } from '@/hooks/useExperiment'; import { ReactNode } from 'react'; interface ABTestProps { experimentId: string; control: ReactNode; variants: Record; fallback?: ReactNode; } export function ABTest({ experimentId, control, variants, fallback = null }: ABTestProps) { const { variant, isLoading } = useExperiment<{ variantId: string }>( experimentId, { variantId: 'control' } ); if (isLoading) return fallback; if (variant.variantId === 'control') { return <>{control}; } return <>{variants[variant.variantId] || control}; } // Usage Example export function CTASection() { return ( Get Started} variants={{ green: Get Started, orange: Start Free Trial }} /> ); } // Tracked CTA Button function CTAButton({ color, children }: { color: string; children: ReactNode; }) { const { trackConversion } = useExperiment('cta-button-color', {}); const handleClick = () => { trackConversion('cta_clicked'); // Navigate or perform action }; return ( ); } ``` --- ## Statistical Analysis ```typescript // lib/ab-testing/statistics.ts interface ExperimentResults { control: { visitors: number; conversions: number; conversionRate: number; }; variant: { visitors: number; conversions: number; conversionRate: number; }; lift: number; significance: number; isSignificant: boolean; confidenceInterval: [number, number]; requiredSampleSize: number; } // Z-Score for confidence levels const Z_SCORES: Record = { 0.90: 1.645, 0.95: 1.96, 0.99: 2.576 }; export function calculateResults( controlVisitors: number, controlConversions: number, variantVisitors: number, variantConversions: number, confidenceLevel: number = 0.95 ): ExperimentResults { const controlRate = controlConversions / controlVisitors; const variantRate = variantConversions / variantVisitors; // Lift const lift = ((variantRate - controlRate) / controlRate) * 100; // Standard Error const se = Math.sqrt( (controlRate * (1 - controlRate)) / controlVisitors + (variantRate * (1 - variantRate)) / variantVisitors ); // Z-Score const zScore = (variantRate - controlRate) / se; // P-Value (two-tailed) const pValue = 2 * (1 - normalCDF(Math.abs(zScore))); // Significance const significance = (1 - pValue) * 100; const isSignificant = pValue < (1 - confidenceLevel); // Confidence Interval const z = Z_SCORES[confidenceLevel] || 1.96; const marginOfError = z * se; const confidenceInterval: [number, number] = [ (variantRate - controlRate - marginOfError) * 100, (variantRate - controlRate + marginOfError) * 100 ]; // Required Sample Size (for 80% power) const requiredSampleSize = calculateSampleSize( controlRate, controlRate * 1.1, // 10% MDE 0.05, 0.8 ); return { control: { visitors: controlVisitors, conversions: controlConversions, conversionRate: controlRate * 100 }, variant: { visitors: variantVisitors, conversions: variantConversions, conversionRate: variantRate * 100 }, lift, significance, isSignificant, confidenceInterval, requiredSampleSize }; } // Normal CDF approximation function normalCDF(x: number): number { const a1 = 0.254829592; const a2 = -0.284496736; const a3 = 1.421413741; const a4 = -1.453152027; const a5 = 1.061405429; const p = 0.3275911; const sign = x < 0 ? -1 : 1; x = Math.abs(x) / Math.sqrt(2); const t = 1.0 / (1.0 + p * x); const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return 0.5 * (1.0 + sign * y); } // Sample Size Calculator export function calculateSampleSize( baselineRate: number, expectedRate: number, alpha: number = 0.05, power: number = 0.8 ): number { const zAlpha = Z_SCORES[1 - alpha] || 1.96; const zBeta = 0.84; // 80% power const p1 = baselineRate; const p2 = expectedRate; const pBar = (p1 + p2) / 2; const n = Math.ceil( 2 * Math.pow(zAlpha * Math.sqrt(2 * pBar * (1 - pBar)) + zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)), 2) / Math.pow(p1 - p2, 2) ); return n; } ``` --- ## Results Dashboard ```tsx // app/admin/experiments/[id]/page.tsx 'use client'; import { useState, useEffect } from 'react'; import { calculateResults, ExperimentResults } from '@/lib/ab-testing/statistics'; interface ExperimentData { experiment: Experiment; results: { control: { visitors: number; conversions: number }; variants: Record; }; } export default function ExperimentResultsPage({ params }: { params: { id: string } }) { const [data, setData] = useState(null); const [analysis, setAnalysis] = useState(null); useEffect(() => { fetch(`/api/experiments/${params.id}/results`) .then(res => res.json()) .then(setData); }, [params.id]); useEffect(() => { if (!data) return; const variant = Object.values(data.results.variants)[0]; if (!variant) return; const results = calculateResults( data.results.control.visitors, data.results.control.conversions, variant.visitors, variant.conversions ); setAnalysis(results); }, [data]); if (!data || !analysis) return
Loading...
; return (

{data.experiment.name}

{/* Results Cards */}
0} />

Lift

0 ? 'text-green-600' : 'text-red-600'}`}> {analysis.lift > 0 ? '+' : ''}{analysis.lift.toFixed(2)}%

{/* Statistical Significance */}

Statistical Analysis

Confidence Level

{analysis.significance.toFixed(1)}%

Status

{analysis.isSignificant ? 'Significant' : 'Not Significant'}

Confidence Interval

[{analysis.confidenceInterval[0].toFixed(2)}%, {analysis.confidenceInterval[1].toFixed(2)}%]

Required Sample Size

{analysis.requiredSampleSize.toLocaleString()} per variant

{/* Recommendation */} {analysis.isSignificant && (
0 ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200' }`}>

Recommendation

{analysis.lift > 0 ? 'The variant significantly outperforms the control. Consider rolling out to 100% of users.' : 'The control outperforms the variant. Consider keeping the original version.'}

)}
); } ``` --- ## Best Practices | Aspect | Recommendation | |--------|----------------| | **Sample Size** | Min 1,000 visitors per variant | | **Duration** | Run for at least 1-2 full weeks | | **Significance** | Wait for 95% confidence | | **One Change** | Test one variable at a time | | **Primary Metric** | Define before starting | | **Segmentation** | Analyze by device, location | --- ## Fazit A/B Testing erfordert: 1. **Deterministic Assignment**: Hash-basierte Zuweisung 2. **Statistical Rigor**: Signifikanz-Tests 3. **Proper Tracking**: Events und Conversions 4. **Patience**: Ausreichende Sample Size abwarten Datengetriebene Entscheidungen führen zu besseren Produkten. --- ## Bildprompts 1. "A/B testing dashboard showing variant comparison, charts and metrics" 2. "Statistical significance visualization, confidence intervals" 3. "Experiment funnel diagram, control vs variant flow" --- ## Quellen - [Google Optimize Best Practices](https://support.google.com/optimize) - [Evan Miller A/B Testing Calculator](https://www.evanmiller.org/ab-testing/) - [VWO A/B Testing Guide](https://vwo.com/ab-testing/) - [Statistical Methods in A/B Testing](https://www.optimizely.com/optimization-glossary/statistical-significance/)