Traditional analytics platforms introduce performance overhead and privacy concerns, while A/B testing typically requires complex client-side integration. By leveraging Cloudflare Workers, Durable Objects, and the built-in Web Analytics platform, we can implement a sophisticated real-time analytics and A/B testing system that operates entirely at the edge. This technical guide details the architecture for capturing user interactions, managing experiment allocations, and processing analytics data in real-time, all while maintaining Jekyll's static nature and performance characteristics.

In This Guide

Edge Analytics Architecture and Data Flow

The edge analytics architecture processes data at Cloudflare's global network, eliminating the need for external analytics services. The system comprises data collection (Workers), real-time processing (Durable Objects), persistent storage (R2), and visualization (Cloudflare Analytics + custom dashboards).

Data flows through a structured pipeline: user interactions are captured by a lightweight Worker script, routed to appropriate Durable Objects for real-time aggregation, stored in R2 for long-term analysis, and visualized through integrated dashboards. The entire system operates with sub-50ms latency and maintains data privacy by processing everything within Cloudflare's network.


// Architecture Data Flow:
// 1. User visits Jekyll site → Worker injects analytics script
// 2. User interaction → POST to /api/event Worker
// 3. Worker routes event to sharded Durable Objects
// 4. Durable Object aggregates metrics in real-time
// 5. Periodic flush to R2 for long-term storage
// 6. Cloudflare Analytics integration for visualization
// 7. Custom dashboard queries R2 via Worker

// Component Architecture:
// - Collection Worker: /api/event endpoint
// - Analytics Durable Object: real-time aggregation  
// - Experiment Durable Object: A/B test allocation
// - Storage Worker: R2 data management
// - Query Worker: dashboard API

Durable Objects for Real-time State Management

Durable Objects provide strongly consistent storage for real-time analytics data and experiment state. Each object manages a shard of analytics data or a specific A/B test, enabling horizontal scaling while maintaining data consistency.

Here's the Durable Object implementation for real-time analytics aggregation:


export class AnalyticsDO {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    this.analytics = {
      pageviews: new Map(),
      events: new Map(),
      sessions: new Map(),
      experiments: new Map()
    };
    this.lastFlush = Date.now();
  }

  async fetch(request) {
    const url = new URL(request.url);
    
    switch (url.pathname) {
      case '/event':
        return this.handleEvent(request);
      case '/metrics':
        return this.getMetrics(request);
      case '/flush':
        return this.flushToStorage();
      default:
        return new Response('Not found', { status: 404 });
    }
  }

  async handleEvent(request) {
    const event = await request.json();
    const timestamp = Date.now();
    
    // Update real-time counters
    await this.updateCounters(event, timestamp);
    
    // Update session tracking
    await this.updateSession(event, timestamp);
    
    // Update experiment metrics if applicable
    if (event.experimentId) {
      await this.updateExperiment(event);
    }
    
    // Flush to storage if needed
    if (timestamp - this.lastFlush > 30000) { // 30 seconds
      this.state.waitUntil(this.flushToStorage());
    }
    
    return new Response('OK');
  }

  async updateCounters(event, timestamp) {
    const minuteKey = Math.floor(timestamp / 60000) * 60000;
    
    // Pageview counter
    if (event.type === 'pageview') {
      const key = `pageviews:${minuteKey}:${event.path}`;
      const current = (await this.analytics.pageviews.get(key)) || 0;
      await this.analytics.pageviews.put(key, current + 1);
    }
    
    // Event counter
    const eventKey = `events:${minuteKey}:${event.category}:${event.action}`;
    const eventCount = (await this.analytics.events.get(eventKey)) || 0;
    await this.analytics.events.put(eventKey, eventCount + 1);
  }
}

A/B Test Allocation and Statistical Validity

The A/B testing system uses deterministic hashing for consistent variant allocation and implements statistical methods for valid results. The system manages experiment configuration, user bucketing, and result analysis.

Here's the experiment allocation and tracking implementation:


export class ExperimentDO {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    this.storage = state.storage;
  }

  async allocateVariant(experimentId, userId) {
    const experiment = await this.getExperiment(experimentId);
    if (!experiment || !experiment.active) {
      return { variant: 'control', experiment: null };
    }

    // Deterministic variant allocation
    const hash = await this.generateHash(experimentId, userId);
    const variantIndex = hash % experiment.variants.length;
    const variant = experiment.variants[variantIndex];
    
    // Track allocation
    await this.recordAllocation(experimentId, variant.name, userId);
    
    return {
      variant: variant.name,
      experiment: {
        id: experimentId,
        name: experiment.name,
        variant: variant.name
      }
    };
  }

  async recordConversion(experimentId, variantName, userId, conversionData) {
    const key = `conversion:${experimentId}:${variantName}:${userId}`;
    
    // Prevent duplicate conversions
    const existing = await this.storage.get(key);
    if (existing) return false;
    
    await this.storage.put(key, {
      timestamp: Date.now(),
      data: conversionData
    });
    
    // Update real-time conversion metrics
    await this.updateConversionMetrics(experimentId, variantName, conversionData);
    
    return true;
  }

  async calculateResults(experimentId) {
    const experiment = await this.getExperiment(experimentId);
    const results = {};
    
    for (const variant of experiment.variants) {
      const allocations = await this.getAllocationCount(experimentId, variant.name);
      const conversions = await this.getConversionCount(experimentId, variant.name);
      
      results[variant.name] = {
        allocations,
        conversions,
        conversionRate: conversions / allocations,
        statisticalSignificance: await this.calculateSignificance(
          experiment.controlAllocations,
          experiment.controlConversions,
          allocations,
          conversions
        )
      };
    }
    
    return results;
  }

  // Chi-squared test for statistical significance
  async calculateSignificance(controlAlloc, controlConv, variantAlloc, variantConv) {
    const controlRate = controlConv / controlAlloc;
    const variantRate = variantConv / variantAlloc;
    
    // Implement chi-squared calculation
    const chiSquared = this.computeChiSquared(
      controlConv, controlAlloc - controlConv,
      variantConv, variantAlloc - variantConv
    );
    
    // Convert to p-value (simplified)
    return this.chiSquaredToPValue(chiSquared);
  }
}

Privacy-First Event Tracking and User Session Management

The event tracking system prioritizes user privacy while capturing essential engagement metrics. The implementation uses first-party cookies, anonymized data, and configurable data retention policies.

Here's the privacy-focused event tracking implementation:


// Client-side tracking script (injected by Worker)
class PrivacyFirstTracker {
  constructor() {
    this.sessionId = this.getSessionId();
    this.userId = this.getUserId();
    this.consent = this.getConsent();
  }

  trackPageview(path, referrer) {
    if (!this.consent.necessary) return;
    
    this.sendEvent({
      type: 'pageview',
      path: path,
      referrer: referrer,
      sessionId: this.sessionId,
      timestamp: Date.now(),
      // Privacy: no IP, no full URL, no personal data
    });
  }

  trackEvent(category, action, label, value) {
    if (!this.consent.analytics) return;
    
    this.sendEvent({
      type: 'event',
      category: category,
      action: action,
      label: label,
      value: value,
      sessionId: this.sessionId,
      timestamp: Date.now()
    });
  }

  sendEvent(eventData) {
    // Use beacon API for reliability
    navigator.sendBeacon('/api/event', JSON.stringify(eventData));
  }

  getSessionId() {
    // Session lasts 30 minutes of inactivity
    let sessionId = localStorage.getItem('session_id');
    if (!sessionId || this.isSessionExpired(sessionId)) {
      sessionId = this.generateId();
      localStorage.setItem('session_id', sessionId);
      localStorage.setItem('session_start', Date.now());
    }
    return sessionId;
  }

  getUserId() {
    // Persistent but anonymous user ID
    let userId = localStorage.getItem('user_id');
    if (!userId) {
      userId = this.generateId();
      localStorage.setItem('user_id', userId);
    }
    return userId;
  }
}

Real-time Analytics Processing and Aggregation

The analytics processing system aggregates data in real-time and provides APIs for dashboard visualization. The implementation uses time-window based aggregation and efficient data structures for quick query response.


// Real-time metrics aggregation
class MetricsAggregator {
  constructor() {
    this.metrics = {
      // Time-series data with minute precision
      pageviews: new CircularBuffer(1440), // 24 hours
      events: new Map(),
      sessions: new Map(),
      locations: new Map(),
      devices: new Map()
    };
  }

  async aggregateEvent(event) {
    const minute = Math.floor(event.timestamp / 60000) * 60000;
    
    // Pageview aggregation
    if (event.type === 'pageview') {
      this.aggregatePageview(event, minute);
    }
    
    // Event aggregation  
    else if (event.type === 'event') {
      this.aggregateCustomEvent(event, minute);
    }
    
    // Session aggregation
    this.aggregateSession(event);
  }

  aggregatePageview(event, minute) {
    const key = `${minute}:${event.path}`;
    const current = this.metrics.pageviews.get(key) || {
      count: 0,
      uniqueVisitors: new Set(),
      referrers: new Map()
    };
    
    current.count++;
    current.uniqueVisitors.add(event.sessionId);
    
    if (event.referrer) {
      const refCount = current.referrers.get(event.referrer) || 0;
      current.referrers.set(event.referrer, refCount + 1);
    }
    
    this.metrics.pageviews.set(key, current);
  }

  // Query API for dashboard
  async getMetrics(timeRange, granularity, filters) {
    const startTime = this.parseTimeRange(timeRange);
    const data = await this.queryTimeRange(startTime, Date.now(), granularity);
    
    return {
      pageviews: this.aggregatePageviews(data, filters),
      events: this.aggregateEvents(data, filters),
      sessions: this.aggregateSessions(data, filters),
      summary: this.generateSummary(data, filters)
    };
  }
}

Jekyll Integration and Feature Flag Management

Jekyll integration enables server-side feature flags and experiment variations. The system injects experiment configurations during build and manages feature flags through Cloudflare Workers.

Here's the Jekyll plugin for feature flag integration:


# _plugins/feature_flags.rb
module Jekyll
  class FeatureFlagGenerator < Generator
    def generate(site)
      # Fetch active experiments from Cloudflare API
      experiments = fetch_active_experiments
      
      # Generate experiment configuration
      site.data['experiments'] = experiments
      
      # Create experiment variations
      experiments.each do |experiment|
        generate_experiment_variations(site, experiment)
      end
    end
    
    private
    
    def fetch_active_experiments
      # Call Cloudflare Worker API to get active experiments
      # This runs during build time to bake in experiment configurations
      uri = URI.parse("https://your-worker.workers.dev/api/experiments")
      response = Net::HTTP.get_response(uri)
      
      if response.is_a?(Net::HTTPSuccess)
        JSON.parse(response.body)['experiments']
      else
        []
      end
    end
    
    def generate_experiment_variations(site, experiment)
      experiment['variants'].each do |variant|
        # Create variant-specific content or configurations
        if variant['type'] == 'content'
          create_content_variation(site, experiment, variant)
        elsif variant['type'] == 'layout'
          create_layout_variation(site, experiment, variant)
        end
      end
    end
  end
end

This real-time analytics and A/B testing system provides enterprise-grade capabilities while maintaining Jekyll's performance and simplicity. The edge-based architecture ensures sub-50ms response times for analytics collection and experiment allocation, while the privacy-first approach builds user trust. The system scales to handle millions of events per day and provides statistical rigor for reliable experiment results.