Skip to content

Server-side bot tracking ​

Most crawlers never execute JavaScript: Googlebot sometimes renders pages, but GPTBot, ClaudeBot, PerplexityBot, CCBot and the AI assistants reading a page on demand (ChatGPT-User, Claude-User, Perplexity-User) usually don't. To measure them, send Eureka a hit from your server or your CDN for every HTML page served. This fills the Indexing bots — SEO & AI, Pages read by AI assistants and Verified vs spoofed bots reports.

Available on the Pro and Scale plans.

1. Create an ingest key ​

Admin → Properties → Ingest keys → New key. The key is shown once. Keep it secret: it goes in your server or CDN configuration, never in a page.

2a. Cloudflare Worker (no latency added) ​

The worker forwards one hit per HTML response, in the background (waitUntil).

js
// Env vars: AH_COLLECTOR (https://collect.example.com), AH_PID (AH-XXXXXX), AH_INGEST_KEY (secret)
const FORWARDED_HEADERS = ["accept", "accept-language", "from", "signature-agent", "signature-input", "signature", "sec-ch-ua", "sec-fetch-mode"];

export default {
  async fetch(request, env, ctx) {
    const response = await fetch(request);
    const type = response.headers.get("content-type") || "";
    if (request.method === "GET" && type.includes("text/html")) {
      ctx.waitUntil(sendHit(request, response, env).catch(() => {}));
    }
    return response;
  },
};

async function sendHit(request, response, env) {
  const h = {};
  for (const name of FORWARDED_HEADERS) {
    const v = request.headers.get(name);
    if (v) h[name] = v.slice(0, 512);
  }
  await fetch(`${env.AH_COLLECTOR}/v1/s`, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${env.AH_INGEST_KEY}` },
    body: JSON.stringify({
      v: 1, pid: env.AH_PID,
      hits: [{
        ts: Date.now(), ip: request.headers.get("cf-connecting-ip") || "",
        ua: request.headers.get("user-agent") || "", m: request.method, url: request.url,
        st: response.status, ref: request.headers.get("referer") || "", h,
      }],
    }),
  });
}

The full version (with Cloudflare's verified-bot flag and ASN) is in the repository at deploy/edge/cloudflare-worker.js. The same approach works with Fastly Compute and Vercel middleware.

2b. nginx + Vector ​

Log HTML responses as JSON, and ship them to the collector with Vector:

nginx
log_format ah escape=json '{"ts":"$msec","ip":"$remote_addr","ua":"$http_user_agent","m":"$request_method",'
  '"url":"$scheme://$host$request_uri","st":$status,"ref":"$http_referer","bytes":$body_bytes_sent,'
  '"sa":"$http_signature_agent","ct":"$sent_http_content_type"}';
access_log /var/log/nginx/ah.json.log ah;

The Vector configuration (deploy/edge/nginx-vector.toml) keeps HTML responses only and posts batches of up to 500 hits to https://collect.example.com/v1/s?pid=AH-XXXXXX with Authorization: Bearer ${AH_INGEST_KEY}. Fluent Bit or any log shipper that can POST JSON works the same way.

The endpoint ​

POST {collector}/v1/s, Authorization: Bearer <ingest key>. Body: {"v":1,"pid":"AH-XXXXXX","hits":[…]}, or a bare JSON array of hits with ?pid= in the URL.

Hit fieldMeaning
tsTime, epoch milliseconds
ipClient IP (used for verification and geography, then truncated; never stored)
uaUser agent
m, url, st, ref, bytesMethod, full URL, HTTP status, referrer, size
hSelected request headers: signature-agent, signature-input, signature, from, accept…

Response 202 {"accepted": n}. Under load the collector answers 503 with the number accepted: retry the batch, duplicates are absorbed.

Hits from humans are dropped (the tag already measures them), and a server hit matching a page view the tag sent in the last 30 seconds is not counted twice.

Agents & humans, every visit counts!