Most of your visitors were not people

What happened on a pilot account, what it cost, and how to check your own data.


The number that was wrong

A B2B diagnostics company had been running the pixel for months. Their identified-visitor count looked healthy and grew steadily. Then someone asked a question nobody had asked before: how many distinct people are behind these events?

224,084  "human" visitors over 90 days
 10,496  of them were crawlers          (4.7%)
     59  distinct crawler user agents, every one self-identifying

On the paired staging tenant the proportion was far worse: 69.4% of events were unflagged crawlers. Roughly seven in ten "visits" were a machine.

Why the bot filter missed them

There was a bot filter. It matched a list of names — googlebot, bingbot, semrushbot and a few dozen others. It missed:

  • Every Google crawler that is not Googlebot. AdsBot-Google, GoogleOther, Google-InspectionTool, Feedfetcher-Google, Google-Read-Aloud, Storebot-Google. Six products, one name in the list.
  • Semrush's site auditor. Its token is SiteAuditBot. The list contained semrushbot. Same vendor, different string, no match.

The filter was not broken. It was a list, and a list is only ever as complete as the last person who edited it.

The fix that generalises

A crawler that puts a URL inside its own user agent is telling you what it is:

Mozilla/5.0 (compatible; SiteAuditBot/0.97; +http://www.semrush.com/bot.html)
Mozilla/5.0 (compatible; DuckAssistBot/1.0; +https://duckduckgo.com/duckassistbot/)

So alongside the name list, one rule:

\+https?://\S*(bot|crawl|spider)

That single pattern caught SiteAuditBot, DuckAssistBot and the Facebook crawler without anyone having enumerated them. It generalises to crawlers that do not exist yet, which a list cannot.

What it actually cost — and what it did not

This is the part most write-ups get wrong, so it is worth being exact.

Barely affected: identified people. A crawler has no email to resolve, so it almost never becomes an identified visitor:

19,517  unflagged crawler events, all time
   124  of those ever resolved to an identity   (0.6%)

Against 34,841 identified visitors on that account, 124 is noise.

Badly affected: anything counted per event. Traffic totals, page-view counts, "most visited pages", funnel step counts — every statistic not scoped to an identified person was inflated, and the inflation was concentrated on the pages crawlers like: pricing, sitemap-linked landing pages, anything in the nav.

The first version of our own internal write-up claimed identified-visitor counts were inflated by one in twenty. That was wrong, and we only found out by measuring instead of reasoning. If you take one thing from this: the blast radius of a data-quality bug is rarely where you first assume.

How to check your own data

Before believing any behavioural statistic, check that the number of events is plausible against the number of distinct actors:

curl "https://app.signal.geysera.com/signal-api/v1/visitors?page_size=1" \
  -H "Authorization: Bearer sk_sig_…"

The response's total is people. If your event counts are orders of magnitude larger than that and you are not a high-frequency consumer app, something in between is not a person.

Two rules worth adopting permanently:

  1. n ≈ distinct visitors. If one "visitor" accounts for hundreds of page-views in a session, look at the user agent before building a funnel on it.
  2. Treat unknown as unknown. Our own is_bot column was NULL — never evaluated — for 28.9% of rows. A query written as WHERE NOT is_bot drops every one of those, because NOT NULL is not TRUE. WHERE is_bot IS NOT TRUE is what you want. A three-state column read as two states is a silent filter.

What we changed

  • The self-identifying-crawler rule now runs at ingest, so this cannot accumulate again.
  • History was corrected in a migration that marks the rows it touched, so the change is reversible and auditable.
  • Crawler-only visitors are hidden by default and still viewable — a filter, not a delete. Somebody eventually wants to know how much crawler traffic a page gets, and deleting the evidence to clean up a number is how you end up unable to answer that.

Markdown source: /developers/case-studies/most-of-your-visitors-were-not-people.md