Receiving events
Batches arrive as a POST with a text/plain body containing {"events": [...]}. Read the body as text, parse it as JSON, validate it, and store it. Reply 204 No Content: the browser doesn’t read the response.
import express from 'express';
const app = express();
app.post('/api/ux', express.text({ type: '*/*', limit: '100kb' }), async (req, res) => { try { const { events } = JSON.parse(req.body); if (!Array.isArray(events)) return res.sendStatus(400); await db.insertMany('ux_events', events.slice(0, 100)); res.sendStatus(204); } catch { res.sendStatus(400); }});export async function POST(request: Request) { try { const { events } = JSON.parse(await request.text()); if (!Array.isArray(events)) return new Response(null, { status: 400 }); await db.insertMany('ux_events', events.slice(0, 100)); return new Response(null, { status: 204 }); } catch { return new Response(null, { status: 400 }); }}export default { async fetch(request: Request, env: Env): Promise<Response> { if (request.method !== 'POST') return new Response(null, { status: 405 }); try { const { events } = JSON.parse(await request.text()); if (!Array.isArray(events)) return new Response(null, { status: 400 }); await env.UX_EVENTS.send(events.slice(0, 100)); // a Queue, then batch into D1 / R2 return new Response(null, { status: 204 }); } catch { return new Response(null, { status: 400 }); } },};import { createServer } from 'node:http';
createServer((req, res) => { if (req.method !== 'POST' || req.url !== '/api/ux') return res.writeHead(404).end(); let body = ''; req.on('data', (chunk) => { body += chunk; if (body.length > 100_000) req.destroy(); }); req.on('end', () => { try { const { events } = JSON.parse(body); store(events); res.writeHead(204).end(); } catch { res.writeHead(400).end(); } });}).listen(3000);Cross-origin endpoints
Section titled “Cross-origin endpoints”Because the body is text/plain and beacons send no custom headers, a cross-origin endpoint needs no CORS preflight. The browser still sends the request; it just can’t read the response, and pagepal doesn’t need to.
Storing events
Section titled “Storing events”One row per event works well. Suggested columns:
| Column | Type |
|---|---|
type | text, indexed |
ts | timestamp |
page | text, indexed |
view | text |
data | JSON (everything else) |
Then feed rows back to summarize() or query them directly. For example, the top dead clicks this week:
select data->>'element' as element, page, count(*) as clicksfrom ux_eventswhere type = 'dead_click' and ts > now() - interval '7 days'group by 1, 2order by clicks desclimit 10;The pal in the corner is pagepal itself.