Analyze Cloudflare Worker traffic

When your documentation site, web application, and APIs run on Cloudflare Workers, who is actually reading your content: browsers navigating between pages, background SPA route prefetches, or AI coding agents fetching /llms.txt and .md files?

Requests that do not execute client-side JavaScript, such as AI crawlers, coding agents, RSS readers, and API callers, are invisible to browser analytics scripts. Raw Worker logs capture every invocation, but frameworks such as Next.js and SvelteKit also fire background link prefetches (Next-Router-Prefetch: 1, Sec-Purpose: prefetch) and RSC payloads (RSC: 1) that mix with real page navigations.

In this guide, you deploy a single Cloudflare Tail Worker (scopedb-tail) in your Cloudflare account and attach your producer Workers with tail_consumers without modifying application code. You then query the prebuilt ScopeQL views to separate browser document navigations, background prefetches, and AI crawler or Markdown reads, and export offline interactive HTML charts with the ScopeDB Agent Skill and @scopedb/scopeview.

Allow about 15 minutes to deploy the Tail Worker and run the queries.

1. Create the table and views in ScopeDB

You need a ready ScopeDB Cloud workspace, a Cloudflare account with one or more Workers, Node.js 24 or later, pnpm, and the scope CLI connected to your workspace.

Clone the scopedb/cloudflare-tail-worker repository and install its dependencies:

git clone https://github.com/scopedb/cloudflare-tail-worker.git
cd cloudflare-tail-worker
pnpm install

In Cloudflare Workers, a single Tail Worker can receive execution events from any number of producer Workers in the same Cloudflare account. Every event delivered by Cloudflare includes the producer Worker's scriptName, which scopedb-tail writes to the service column of scopedb.public.cloudflare_worker_events (for example, scopedb-site, app-console-staging, or app-console-production).

The base table stores twelve typed envelope columns alongside a semi-structured attributes (object) column and clusters rows by service and occurred_at:

catalog/table.yaml
name: cloudflare_worker_events
comment: Cloudflare Worker invocations, console logs, and uncaught exceptions
columns:
  - name: event_id
    data_type: string
    comment: Unique row identifier
  - name: occurred_at
    data_type: timestamp
    comment: Event timestamp in UTC
  - name: service
    data_type: string
    comment: Producer Worker name (scriptName)
  - name: event_type
    data_type: string
    comment: worker.invocation, worker.log, or worker.exception
  - name: outcome
    data_type: string
    comment: Worker execution outcome (ok, exception, exceededCpu, etc.)
  - name: level
    data_type: string
    comment: Normalized severity (debug, info, warn, error)
  - name: method
    data_type: string
    comment: HTTP request method when triggered by fetch
  - name: host
    data_type: string
    comment: Request URL hostname
  - name: path
    data_type: string
    comment: Request URL pathname without query parameters
  - name: status
    data_type: int
    comment: HTTP response status code
  - name: ray_id
    data_type: string
    comment: Cloudflare Ray ID (cf-ray)
  - name: message
    data_type: string
    comment: Human-readable event summary, log line, or exception message
  - name: attributes
    data_type: object
    comment: Structured invocation timings, edge metadata, log arguments, or stack trace
cluster_by:
  - service
  - occurred_at

On top of cloudflare_worker_events, the repository defines four views in catalog/:

ObjectKindPurpose
scopedb.public.cloudflare_worker_eventsTableBase table storing invocation, log, and exception events across attached Workers.
scopedb.public.v_cloudflare_requestsViewInvocation metrics, query, UTM fields, prefetch/RSC flags, trace_id, and Cloudflare truncation status.
scopedb.public.v_cloudflare_pageviewsViewSuccessful browser document navigations, excluding prefetches, RSC payloads, and known bots.
scopedb.public.v_cloudflare_ai_botsViewSeparate verified AI, AI user-agent, and Markdown/llms.txt content signals.
scopedb.public.v_cloudflare_errorsView5xx responses, non-ok Worker outcomes, error logs, and uncaught exceptions.

Apply the table and all four views using scope catalog apply:

scope catalog apply --table scopedb.public.cloudflare_worker_events -f catalog/table.yaml --yes
scope catalog apply --view scopedb.public.v_cloudflare_requests -f catalog/v_cloudflare_requests.yaml --yes
scope catalog apply --view scopedb.public.v_cloudflare_pageviews -f catalog/v_cloudflare_pageviews.yaml --yes
scope catalog apply --view scopedb.public.v_cloudflare_errors -f catalog/v_cloudflare_errors.yaml --yes
scope catalog apply --view scopedb.public.v_cloudflare_ai_bots -f catalog/v_cloudflare_ai_bots.yaml --yes

2. Deploy scopedb-tail and attach producer Workers

In Console, copy your workspace ScopeDB API endpoint and create an API key with write access to scopedb.public.cloudflare_worker_events. Upload both values as encrypted Wrangler secrets and deploy scopedb-tail:

pnpm exec wrangler secret put SCOPEDB_ENDPOINT
pnpm exec wrangler secret put SCOPEDB_API_KEY
pnpm run deploy

scopedb-tail sets workers_dev = false in wrangler.toml and exports only a tail() handler, so it exposes no public HTTP route and runs asynchronously after each producer Worker finishes responding.

Next, attach each producer Worker you want to observe by adding tail_consumers to its wrangler.toml and redeploying that Worker:

wrangler.toml
tail_consumers = [{ service = "scopedb-tail" }]

If a producer Worker defines named environments such as [env.staging] and [env.production], add tail_consumers = [{ service = "scopedb-tail" }] under each environment block. Cloudflare automatically sets scriptName to the environment-specific Worker name (my-app-staging versus my-app-production), so both environments land in cloudflare_worker_events with distinct service values.

Trigger a few requests to your producer Worker and verify that invocations are arriving in ScopeDB:

FROM scopedb.public.v_cloudflare_requests
WHERE occurred_at >= NOW() - '15m'::interval
SELECT occurred_at, service, method, path, status, wall_time_ms
ORDER BY occurred_at DESC
LIMIT 5

Customize URL capture and redaction (optional)

By default, scopedb-tail captures decoded URL query parameters as a multi-value map in attributes['query'] (Record<string, string[]>, exposed as query in the views alongside utm_* and ref convenience columns), strips URL fragments and Referer query strings, omits Cookie and Authorization headers, and applies heuristic token redaction to request and Referer path segments.

If your application requires an explicit query parameter allowlist or custom path/value redaction rules, pass UrlCaptureOptions (allowedQueryParams, redactQueryValue, redactPath) to the export default createTailWorker() call at the bottom of src/index.ts and redeploy scopedb-tail:

src/index.ts
export default createTailWorker({
  allowedQueryParams: ["utm_source", "utm_medium", "utm_campaign", "plan"],
  redactQueryValue: (key, value) =>
    key === "plan" && !["free", "team"].includes(value) ? null : value,
  redactPath: (pathname) => pathname.replace(/^\/reset\/[^/]+/, "/reset/:token"),
});

3. Distinguish browser document requests, prefetches, and AI signals

v_cloudflare_pageviews selects GET requests with status 200, Sec-Fetch-Mode: navigate, and Sec-Fetch-Dest: document, while excluding background link prefetches (is_prefetch), React Server Component payloads (is_rsc), static assets, /api/* routes, and known bots:

Definition of v_cloudflare_pageviews
FROM scopedb.public.cloudflare_worker_events
WHERE event_type = 'worker.invocation'
SELECT
    occurred_at,
    service,
    method,
    host,
    path,
    status,
    attributes['continent']::string AS continent,
    attributes['country']::string AS country,
    attributes['region']::string AS region,
    attributes['city']::string AS city,
    attributes['timezone']::string AS timezone,
    attributes['colo']::string AS colo,
    attributes['as_organization']::string AS as_organization,
    if(
        attributes['referer_host']::string = host
            OR attributes['referer_host']::string = concat('www.', host)
            OR concat('www.', attributes['referer_host']::string) = host,
        NULL,
        attributes['referer_host']::string
    ) AS referer_host,
    if(
        attributes['referer_host']::string = host
            OR attributes['referer_host']::string = concat('www.', host)
            OR concat('www.', attributes['referer_host']::string) = host,
        NULL,
        attributes['referer_path']::string
    ) AS referer_path,
    if(attributes['query'] IS NULL, attributes['utm_source']::string, attributes['query']['utm_source'][0]::string) AS utm_source,
    if(attributes['query'] IS NULL, attributes['utm_medium']::string, attributes['query']['utm_medium'][0]::string) AS utm_medium,
    if(attributes['query'] IS NULL, attributes['utm_campaign']::string, attributes['query']['utm_campaign'][0]::string) AS utm_campaign,
    if(attributes['query'] IS NULL, attributes['utm_term']::string, attributes['query']['utm_term'][0]::string) AS utm_term,
    if(attributes['query'] IS NULL, attributes['utm_content']::string, attributes['query']['utm_content'][0]::string) AS utm_content,
    if(attributes['query'] IS NULL, attributes['ref']::string, attributes['query']['ref'][0]::string) AS ref,
    attributes['query'] AS query,
    attributes['user_agent']::string AS user_agent,
    attributes['sec_fetch_mode']::string AS sec_fetch_mode,
    attributes['sec_fetch_dest']::string AS sec_fetch_dest,
    attributes['is_prefetch']::boolean AS is_prefetch,
    attributes['is_rsc']::boolean AS is_rsc,
    attributes['verified_bot_category']::string AS verified_bot_category,
    attributes['wall_time_ms']::float AS wall_time_ms,
    attributes['cpu_time_ms']::float AS cpu_time_ms
WHERE method = 'GET'
  AND status = 200
  AND path IS NOT NULL
  AND sec_fetch_mode = 'navigate'
  AND sec_fetch_dest = 'document'
  AND (is_prefetch IS NULL OR NOT is_prefetch)
  AND (is_rsc IS NULL OR NOT is_rsc)
  AND verified_bot_category IS NULL
  AND NOT starts_with(path, '/api/')
  AND NOT starts_with(path, '/_next/')
  AND NOT starts_with(path, '/_app/')
  AND NOT starts_with(path, '/.')
  AND NOT ends_with(path, '/__data.json')
  AND NOT regexp_like(path, regex => '(?i)[.](md|txt|xml|ico|svg|png|jpg|jpeg|webp|gif|css|js|map|json|woff2?)$')
  AND (user_agent IS NULL OR NOT regexp_like(user_agent, regex => '(?i)(bot|crawler|spider|slurp|facebookexternalhit|feedbin|pingwatch|uptime|statuscake|cortex-xpanse|paloaltonetworks|censys|shodan|zgrab|curl|wget|python|go-http-client|axios|node-fetch|undici|okhttp|headless|Claude-User|ChatGPT-User|Perplexity-User|Anthropic|cohere-ai|Cursor|Codex)'))

Rank document paths

The examples below query 24 hours of observed scopedb-site traffic (2026-11-08 14:00:00 UTC to 2026-11-09 14:00:00 UTC). Replace the service name and time range with your own.

Rank paths by browser document requests:

FROM scopedb.public.v_cloudflare_pageviews
WHERE service = 'scopedb-site'
  AND occurred_at >= '2026-11-08T14:00:00Z'::timestamp
  AND occurred_at < '2026-11-09T14:00:00Z'::timestamp
GROUP BY path
AGGREGATE count() AS document_requests
ORDER BY document_requests DESC, path ASC
LIMIT 10
pathdocument_requests
/32
/blog/raft-agent-observability11
/docs6
/docs/reference/commands/stmt-query5
/blog/manage-observability-data-in-petabytes4
/contact4
/blog3
/blog/schema-on-the-fly3
/blog/scopedb-cloud-public-preview3
/blog/scopeql-origins3

Inspect external referrers and request geography

v_cloudflare_pageviews normalizes same-host and www./apex self-referrals to NULL. Group the remaining referrers with utm_source to inspect external traffic sources:

FROM scopedb.public.v_cloudflare_pageviews
WHERE service = 'scopedb-site'
  AND occurred_at >= '2026-11-08T14:00:00Z'::timestamp
  AND occurred_at < '2026-11-09T14:00:00Z'::timestamp
  AND referer_host IS NOT NULL
GROUP BY referer_host, utm_source
AGGREGATE count() AS document_requests
ORDER BY document_requests DESC, referer_host ASC, utm_source ASC
LIMIT 10
referer_hostutm_sourcedocument_requests
github.comNULL7
www.google.comNULL6
t.coNULL4
docs.scopedb.ioNULL2
raft.buildNULL2
scopedb.aiNULL1
vuink.comNULL1
www.google.ioNULL1
www.reddit.comNULL1
www.superfluidity.aiNULL1

Group by continent to inspect the geographic distribution from Cloudflare's edge GeoIP metadata:

FROM scopedb.public.v_cloudflare_pageviews
WHERE service = 'scopedb-site'
  AND occurred_at >= '2026-11-08T14:00:00Z'::timestamp
  AND occurred_at < '2026-11-09T14:00:00Z'::timestamp
  AND continent IS NOT NULL
GROUP BY continent
AGGREGATE count() AS document_requests
ORDER BY document_requests DESC, continent ASC
continentdocument_requests
AS82
NA54
EU19
SA7

Query application parameters without changing the table

Query parameters are stored in the semi-structured object column attributes['query'] (projected as query on v_cloudflare_requests and v_cloudflare_pageviews). For example, if a request arrives with ?plan=team&plan=free, query['plan'] retains both values and query['plan'][0]::string selects team. With schema-on-the-fly, you can filter or group by any application parameter directly in ScopeQL without altering the table or redeploying the collector:

FROM scopedb.public.v_cloudflare_requests
WHERE service = 'app-console-production'
  AND occurred_at >= NOW() - '24h'::interval
  AND query['plan'][0]::string IS NOT NULL
GROUP BY query['plan'][0]::string AS plan
AGGREGATE count() AS requests
ORDER BY requests DESC, plan ASC

Distinguish AI identity from content access

v_cloudflare_ai_bots exposes three boolean flags so you can analyze AI crawler traffic and Markdown documentation reads together or separately:

  • is_verified_ai_bot: Cloudflare's verified bot category is AI Crawler, AI Assistant, or AI Search.
  • is_ai_user_agent: the User-Agent header matches known AI crawlers or coding agents (such as ClaudeBot, GPTBot, ChatGPT-User, PerplexityBot, Cursor, or Codex).
  • is_markdown_request: the request targets /llms.txt, a .md path, or sends Accept: text/markdown.

Group by all three flags to inspect how these signals break down across requests:

FROM scopedb.public.v_cloudflare_ai_bots
WHERE service = 'scopedb-site'
  AND occurred_at >= '2026-11-08T14:00:00Z'::timestamp
  AND occurred_at < '2026-11-09T14:00:00Z'::timestamp
GROUP BY is_verified_ai_bot, is_ai_user_agent, is_markdown_request
AGGREGATE count() AS requests
ORDER BY requests DESC
is_verified_ai_botis_ai_user_agentis_markdown_requestrequests
falsetruefalse767
falsefalsetrue347
truetruefalse73
truefalsefalse62
truetruetrue7
truefalsetrue2
falsetruetrue1

To drill into the specific documentation pages and crawler user agents behind AI bot traffic, filter with WHERE is_verified_ai_bot OR is_ai_user_agent and group by path and user_agent.

Correlate logs and exceptions within an invocation

When investigating a 5xx response or uncaught exception in v_cloudflare_errors (or a slow request in v_cloudflare_requests), every console.log, console.error, and exception emitted during that Worker execution shares the same attributes['invocation_id']. If your frontend or BFF propagates a W3C traceparent header, scopedb-tail also extracts the 32-character hexadecimal trace_id into attributes['trace_id'].

Copy an invocation_id from v_cloudflare_errors or v_cloudflare_requests and query cloudflare_worker_events to inspect all captured logs and stack traces for that invocation:

FROM scopedb.public.cloudflare_worker_events
WHERE attributes['invocation_id']::string = '<invocation_id>'
SELECT occurred_at, event_type, level, status, path, message, attributes
ORDER BY occurred_at ASC

4. Export interactive charts with $scopedb and ScopeView

Instead of maintaining a separate analytics dashboard login, you can use the ScopeDB Agent Skill ($scopedb) and @scopedb/scopeview to query v_cloudflare_pageviews and v_cloudflare_ai_bots and export standalone HTML charts that open offline in any browser.

Paste this prompt into Claude Code, Codex, or another agent configured with $scopedb:

Use $scopedb to analyze Cloudflare traffic from scopedb.public.v_cloudflare_pageviews and scopedb.public.v_cloudflare_ai_bots where service = 'scopedb-site', from 2026-11-08T14:00:00Z to 2026-11-09T14:00:00Z, and render @scopedb/scopeview interactive HTML charts for:
1. Browser document requests over time in 1-hour buckets using floor(occurred_at, n => 1, unit => 'hour') (line chart)
2. Top 10 document paths ordered by request count descending (top-list chart)
3. Top external referrers and utm_source values (top-list chart)
4. Request continents from Cloudflare GeoIP metadata (top-list chart)
5. Request counts grouped by is_verified_ai_bot, is_ai_user_agent, and is_markdown_request (top-list chart)

Your agent executes each read-only query, formats the rows into a ScopeView JSON document, and exports a self-contained .html file:

top-pages.json
{
  "version": 1,
  "title": "scopedb.io — Top 10 document paths",
  "description": "Source: scopedb.public.v_cloudflare_pageviews (service = scopedb-site, 2026-11-08 14:00 UTC to 2026-11-09 14:00 UTC, LIMIT 10).",
  "data": {
    "fields": [
      { "name": "path", "data_type": "string" },
      { "name": "document_requests", "data_type": "number" }
    ],
    "rows": [
      ["/", "32"],
      ["/blog/raft-agent-observability", "11"],
      ["/docs", "6"],
      ["/docs/reference/commands/stmt-query", "5"],
      ["/blog/manage-observability-data-in-petabytes", "4"],
      ["/contact", "4"],
      ["/blog", "3"],
      ["/blog/schema-on-the-fly", "3"],
      ["/blog/scopedb-cloud-public-preview", "3"],
      ["/blog/scopeql-origins", "3"]
    ]
  },
  "spec": {
    "visualization": "top-list",
    "encoding": {
      "category": "path",
      "value": "document_requests"
    }
  },
  "semantics": {
    "provenance": {
      "kind": "rows",
      "scope": "returned"
    }
  }
}

To validate and export top-pages.json directly from your terminal:

npx @scopedb/scopeview render top-pages.json -o top-pages.html --theme system

Open top-pages.html in your browser to inspect the interactive chart offline.

Continue with Model event data, Query events, or Set data retention to adapt the schema and retention policy for your own Workers.