skills/filtering-bot-traffic/SKILL.md
Identify, measure, and exclude bot / crawler / AI-agent traffic in PostHog web and product analytics using the traffic classification surface (the isLikelyBot / getTrafficType HogQL functions and the $virt_* virtual properties). Use when the user asks to "exclude bots", "filter out crawlers", "remove bot traffic from my numbers", "how much of my traffic is bots / AI crawlers", "is GPTBot / ChatGPT / Claude hitting my site", "break down traffic by human vs bot", or wants clean human-only counts in an insight or dashboard. For the real-time Live tab bot tiles, use exploring-live-traffic instead.
npx skillsauth add posthog/ai-plugin filtering-bot-trafficInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
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.
Use it when the user wants to:
Do not use it for the Live tab, real-time numbers, or the per-minute bot charts — that is exploring-live-traffic.
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.
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 |
Pass the user agent explicitly. Use coalesce(nullIf(properties.$raw_user_agent, ''), properties.$user_agent)
to cover both server-side ($raw_user_agent) and JS SDK ($user_agent) captures. The nullIf
keeps an empty $raw_user_agent from shadowing a real $user_agent and being misread as a bot —
this mirrors the expression the virtual properties use internally.
| Function | Returns |
| ------------------------ | ---------------------------------------------------------------------------- |
| isLikelyBot(ua) | true if the UA matches a bot/automation pattern (empty UA counts as a bot) |
| 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 |
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 |
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.
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" }
}
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" }
}
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.
-- human pageviews only
SELECT count() AS human_pageviews
FROM events
WHERE event = '$pageview'
AND NOT isLikelyBot(coalesce(nullIf(properties.$raw_user_agent, ''), properties.$user_agent))
-- top bots by hits
SELECT
getBotName(coalesce(nullIf(properties.$raw_user_agent, ''), properties.$user_agent)) AS bot,
getBotOperator(coalesce(nullIf(properties.$raw_user_agent, ''), properties.$user_agent)) AS operator,
count() AS hits
FROM events
WHERE event = '$pageview'
AND isLikelyBot(coalesce(nullIf(properties.$raw_user_agent, ''), properties.$user_agent))
GROUP BY bot, operator
ORDER BY hits DESC
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.
$raw_user_agent / $user_agent, so it works on any historical event — there's no need to
restrict dateRange.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 to
Automation / no_user_agent, below).isLikelyBot is "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.Automation / no_user_agent, so isLikelyBot returns true.$host in properties — leaving it out changes the answer.products/web_analytics/backend/hogql_queries/bot_definitions.py.data-ai
Signals scout for PostHog Tasks, the agent work items a project runs. Two lenses: delivery health (runs failing, clustered by repository and error class, and retry storms) every run, and on a slower rotation demand (recurring asks across human-authored tasks that point at a product gap). Skips the scout fleet's own run rows.
devops
Signals scout for the PostHog Conversations (support inbox) product. Watches the `$conversation_*` ticket-lifecycle events for support-delivery regressions — SLA breach-rate steps, first-response latency blowouts, backlog inflow-vs-resolution imbalance, and channel / assignment concentration — and files each dated regression as a report. Complements the per-ticket product-feedback signals the emission pipeline already fires; does not re-surface individual ticket content.
development
Populates and maintains a project's data catalog (semantic layer): canonical metrics, trust marks (certifications) on warehouse tables/views, and reviewed table relationships. Use when asked to set up / seed / bootstrap the data catalog or semantic layer, to catalog a project's metrics, to certify or deprecate data sources, to propose or review table joins, or to work through the proposal review queue. To *use* an existing catalog to answer a business-number question, see querying-posthog-data instead. Trigger terms: data catalog, semantic layer, canonical metric, certify table, deprecate source, relationship proposal, metric drift, review queue.
tools
Investigate logs in a PostHog project: verify a service or deployment is healthy, explain an error spike, triage an incident, or understand what a log stream is saying. Use when the user asks to "check the logs", asks whether a service, deploy, release, or change is working or broke anything, asks why errors are up or what changed, or wants the root cause of failures visible in logs. Routes the logs MCP tools (services overview, pattern mining, before/after pattern diffing, bucketed counts, facets, raw rows) so investigations start from summaries instead of raw rows or hand-written SQL over the logs table.