This guide explains how to:
- Create a Cloudflare Worker
- Add environment variables securely
- Attach a route to your domain
- Validate that events are reaching aeo.press
The Worker sits in front of your existing site, forwards traffic to your origin normally, and asynchronously sends structured analytics data to ag-nts.
No changes to your application code are required.
Architecture
Visitor
↓
Cloudflare Edge
↓
Cloudflare Worker (logs request)
↓
Your Origin Server
↓
Response to Visitor
Worker (async)
↓
POST → ag-nts Rails endpoint
The Worker:
- Passes the request to your origin (fetch(request))
- Measures response time
- Captures metadata (IP, user agent, status, colo, country, etc.)
- Sends a non-blocking POST to ag-nts using ctx.waitUntil()
This ensures:
- No added latency to your users
- Logging failures do not affect site availability
Step 1 — Create the Worker
- Log into your Cloudflare Dashboard
- Navigate to Workers & Pages
- Click Create Application
- Choose Create Worker
- Deploy the default Worker
- Open the Worker and replace the default code with the provided script
export default { async fetch(request, env, ctx) { const started = Date.now(); const response = await fetch(request); const pageUrl = new URL(request.url); pageUrl.hash = ""; const cfRay = request.headers.get("CF-Ray") || ""; const body = { ip_address: request.headers.get("CF-Connecting-IP") || "", user_agent: request.headers.get("User-Agent") || "", referer: request.headers.get("Referer") || "", path: pageUrl.toString(), http_status: response.status, epoch_ms: started, response_time_ms: Date.now() - started, event_type: "edge_view", custom_data: { source: "cloudflare", request_id: cfRay, ray_id: cfRay, method: request.method, colo: request.cf?.colo || "", country: request.cf?.country || "", }, }; ctx.waitUntil( postToRails( env.RAILS_URL || "https://app.aeo.press/analytics/cloudflare_event", body, env.log_secret ) ); return response; },};async function postToRails(url, obj, secret) { const maxAttempts = 2; const timeoutMs = 2500; for (let attempt = 1; attempt <= maxAttempts; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(url, { method: "POST", headers: { "content-type": "application/json", ...(secret ? { "X-Auth": secret } : {}), }, body: JSON.stringify(obj), signal: controller.signal, }); clearTimeout(timer); if (res.ok) return; const text = await res.text().catch(() => ""); // 4xx = non-retryable (bad secret, validation, etc.) if (res.status >= 400 && res.status < 500) { console.log(`[edge_event] Rails ${res.status} (no retry): ${text.slice(0, 200)}`); return; } // 5xx retry once console.log(`[edge_event] Rails ${res.status} (attempt ${attempt}): ${text.slice(0, 200)}`); } catch (e) { clearTimeout(timer); // Network/timeout -> retry once console.log(`[edge_event] POST failed (attempt ${attempt}): ${e?.message || String(e)}`); } // Backoff before retry (only if we have another attempt) if (attempt < maxAttempts) { await sleep(200 * attempt); } }}function sleep(ms) { return new Promise((r) => setTimeout(r, ms));}Click Save and Deploy
Step 2 — Add Environment Variables
In the Worker settings:
- Go to Settings → Variables
- Add the following:
Variable (non-secret)
Name: RAILS_URLValue: https://app.aeo.press/analytics/cloudflare_eventSecret (secure)
Name: LOG_SECRETValue: (provided by aeo.press)IMPORTANT: LOG_SECRET must be added as a Secret, not a plain variable.
Save changes.
Step 3 — Add a Route
Now attach the Worker to your domain traffic.
- Go to Settings → Domains & Routes
- Click Add Route
- Select your zone
- Enter a route pattern
Typical Route Patterns
Log entire site:
example.com/*Log only www:
www.example.com/*Click Save
The Worker is now active.