AIKit's content performance analytics pipeline collects 30+ engagement metrics across blog, social, email, and search channels in real time, processing them entirely on Cloudflare Workers and aggregating them in D1 for sub-second dashboard queries.
The Data Problem
Content marketing teams face a fragmented analytics landscape. Blog traffic lives in Google Analytics, social engagement scatters across native platform dashboards, email performance hides in ESPs, and search rankings are tracked through yet another tool. The result is a chronic inability to answer the most important question: which pieces of content are actually driving results?
For a tool like AIKit — a serverless content management system built on EmDash — this fragmentation is especially painful. AIKit powers dozens of marketing channels, each generating its own stream of engagement data. Pulling everything together manually or through periodic ETL jobs introduces latency, human error, and significant operational burden. By the time a report is assembled, the data is already stale.
Traditional analytics solutions fail here. They're expensive — a full Segment or Snowplow deployment costs hundreds per month. They're slow — batched processing means reports are at best 24 hours behind. And they're brittle — schema changes in any upstream channel can break the entire pipeline. AIKit needed something purpose-built: serverless, real-time, and inexpensive enough that tracking 30+ metrics wouldn't blow the infrastructure budget.
The Solution
AIKit's solution is a fully serverless event collection pipeline on Cloudflare Workers. Every content interaction — a page view, social share, email open, search click — is captured as a structured event and pushed through a Cloudflare Queue into D1, Cloudflare's serverless SQL database. From there, materialized views power a real-time dashboard that refreshes every time you load it.
The design philosophy is simple: capture everything, pay only for what you use, and never batch-process when you can stream. Because Workers run at the edge, events are captured with minimal latency regardless of user location. A visitor in Tokyo clicking a share button on an AIkit-hosted blog generates an event processed on a nearby edge node, queued within milliseconds, and landed in D1 within seconds.
This replaces what would traditionally require Kafka, Logstash, and Elasticsearch with a few hundred lines of TypeScript and a D1 database that costs a few dollars a month. Operational surface area is essentially zero — no server to patch, no queue to tune, no cluster to resize.
Architecture
The pipeline has four stages: capture, queue, store, and serve.
Capture Layer
Every AIKit content page includes a JavaScript beacon that fires structured events. Here's the Worker that receives them:
```typescript
export interface ContentEvent {
eventId: string;
contentId: string;
channel: 'blog' | 'social' | 'email' | 'search';
metric: 'pageview' | 'share' | 'click' | 'open' | 'impression';
source?: string;
referer?: string;
userAgent?: string;
timestamp: number;
metadata?: Record<string, string | number>;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
const event: ContentEvent = await request.json();
if (!event.contentId || !event.metric || !event.channel) {
return new Response('Missing required fields', { status: 400 });
}
event.timestamp = event.timestamp || Date.now();
event.referer = event.referer || request.headers.get('Referer') || undefined;
event.userAgent = event.userAgent || request.headers.get('User-Agent') || undefined;
await env.CONTENT_EVENTS_QUEUE.send(event);
return new Response('OK', { status: 202 });
},
};
```
The Worker validates the event, enriches it with HTTP context, and pushes it to a Cloudflare Queue. The queue buffers events — if D1 is unavailable, events retry automatically without data loss.
Queue and Store Layer
A consumer Worker drains events in batches of 100 and writes to D1 in a single transaction:
```typescript
export default {
async queue(batch: MessageBatch<ContentEvent>, env: Env): Promise<void> {
const stmt = env.DB.prepare(
`INSERT INTO events (event_id, content_id, channel, metric, source, referer, timestamp, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
);
const batchStmt = batch.messages.map(msg => {
const e = msg.body;
return stmt.bind(
e.eventId, e.contentId, e.channel, e.metric,
e.source || null, e.referer || null, e.timestamp,
e.metadata ? JSON.stringify(e.metadata) : null
);
});
await env.DB.batch(batchStmt);
},
};
```
Each batch operation costs a single D1 write unit regardless of row count.
Dashboard Layer
The dashboard queries materialized views through a simple REST API on Workers. Response times are typically under 50ms because D1's SQLite engine is fast for aggregate queries.
Implementation
D1 Schema Design
The core table stores every event as an immutable row:
```sql
CREATE TABLE events (
event_id TEXT PRIMARY KEY,
content_id TEXT NOT NULL,
channel TEXT NOT NULL CHECK(channel IN ('blog', 'social', 'email', 'search')),
metric TEXT NOT NULL CHECK(metric IN ('pageview', 'share', 'click', 'open', 'impression')),
source TEXT,
referer TEXT,
user_agent TEXT,
timestamp INTEGER NOT NULL,
metadata TEXT
);
CREATE INDEX idx_events_content_time ON events(content_id, timestamp);
CREATE INDEX idx_events_channel_metric ON events(channel, metric, timestamp);
```
Raw event tables are great for ingestion but slow for dashboards. The solution is a materialized view updated incrementally:
```sql
CREATE TABLE daily_content_metrics (
content_id TEXT NOT NULL,
channel TEXT NOT NULL,
metric TEXT NOT NULL,
date TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (content_id, channel, metric, date)
);
```
A cron Worker runs every 5 minutes, picking up new events and aggregating them:
```typescript
await env.DB.prepare(`
INSERT OR REPLACE INTO daily_content_metrics (content_id, channel, metric, date, count)
SELECT content_id, channel, metric,
DATE(timestamp / 1000, 'unixepoch') as date,
COUNT(*) as count
FROM events
WHERE timestamp >= ?
GROUP BY content_id, channel, metric, date
`).bind(lastRun).run();
```
This powers the 30-day trending query for the main dashboard:
```sql
SELECT content_id, channel, SUM(count) as total_events
FROM daily_content_metrics
WHERE date >= DATE('now', '-30 days')
GROUP BY content_id, channel
ORDER BY total_events DESC
LIMIT 50;
```
This returns top content across all channels in under 30ms — even with millions of raw events.
Tracking 30+ Metrics
The metrics break down by channel: **Blog (10)** — pageviews, unique visitors, scroll depth, time on page bucket, exit rate, internal link clicks, CTAs clicked, copy-to-clipboard, print, PDF download. **Social (8)** — shares per platform (Twitter, LinkedIn, Facebook, Reddit, HN), saves, comments, quote tweets, reposts, share conversion rate. **Email (7)** — opens, unique opens, click-throughs, CTOR, unsubscribes, spam reports, forward rate. **Search (6)** — impressions, clicks, CTR, average position, featured snippet appearances, branded vs non-branded.
Each metric is just a different value in the `metric` column. Adding a new one requires zero schema changes.
Results
After six months running across AIKit's properties and three beta customer sites:
**Query latency:** The main dashboard loads in under 200ms. Drill-down queries return in under 50ms. The previous manual CSV process took hours and was always three days stale.
**Data freshness:** Events appear in the dashboard within 15 seconds. The queue consumer processes continuously, and the materialized view cron runs every 5 minutes.
**Cost per event:** At ~500,000 events/month, total infrastructure cost is $7.43/month. Workers are free for this volume, D1 storage costs $0.89/GB, and writes cost $0.75/million. That's roughly $0.000014 per event. A comparable BigQuery or Snowflake setup would be 50-100x more expensive.
**Storage efficiency:** The events table holds 3 million rows at 180 MB. The materialized view compresses to 80,000 rows (8 MB) — a 22x reduction that makes dashboards fast without any caching.
**Reliability:** 99.97% uptime over six months. Queue retry logic ensured zero data loss during the two brief Cloudflare platform incidents.
Key Takeaways
1. **Serverless analytics works at scale.** A pipeline on Workers + Queue + D1 handles half a million events monthly for under $8. The key insight: match storage to workload — cheap writes for raw events, fast reads via materialized views. One database serves both.
2. **Capture everything, aggregate later.** The unified events table makes adding new metrics trivial. Instead of designing a complex star schema upfront, store every event as a row and aggregate on read. No schema migrations needed.
3. **Materialized views are the unsung heroes of serverless databases.** Pre-aggregating into daily rollups achieves dashboard speeds rivaling dedicated OLAP systems, without the cost or complexity.
4. **Cost discipline changes what you measure.** At $0.000014 per event, tracking scroll depth, copy-to-clipboard, or any other granular metric is practically free. You can measure everything that matters.
AIKit's content analytics proves you don't need a data engineering team or a six-figure Snowflake bill for real-time content intelligence. A few Workers, one D1 database, and well-designed SQL answer the questions that move your marketing forward.