Filtering Bot Traffic
PostHogOfficialPublished by PostHog
- Skill
- Tool integration
- Data and AI
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a framework for distinguishing human visitors from automated traffic in PostHog. By leveraging virtual properties and HogQL functions, users can filter out noise, quantify AI-agent activity, and gain insights into which bots are interacting with their site. It covers the classification surface, best practices for handling different traffic types, and methods for defining custom bot rules to ensure analytics remain accurate and actionable.
What it does
- Filters out bot and crawler traffic from insights and dashboards.
- Quantifies the volume of automated vs. human traffic.
- Identifies specific AI agents and crawlers visiting a site.
- Provides SQL functions for advanced traffic analysis.
- Allows for the creation of custom bot rules for niche scrapers.
- Categorizes traffic into Regular, AI Agent, Bot, and Automation buckets.
When to use it
- When you need to exclude bots to get accurate human-only traffic counts.
- When you want to measure the impact of AI crawlers and search engines on your content.
- When you need to identify which specific bots or companies are hitting your site.
- When you need to break down traffic trends by bot name or operator.
- When you need to define custom rules for internal load tests or partner integrations.
Advantages
- Works on historical data without requiring re-ingestion.
- Provides consistent classification across both UI filters and raw SQL.
- Distinguishes between useful AI-agent traffic and low-value noise.
- Extensible via custom bot rules for non-standard crawlers.
Usage examples
- “How can I exclude bots from my pageview trends?”
- Add a property filter to your insight where $virt_is_bot is exact false to ensure only human traffic is counted.
- “What percentage of our traffic is coming from AI agents?”
- Break down your pageview trend by the $virt_traffic_type property to see the distribution between Regular, AI Agent, Bot, and Automation traffic.
- “Is ChatGPT hitting our documentation site?”
- Filter your events by $virt_is_bot equals true and break down by $virt_bot_name to see if GPTBot or other OpenAI crawlers appear in the list.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/posthog/filtering-bot-traffic/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip filtering-bot-traffic.zip -d .agents/skills
Documentation by PostHog
Filtering and measuring bot traffic
PostHog classifies every request by user agent so you can tell humans apart from bots, crawlers, and AI agents anywhere HogQL runs — the SQL editor, insights, trends, and Web analytics breakdowns. This skill teaches you (the agent) how to use that classification to:
exclude bots so analytics reflect human traffic only
measure how much traffic is automated, and which bots / operators are responsible
separate AI-agent traffic (worth measuring) from noise (worth dropping)
pick the right surface — virtual properties for the insight builder, functions for raw SQL
For real-time ("right now", last 30 min) bot questions and the Live tab tiles, use the exploring-live-traffic skill instead. This skill is for historical windows, saved insights, dashboards, and filtering.
When to use this skill
Use it when the user wants to:
exclude or filter out bots ("remove bots from my pageviews", "humans only")
quantify automated traffic ("what % of traffic is bots?", "how much is AI crawlers?")
find which bots hit them ("which crawlers visit us?", "is ChatGPT reading our docs?")
break a trend down by traffic type or bot name
measure AI-agent / AI-search traffic specifically (AEO / answer-engine visibility)
Do not use it for the Live tab, real-time numbers, or the per-minute bot charts — that is exploring-live-traffic.
The classification surface
Two equivalent ways to reach the same classification. Prefer virtual properties in the insight builder and filters; use functions in hand-written SQL or when you need a value the virtual properties don't expose.
Virtual properties (insight builder, filters, breakdowns)
These read the user agent for you (falling back from $raw_user_agent to $user_agent),
so you don't pass anything in. Available wherever you pick an event property.
| Property | Value |
|---|---|
$virt_is_bot | boolean — true for bots / crawlers / automation |
$virt_traffic_type | Regular, AI Agent, Bot, or Automation |
$virt_traffic_category | finer category, e.g. ai_crawler, ai_search, ai_assistant, search_crawler, seo_crawler, social_crawler, monitoring, http_client, headless_browser, no_user_agent, regular |
$virt_bot_name | display name, e.g. Googlebot, GPTBot, ClaudeBot |
$virt_bot_operator | company behind the bot, e.g. Google, OpenAI, Anthropic |
HogQL functions (raw SQL)
Pass properties.$raw_user_agent (and optionally properties.$ip) as is, the same arguments the
virtual properties use. Don't wrap it, e.g. in coalesce(...): server and mobile SDK events (and
cookieless events) are exempt from the empty-UA rule only when the argument is that plain field.
| Function | Returns |
|---|---|
isLikelyBot(ua) | true on a bot pattern or empty UA (server/mobile SDK events exempt) |
getTrafficType(ua) | AI Agent / Bot / Automation / Regular |
getTrafficCategory(ua) | subcategory; regular for humans |
getBotType(ua) | same subcategory but empty string for humans — handy for filtering |
getBotName(ua) | bot name; empty for humans |
getBotOperator(ua) | operator/company; empty for humans |
Traffic types — what to keep vs drop
getTrafficType / $virt_traffic_type sorts every request into four buckets. The default
move differs per bucket — don't treat them all as noise:
| Type | What it is | Default move |
|---|---|---|
Regular | Human visitors | Keep |
AI Agent | AI crawlers, AI search, AI assistants (GPTBot, ClaudeBot, PerplexityBot, ChatGPT-User) | Often measure, don't drop — these are how AI tools find and cite content |
Bot | Search crawlers, SEO tools, social previews, monitoring (Googlebot, AhrefsBot, Pingdom) | Exclude from human metrics; track separately for SEO |
Automation | HTTP clients and headless browsers (curl, python-requests, Puppeteer) | Usually noise — exclude |
Recipes
Exclude bots from an insight (humans only)
Add a property filter $virt_is_bot exact false:
{ "key": "$virt_is_bot", "value": ["false"], "operator": "exact", "type": "event" }Drop it into any TrendsQuery / FunnelsQuery / etc. properties. Visitor, session, and
pageview counts then reflect human traffic only, without changing stored data.
To exclude a narrower slice (e.g. keep AI agents but drop monitoring + automation), filter
on $virt_traffic_type or $virt_traffic_category with operator: is_not instead.
What share of traffic is automated
Break a pageview trend down by $virt_traffic_type:
{
"kind": "TrendsQuery",
"dateRange": { "date_from": "-30d" },
"series": [{ "kind": "EventsNode", "event": "$pageview", "math": "total" }],
"breakdownFilter": { "breakdown": "$virt_traffic_type", "breakdown_type": "event" },
"trendsFilter": { "display": "ActionsBarValue" }
}Which bots / operators are hitting us
Filter to bots and break down by name (or $virt_bot_operator for company-level):
{
"kind": "TrendsQuery",
"dateRange": { "date_from": "-30d" },
"series": [{ "kind": "EventsNode", "event": "$pageview", "math": "total" }],
"properties": [{ "key": "$virt_is_bot", "value": ["true"], "operator": "exact", "type": "event" }],
"breakdownFilter": { "breakdown": "$virt_bot_name", "breakdown_type": "event", "breakdown_limit": 25 },
"trendsFilter": { "display": "ActionsBarValue" }
}Measure AI-agent traffic specifically
Filter $virt_traffic_type exact AI Agent, break down by $virt_bot_operator to see
which tools (OpenAI, Anthropic, Perplexity, …) read your site and which pages they hit.
Raw SQL equivalents
-- human pageviews only
SELECT count() AS human_pageviews
FROM events
WHERE event = '$pageview'
AND NOT isLikelyBot(properties.$raw_user_agent)
-- top bots by hits
SELECT
getBotName(properties.$raw_user_agent) AS bot,
getBotOperator(properties.$raw_user_agent) AS operator,
count() AS hits
FROM events
WHERE event = '$pageview'
AND isLikelyBot(properties.$raw_user_agent)
GROUP BY bot, operator
ORDER BY hits DESCAdding a bot PostHog doesn't know yet
The built-in list only covers self-declared user agents PostHog already knows. When a
scraper matters to a project but isn't detected — an internal load test, a partner
integration, a niche crawler — add a custom bot rule instead of waiting for the
built-in list to catch up. Rules extend the same classification surface, so Is bot
(isLikelyBot), Bot name, and Traffic category reflect them everywhere HogQL runs.
A rule matches one event property — the user agent by default, but also $ip, $lib,
$host, $pathname, $current_url, $browser, $os, $browser_language,
$screen_width, $screen_height, $geoip_country_code, $referrer, or
$referring_domain — using contains (case-insensitive substring), regex (RE2), or
cidr (an IP range, only valid with $ip). Set name to the label reported by Bot name, and optionally category to a built-in category like ai_crawler to relabel the
traffic type.
Three ways to manage rules:
Settings UI — Settings → Environment → Custom bots.
MCP tools —
web-analytics-bot-rules-list,web-analytics-bot-rules-create,web-analytics-bot-rules-destroy. Prefer these when driving PostHog through an agent.REST API —
GET/POST /api/projects/{project_id}/web_analytics_bot_rules/andDELETE /api/projects/{project_id}/web_analytics_bot_rules/{id}/.
Listing is open to project members; creating and deleting require a project admin
(they mutate the admin-only modifiers team setting). A rule whose pattern can't run is
rejected on save, so a bad rule can never take down the project's classification queries.
Seeing bots that don't run JavaScript
Most crawlers and AI agents never execute JS, so posthog-js never fires a $pageview for
them — they're invisible to client-side analytics. To measure them, the project must forward
server access logs as $http_log events carrying $raw_user_agent. If a user asks "why
don't I see GPTBot when I know it's crawling us?", the answer is almost always: no $http_log
ingestion. Point them at server-side capture (the Vercel logs source, an edge worker, or
the capture API) before building bot insights.
Gotchas
Needs a captured user agent. Classification is computed at query time from the event's
$raw_user_agent/$user_agent, so it works on any historical event — there's no need to restrictdateRange.date_from. The one requirement is that a user agent was captured; events from sources that never set one can't be classified (and empty UAs fall through toAutomation/no_user_agent, below).isLikelyBotis "likely". Detection is a user-agent heuristic — some bots spoof real browser UAs, and some legit tools use bot-like ones. Treat it as best-effort, not ground truth.Empty user agent = bot, except from server and mobile SDKs. Events with no UA classify as
Automation/no_user_agent, unless$libis a server or mobile SDK, which only sends a UA when the app forwards one.Don't silently drop the host filter. If the user is scoped to one domain, inherit
$hostinproperties— leaving it out changes the answer.Bot definitions evolve. The detected-bot list changes over time, so re-running the same query later can classify older events differently.
Files
- ATTRIBUTION.mdAdded by AutoGineer861 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md11.8 KiB
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Python
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- PostHog
- Publisher
- PostHog (Official)
- Source
- PostHog/skills · skills/omnibus/filtering-bot-traffic · commit a5d30e7
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by PostHog or PostHog.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download filtering-bot-traffic.zip