All posts
Detection

Your Analytics Is Structurally Blind to AI Crawlers

Search your analytics for GPTBot and you will find nothing: not a small number, nothing. Why no browser-based tool can see a crawl, and where the data actually lives.

Jay Patel11 min read
browsersnippet runsrecordedGPTBotno JSinvisible

Open your analytics tool and search for GPTBot. You will find nothing. Not a small number. Nothing. The same is true for ClaudeBot, PerplexityBot, OAI-SearchBot, Google-Extended and every other AI crawler currently reading your site.

This is not a filter you forgot to turn off. It is a consequence of how browser analytics works, and no amount of configuration will change it. Every JavaScript-based analytics tool (Google Analytics, Plausible, Fathom, Matomo's default install, and the Traceten snippet) measures by running code in a browser. AI crawlers do not run a browser. They issue one HTTP GET, take the HTML, and leave. There is nobody home to execute your tracking script.

So the most consequential audience your content has acquired in three years is the one audience your measurement stack cannot see at all.

Why the snippet cannot see it

A browser analytics snippet works in a specific sequence. The browser requests the HTML. It parses it. It finds a <script> tag. It fetches, parses and executes that script. The script reads the page context, builds an event, and sends it to a collector.

An AI crawler's request stops at step one. It wants the HTML (the words) and it has no reason to spend compute rendering the page. Rendering is expensive at scale, and at the scale these crawlers operate the expense is enormous.

Where a crawl and a browser visit diverge

The two paths are identical for exactly one step. Everything a browser analytics tool measures happens after the point where the crawler has already left.

01

GET /page

Identical for both. Your server returns the same HTML to a crawler and a person.

02

Parse HTML

Both parse it, but for different reasons: one to render, one to extract text.

03

Execute JavaScript

The browser runs your snippet here. The crawler does not run this step at all.

04

Beacon sent

Only the browser reaches this. The crawl leaves no trace in any client-side tool.

A few crawlers do render. Some search-oriented fetchers execute JavaScript for a subset of pages, and the picture will keep shifting as AI search products mature. But designing your measurement around the exceptions is backwards. The dominant behaviour today is fetch-and-leave, and a measurement approach that only works when a bot voluntarily runs your code is not a measurement approach.

The size of what you are missing

The reason this matters is not that crawls are interesting in themselves. It is the ratio.

Cloudflare has published crawl-to-refer ratios: how many pages a given AI operator crawls for every one human visit it sends back. The numbers are not close to balanced, and the spread between operators is enormous.

Pages crawled per human referral sent back

Order-of-magnitude figures from Cloudflare's 2025 reporting on AI crawl and referral traffic. These are point-in-time and have already moved, so check Cloudflare Radar for current values rather than quoting these.

OperatorCrawl-to-refer ratioReading
AnthropicTens of thousands to oneAlmost entirely collection; referral traffic is a rounding error
OpenAIRoughly a thousand to oneHeavily one-sided, but a real referral channel exists
GoogleRoughly ten to oneThe closest thing to a conventional search exchange

Cloudflare, 2025 reporting on AI crawl-to-refer ratios

Whatever the exact figures are on the day you read this, the shape holds: for most AI operators, the overwhelming majority of your relationship with them consists of activity you currently do not log, do not report, and cannot see.

Two consequences follow.

The first is operational. Crawlers consume bandwidth, hit your origin, and can pin an under-provisioned server. If you cannot count them, you cannot capacity-plan for them or reason about the cost.

The second is strategic, and it is the bigger one. A page cannot be cited in an AI answer if it was never fetched. Crawl coverage is upstream of every AI visibility outcome you care about. Watching only the referral end of the funnel means you find out about a content gap weeks after it started costing you, if you find out at all.

The one-line version

Referral data tells you what already worked. Crawl data tells you what is eligible to work. If you only have the first, every diagnosis you make is retrospective.

Where you can actually observe it

There are exactly three places a crawl is visible, and none of them is the browser.

Observation points for AI crawler traffic

Ranked by how much work stands between the raw data and an answer to a real question.

Observation pointWhat it gives youWhat it costs
CDN / edge logsComplete request record including blocked and cached requests the origin never seesLog export pipeline, storage, and a parser per vendor format
Origin server logsEvery request that reached your app, with user agent and IPLog retention, rotation, and a job to parse and aggregate them
Application middlewareA structured event at request time, with routing context already resolvedA dependency in the request path, which must never slow it down
Browser snippetNothing. Structurally cannot observe a non-rendering clientNot applicable

Log analysis is the traditional answer, and it works. It is also where this problem goes to die in practice: the logs exist, someone writes a grep, the numbers get pasted into a spreadsheet once, and nobody does it again the following month. Fetching a log export is a task. A dashboard is a habit.

Middleware is the alternative: a small piece of code in your request path that recognises a crawler and reports it, in the same way the snippet reports a browser visit. Done correctly it never blocks the response, never throws, and reports after the response has already been sent.

The three hard parts

Recognising a crawler in a log line looks trivial until you try to answer a question with the result.

Hard part one: the user agent is a claim, not a proof. User-Agent: GPTBot is a string that anyone can send. Cooperative crawlers from major labs identify themselves honestly, which covers most legitimate volume, but any scraper wanting your content can wear the same name, and the moment you make a business decision on crawl data, "someone is impersonating a crawler" stops being hypothetical. Verification means reverse DNS or checking the request IP against the operator's published ranges. Both are real work, and the ranges change.

Hard part two: token boundaries. A naive userAgent.includes("GPTBot") also matches NotReallyGPTBot/1.0 and evil-GPTBot-clone. Match on token boundaries, not substrings.

Hard part three, and the one that ruins reports: not all crawls mean the same thing. A training crawler, a search index crawler and a user-triggered fetch are three different events with three different business meanings, and they arrive over the same HTTP connection wearing the same shape.

Three crawl categories, three meanings

These belong in three different reports. Blending them produces a number that answers no question.

CategoryExample agentsWhat a hit actually means
TrainingGPTBot, ClaudeBot, CCBot, Google-ExtendedYour content may inform a future model. No traffic implication today
Search indexOAI-SearchBot, PerplexityBotYou are eligible to be cited in answers now. The leading indicator that matters
User-triggered fetchChatGPT-User, Claude-User, Perplexity-UserA specific person asked an assistant about this specific page. Live demand

That third row is the one most teams have never seen and would most want to. A user-triggered fetch is not a bulk crawl. It is one page retrieved because one human asked about it, right then. There is no other place in your stack where that signal appears.

The highest-signal request on your site

If you instrument only one thing, instrument the crawler-facing files: /robots.txt, /llms.txt, and your sitemaps.

These are boring text files that no human ever requests and that most analytics setups exclude by default as noise. They are also the clearest possible evidence of intent. When GPTBot fetches /llms.txt, you have learned that an AI operator went looking for your machine-readable guidance, a fact you can otherwise only speculate about.

Watch your deny-lists

Most bot-logging setups skip .txt and .xml to avoid drowning in asset noise. That default silently discards the single most informative request an AI crawler makes. Whatever you build, carve out an explicit exception for robots, llms and sitemap paths.

What a crawl record needs to contain

Enough to answer a question weeks later, and no more than that. In practice:

  • Path, with the query string stripped. Query strings carry session tokens, email addresses and other things that should never enter an analytics store. Strip at the edge, not later.
  • Timestamp, so freshness and cadence are computable.
  • Declared agent, kept as the raw claim it is.
  • Resolved provider and category, decided server-side against a registry so it stays current without a redeploy on your side.
  • Verification status: verified by reverse DNS or published range, or unverified. Never a silent boolean that implies proof it does not have.
  • Status code, because a crawler receiving a run of 404s or 429s from you is a problem you would otherwise learn about never.

Notice what is not on that list: no visitor identifiers, no cookies, no fingerprints. A crawler is infrastructure, not a person. The privacy questions that make visitor analytics genuinely hard mostly do not apply here, provided you stay disciplined about the query strings.

How to start this week

  1. Grep one day of logs. awk over yesterday's access log for the known crawler tokens. You now have a baseline number and, usually, a surprise.
  2. Split it three ways by category. The training/search/user-triggered breakdown will reframe how you read the total.
  3. Check crawl coverage. Take your twenty most commercially important URLs and check which have been fetched by a search index crawler in the last thirty days. The misses are your priority list.
  4. Watch /llms.txt and /robots.txt specifically. Who asks, and how often.
  5. Then make it continuous. A monthly grep decays. Middleware or a log pipeline that feeds a dashboard does not.

The gap here is not that this data is difficult to obtain. It is sitting in your logs right now. The gap is that nothing in the standard analytics stack was built to look at it, so almost nobody does, which is exactly why the teams that start looking find things their competitors have not.

Frequently asked

Google Analytics measures by executing JavaScript in a browser. AI crawlers like GPTBot request the HTML and never execute the page's scripts, so the tracking code never runs and no event is ever sent. This applies to every client-side analytics tool, not just GA4, and it cannot be fixed with configuration. The only way to observe a crawl is server-side.

Sources & further reading

  1. 01Overview of OpenAI crawlers and user agents, OpenAI
  2. 02Does Anthropic crawl data from the web, and how can site owners block the crawler?, Anthropic Support
  3. 03PerplexityBot and Perplexity-User, Perplexity
  4. 04AI Insights: crawl-to-refer ratios and AI bot traffic, Cloudflare Radar
  5. 05Google crawlers and fetchers overview, Google Search Central
Share