Run Agent Analytics with a Cloudflare Worker
Use a Cloudflare Worker to send AI-agent, crawler, and human request metadata to answer.cloud without delaying your website responses.
Cloudflare can run a lightweight Worker in front of your existing website and send request metadata to answer.cloud for agent analytics. The Worker forwards each request to your origin, returns the origin response unchanged, and submits the analytics event in the background.
This setup helps answer.cloud distinguish human visits, user-triggered AI agents, search crawlers, and model-training bots. It does not send page contents, request bodies, or response bodies to answer.cloud.
Before you begin
You need:
- An answer.cloud organization configured with the domain you want to measure.
- Access to that domain's Cloudflare account and DNS zone.
- A proxied Cloudflare DNS record (orange cloud) for every hostname you want the Worker to cover.
- The
SHARED_SECRETshown during answer.cloud onboarding under Set up tracking → Cloudflare. This is the Worker's analytics password, not your answer.cloud account password. Keep it private.
Use a Worker Route for this integration because your existing web server remains the origin. Cloudflare recommends Routes when a Worker runs in front of an external origin. A workers.dev URL by itself will not observe traffic to your website. See Cloudflare's Routes documentation.
1. Create the Worker
- Sign in to the Cloudflare dashboard.
- Open Workers & Pages.
- Select Create application, then create a Worker.
- Name it something recognizable, such as
answercloud-agent-analytics. - Deploy the starter Worker, then open Edit code.
- Replace the starter code with the script below.
const REQUEST_TIMEOUT_MS = 2500;
const MAX_RETRIES = 3;
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 = buildEdgeBody({
ipAddress: request.headers.get("CF-Connecting-IP"),
userAgent: request.headers.get("User-Agent"),
referer: request.headers.get("Referer"),
path: pageUrl.toString(),
httpStatus: response.status,
epochMs: started,
responseTimeMs: Date.now() - started,
sessionId: extractSessionIdFromCookie(request.headers.get("Cookie")),
customData: {
source: "cloudflare",
request_id: cfRay,
ray_id: cfRay,
method: request.method,
colo: request.cf?.colo || "",
country: request.cf?.country || "",
},
});
ctx.waitUntil(
postJsonWithRetries(env.RAILS_URL, body, env.SHARED_SECRET)
);
return response;
},
};
function buildEdgeBody({ ipAddress, userAgent, referer, path, httpStatus, epochMs, responseTimeMs, sessionId, customData }) {
return {
ip_address: ipAddress || "",
user_agent: userAgent || "",
referer: referer || "",
path: path || "/",
http_status: httpStatus,
epoch_ms: epochMs,
response_time_ms: responseTimeMs,
event_type: "edge_view",
session_id: sessionId || undefined,
custom_data: customData || {},
};
}
function extractSessionIdFromCookie(cookieHeader) {
if (!cookieHeader) return undefined;
const match = cookieHeader.match(/(?:^|;\s*)aeo_sid=([^;]+)/);
return match ? decodeURIComponent(match[1]) : undefined;
}
async function postJsonWithRetries(url, obj, secret, attempt = 1) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
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(() => "");
if (res.status >= 400 && res.status < 500) return;
throw new Error(`Rails ${res.status}: ${text.slice(0, 200)}`);
} catch (err) {
clearTimeout(timer);
if (attempt < MAX_RETRIES) {
await new Promise(r => setTimeout(r, 200 * Math.pow(2, attempt)));
return postJsonWithRetries(url, obj, secret, attempt + 1);
}
}
}
Save and deploy the Worker.
The script uses ctx.waitUntil() so the analytics request can continue after the website response is returned. Cloudflare documents this as an appropriate pattern for analytics and other background work in its Context API reference.
2. Add the environment variable and secret
Open the Worker, then go to Settings → Variables and Secrets. Add these bindings:
| Name | Type | Value |
|---|---|---|
RAILS_URL |
Plain text | https://app.answer.cloud/analytics/cloudflare_event |
SHARED_SECRET |
Secret | Copy the value shown under Set up tracking → Cloudflare in answer.cloud |
Answer Cloud generates and displays SHARED_SECRET in the onboarding checklist: expand Set up tracking, select Cloudflare, and copy the value shown beside SHARED_SECRET. It remains visible while Answer Cloud is waiting for the first analytics event. After onboarding is complete, there is not currently a customer-facing screen that reveals it again, so store it in Cloudflare when you first copy it.
Deploy the updated bindings. Add SHARED_SECRET as a Cloudflare Secret, not as visible plain text. Cloudflare hides secret values after they are saved; see Cloudflare's secrets documentation.
Do not put the shared secret directly in the Worker source, a Git repository, screenshots, support tickets, or public documentation.
3. Add routes for your website
In the Worker, open Settings → Domains & Routes, select Add, and add a Route for each hostname that should produce analytics.
For example:
example.com/*
www.example.com/*
Add only the hostnames that actually serve your site. If you intentionally want analytics for every proxied subdomain, you can use:
*.example.com/*
The matching hostname must already have a proxied DNS record in the same Cloudflare zone. Do not choose Custom Domain for a site that already has an external origin: a Custom Domain makes the Worker the origin, while a Route lets fetch(request) continue to your existing server.
4. Verify the integration
- Open a page on the routed hostname in a new browser tab.
- Confirm that the page still returns normally.
- In Cloudflare, open the Worker and use Logs → Live to confirm the Worker is being invoked. Cloudflare's real-time logs guide describes this view.
- Keep the answer.cloud setup checklist open at Record your first visit. That step checks the raw event store every five seconds and completes automatically after an event reaches your organization; it does not wait for consolidated analytics.
- Request the site with a known crawler or test user agent if you want to validate agent classification separately from ordinary browser traffic.
The customer Analytics charts use consolidated data and may not show the new request immediately. The onboarding confirmation is available before consolidation, but it is a first-event check rather than a browsable raw-event log. After onboarding is complete, use Cloudflare's live logs for immediate request-level verification and the answer.cloud Analytics dashboard after the next consolidation.
answer.cloud stores successful GET requests with a 2xx response or 304 Not Modified. Query strings are removed before URLs are stored, and static assets and common analytics-beacon paths are excluded from reporting rollups.
What answer.cloud receives
For each matching request, the Worker sends:
- The requested URL, with the fragment removed.
- User agent and referrer headers.
- HTTP status and origin response time.
- Cloudflare Ray ID, data-center code, country code, and HTTP method.
- The existing
aeo_sidsession ID when that cookie is present.
answer.cloud uses the request host to associate the event with the correct organization. The host must match the domain configured for that organization.
Troubleshooting
The website works, but no analytics appear
- Confirm the Worker Route matches the hostname you are visiting.
example.com/*andwww.example.com/*are separate patterns. - Confirm the hostname's DNS record is proxied through Cloudflare.
- Confirm
RAILS_URLis exactlyhttps://app.answer.cloud/analytics/cloudflare_event. - Confirm the website domain is configured on the intended answer.cloud organization.
- Check Cloudflare's live logs to verify the Worker is invoked.
The analytics request returns 401
The SHARED_SECRET does not match the value expected by answer.cloud. Copy it again from your answer.cloud tracking setup, save it as a Secret, and deploy the new Worker version.
The Worker is not running on the apex domain
A wildcard route such as *.example.com/* covers subdomains but not necessarily the apex hostname. Add example.com/* explicitly.
Charts include unexpected paths
Confirm that the route is not attached to unrelated subdomains. Narrow the route to the exact hostnames that serve the measured site. answer.cloud already excludes common static assets and analytics beacons from rollups.
Disable the integration
Remove the website Route from Settings → Domains & Routes, or detach the Worker from the zone. Removing the route stops new events without changing the website's origin or deleting historical analytics in answer.cloud.