answer.cloud CMS

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:

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

  1. Sign in to the Cloudflare dashboard.
  2. Open Workers & Pages.
  3. Select Create application, then create a Worker.
  4. Name it something recognizable, such as answercloud-agent-analytics.
  5. Deploy the starter Worker, then open Edit code.
  6. 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

  1. Open a page on the routed hostname in a new browser tab.
  2. Confirm that the page still returns normally.
  3. 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.
  4. 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.
  5. 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:

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

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.