Implementing A/B Testing in React

Loading

A/B testing is crucial for data-driven decision making in React applications. Here’s a comprehensive guide to implementing various A/B testing strategies:

Client-Side A/B Testing

1. Feature Flag Implementation

// features.js
export const featureFlags = {
  newCheckoutDesign: {
    enabled: Math.random() < 0.5, // 50% rollout
    variant: Math.random() < 0.3 ? 'A' : 'B' // 30% get variant A
  },
  premiumUpsell: {
    enabled: localStorage.getItem('userSegment') === 'premium'
  }
};

// Usage in component
function CheckoutPage() {
  if (featureFlags.newCheckoutDesign.enabled) {
    return featureFlags.newCheckoutDesign.variant === 'A' 
      ? <CheckoutDesignA /> 
      : <CheckoutDesignB />;
  }
  return <LegacyCheckout />;
}

2. Optimizely Integration

// optimizely.js
import { createInstance } from '@optimizely/optimizely-sdk';

const optimizely = createInstance({
  sdkKey: process.env.REACT_APP_OPTIMIZELY_SDK_KEY,
  datafileOptions: {
    autoUpdate: true,
    updateInterval: 300000 // 5 minutes
  }
});

export async function activateExperiment(userId, experimentKey) {
  await optimizely.onReady();
  const userContext = optimizely.createUserContext(userId);
  const decision = userContext.decide(experimentKey);
  return decision.variationKey;
}

// Usage
const variant = await activateExperiment('user123', 'checkout_experiment');

Server-Side A/B Testing

3. Next.js Middleware Variant Routing

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const url = request.nextUrl;
  const cookie = request.cookies.get('ab-test-variant');

  // Set variant if not already set
  if (!cookie) {
    const variant = Math.random() < 0.5 ? 'a' : 'b';
    url.searchParams.set('variant', variant);
    const response = NextResponse.redirect(url);
    response.cookies.set('ab-test-variant', variant);
    return response;
  }

  // Pass variant to page
  url.searchParams.set('variant', cookie.value);
  return NextResponse.rewrite(url);
}

4. Vercel Edge Config Testing

// lib/ab-testing.js
import { get } from '@vercel/edge-config';

export async function getVariant(userId, experimentName) {
  const experiments = await get('experiments');
  const experiment = experiments[experimentName];

  if (!experiment) return 'control';

  // Deterministic variant assignment based on user ID
  const hash = hashCode(userId);
  const percentage = hash % 100;

  let cumulativePercentage = 0;
  for (const [variant, percent] of Object.entries(experiment.variants)) {
    cumulativePercentage += percent;
    if (percentage < cumulativePercentage) {
      return variant;
    }
  }

  return 'control';
}

function hashCode(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0; // Convert to 32bit integer
  }
  return Math.abs(hash);
}

Analytics and Tracking

5. Google Analytics Integration

// useABTest.js hook
import { useEffect } from 'react';
import ReactGA from 'react-ga4';

export function useABTest(experimentId, variant) {
  useEffect(() => {
    if (variant) {
      ReactGA.gtag('event', 'experiment_impression', {
        experiment_id: experimentId,
        variant_id: variant
      });
    }
  }, [experimentId, variant]);
}

// Component usage
function HeroBanner({ variant }) {
  useABTest('hero_banner_test', variant);

  return (
    <div>
      {variant === 'A' ? <HeroA /> : <HeroB />}
    </div>
  );
}

6. Custom Event Tracking

// tracking.js
export function trackExperimentEvent(experimentName, variant, eventName, metadata = {}) {
  if (window.analytics) {
    window.analytics.track('experiment_event', {
      experiment: experimentName,
      variant,
      event: eventName,
      ...metadata,
      timestamp: new Date().toISOString()
    });
  }

  // Fallback to localStorage if analytics not loaded
  const events = JSON.parse(localStorage.getItem('abTestEvents') || []);
  events.push({
    experimentName,
    variant,
    eventName,
    metadata,
    timestamp: new Date().toISOString()
  });
  localStorage.setItem('abTestEvents', JSON.stringify(events));
}

Advanced Techniques

7. Multi-Armed Bandit Implementation

// bandit.js
class Bandit {
  constructor(variants) {
    this.variants = variants.reduce((acc, variant) => {
      acc[variant] = { wins: 0, trials: 0 };
      return acc;
    }, {});
  }

  selectVariant() {
    // Epsilon-greedy strategy
    const epsilon = 0.1;
    if (Math.random() < epsilon) {
      // Exploration: random variant
      const keys = Object.keys(this.variants);
      return keys[Math.floor(Math.random() * keys.length)];
    } else {
      // Exploitation: best performing variant
      return Object.entries(this.variants).reduce(
        (best, [variant, { wins, trials }]) => {
          const rate = trials > 0 ? wins / trials : 0;
          return rate > best.rate ? { variant, rate } : best;
        },
        { variant: null, rate: -1 }
      ).variant;
    }
  }

  recordWin(variant) {
    if (this.variants[variant]) {
      this.variants[variant].wins += 1;
      this.variants[variant].trials += 1;
    }
  }

  recordLoss(variant) {
    if (this.variants[variant]) {
      this.variants[variant].trials += 1;
    }
  }
}

// Singleton instance
export const bandit = new Bandit(['A', 'B', 'C']);

8. SSR A/B Testing with Cookies

// withABTest.js HOC
import { getCookie } from 'cookies-next';

export function withABTest(WrappedComponent, experimentName) {
  return function ABTestWrapper(props) {
    const variant = getCookie(`ab-${experimentName}`) || 'control';
    return <WrappedComponent {...props} variant={variant} />;
  };
}

// Server-side (Next.js)
export async function getServerSideProps({ req, res }) {
  let variant = getCookie(`ab-checkout-test`, { req, res });

  if (!variant) {
    variant = Math.random() < 0.5 ? 'A' : 'B';
    setCookie(`ab-checkout-test`, variant, { req, res, maxAge: 60 * 60 * 24 * 30 }); // 30 days
  }

  return { props: { variant } };
}

Testing Framework Integration

9. Jest Testing Utilities

// test-utils.js
export function mockFeatureFlags(flags) {
  jest.mock('../features', () => ({
    featureFlags: {
      ...jest.requireActual('../features').featureFlags,
      ...flags
    }
  }));
}

// Test usage
describe('CheckoutPage with feature flag', () => {
  beforeEach(() => {
    mockFeatureFlags({
      newCheckoutDesign: {
        enabled: true,
        variant: 'B'
      }
    });
  });

  it('renders variant B', () => {
    render(<CheckoutPage />);
    expect(screen.getByTestId('checkout-design-b')).toBeInTheDocument();
  });
});

10. Cypress A/B Testing Commands

// cypress/support/commands.js
Cypress.Commands.add('setABTestVariant', (experimentName, variant) => {
  cy.setCookie(`ab-${experimentName}`, variant);
});

// Test usage
describe('Checkout A/B Test', () => {
  it('shows variant A', () => {
    cy.setABTestVariant('checkout_test', 'A');
    cy.visit('/checkout');
    cy.get('[data-testid="checkout-a"]').should('exist');
  });
});

Best Practices Checklist

  1. Implementation Strategy
  • [ ] Choose between client-side and server-side testing
  • [ ] Ensure consistent variant assignment
  • [ ] Implement proper user segmentation
  1. Analytics Integration
  • [ ] Track experiment impressions
  • [ ] Record conversion events
  • [ ] Set up proper goal metrics
  1. Performance Considerations
  • [ ] Minimize flickering between variants
  • [ ] Cache variant assignments
  • [ ] Avoid performance overhead
  1. Testing Framework
  • [ ] Create test utilities for mocking variants
  • [ ] Write integration tests for all variants
  • [ ] Test analytics tracking
  1. Statistical Significance
  • [ ] Determine required sample size
  • [ ] Run tests for adequate duration
  • [ ] Use proper statistical methods
  1. Privacy Compliance
  • [ ] Document data collection
  • [ ] Provide opt-out mechanisms
  • [ ] Comply with GDPR/CCPA
  1. Documentation
  • [ ] Document running experiments
  • [ ] Track hypothesis and results
  • [ ] Share learnings with team

Leave a Reply

Your email address will not be published. Required fields are marked *