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_URL
Value: https://app.aeo.press/analytics/cloudflare_event
Secret (secure)
Name: LOG_SECRET
Value: (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.