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.
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 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);
}
}
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);
}
}
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;
}
}
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 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.