{"id":49182928,"url":"https://github.com/apideck-libraries/agent-analytics","last_synced_at":"2026-04-26T05:00:43.711Z","repository":{"id":352371169,"uuid":"1214873459","full_name":"apideck-libraries/agent-analytics","owner":"apideck-libraries","description":"Track AI agent and bot traffic to your Next.js / Vercel app — PostHog, webhooks, or any custom analytics backend. Detects Claude, ChatGPT, Perplexity, Google-Extended, and more.","archived":false,"fork":false,"pushed_at":"2026-04-21T10:14:58.000Z","size":161,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-23T02:26:25.772Z","etag":null,"topics":["ai-agents","analytics","vercel"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apideck-libraries.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T06:55:54.000Z","updated_at":"2026-04-21T10:14:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/apideck-libraries/agent-analytics","commit_stats":null,"previous_names":["apideck-libraries/agent-analytics"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/apideck-libraries/agent-analytics","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apideck-libraries%2Fagent-analytics","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apideck-libraries%2Fagent-analytics/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apideck-libraries%2Fagent-analytics/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apideck-libraries%2Fagent-analytics/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apideck-libraries","download_url":"https://codeload.github.com/apideck-libraries/agent-analytics/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apideck-libraries%2Fagent-analytics/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32207191,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T01:12:49.758Z","status":"online","status_checked_at":"2026-04-24T02:00:07.115Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-agents","analytics","vercel"],"created_at":"2026-04-23T02:04:25.218Z","updated_at":"2026-04-24T03:01:08.952Z","avatar_url":"https://github.com/apideck-libraries.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# agent-analytics\n\n### See the agents your JavaScript can't.\n\n**Drop-in Next.js / Vercel middleware that tracks ClaudeBot, GPTBot, Perplexity, and 20+ AI crawlers in PostHog — or any analytics backend you already pay for.**\n\n[![npm version](https://img.shields.io/npm/v/@apideck/agent-analytics.svg?style=flat-square\u0026color=2563eb)](https://www.npmjs.com/package/@apideck/agent-analytics)\n[![npm downloads](https://img.shields.io/npm/dm/@apideck/agent-analytics.svg?style=flat-square\u0026color=2563eb)](https://www.npmjs.com/package/@apideck/agent-analytics)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@apideck/agent-analytics?style=flat-square\u0026color=2563eb\u0026label=gzipped)](https://bundlephobia.com/package/@apideck/agent-analytics)\n[![CI](https://img.shields.io/github/actions/workflow/status/apideck-libraries/agent-analytics/ci.yml?style=flat-square\u0026label=ci)](https://github.com/apideck-libraries/agent-analytics/actions)\n[![license](https://img.shields.io/npm/l/@apideck/agent-analytics?style=flat-square\u0026color=2563eb)](./LICENSE)\n[![typescript](https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square)](./tsconfig.json)\n\n[**Install**](#install) · [**Quick start**](#quick-start-60-seconds-to-your-first-event) · [**How it works**](#how-it-works) · [**Adapters**](#built-in-adapters) · [**Markdown mirror**](#advanced-markdown-mirror-for-docs-sites) · [**FAQ**](#faq)\n\n\u003c/div\u003e\n\n---\n\n## The problem\n\nClient-side analytics libraries run in the browser. AI crawlers don't. That means **every time ClaudeBot, GPTBot, or Perplexity fetches a page on your site, your dashboard stays empty.**\n\nYou can't see:\n\n- Which AI agents are reading your docs, marketing pages, or blog\n- Which pages they actually fetch (vs. which you *think* they should)\n- Which agent-driven referrals convert\n- How much of your \"traffic\" is actually LLM training pipelines\n\nServer logs have the data, but turning them into analytics is a pipeline project. This library is the one-line version.\n\n## What you get\n\n```ts\nimport { trackVisit, posthogAnalytics } from '@apideck/agent-analytics'\n\nconst analytics = posthogAnalytics({ apiKey: process.env.POSTHOG_KEY! })\n\nexport function middleware(req: NextRequest) {\n  void trackVisit(req, { analytics })   // ← that's the whole thing\n  return NextResponse.next()\n}\n```\n\nOne line of middleware. Fire-and-forget. Zero impact on your response latency. Events in PostHog within seconds.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhat shows up in your analytics\u003c/b\u003e (click to expand)\u003c/summary\u003e\n\n```jsonc\n{\n  \"event\": \"doc_view\",\n  \"distinct_id\": \"anon_7f3a1b2c\",          // hashed ip:ua, no person profile\n  \"timestamp\": \"2026-04-19T08:30:00.000Z\",\n  \"properties\": {\n    \"$process_person_profile\": false,       // PostHog: don't create a person\n    \"$current_url\": \"https://example.com/docs/intro\",\n    \"path\": \"/docs/intro\",\n    \"user_agent\": \"ClaudeBot/1.0 (+https://claude.ai/bot)\",\n    \"is_ai_bot\": true,                      // strict: matches a branded AI crawler\n    \"bot_name\": \"Claude\",                   // 'Claude' | 'ChatGPT' | ... | 'curl' | 'axios' | 'Electron' | 'Browser' | 'Other'\n    \"ua_category\": \"declared-crawler\",      // 'declared-crawler' | 'coding-agent-hint' | 'browser' | 'other'\n    \"coding_agent_hint\": false,             // loose: HTTP-library / automation UA (curl, axios, got, colly, Electron, ...)\n    \"referer\": \"https://claude.ai/\",\n    \"source\": \"page-view\"                   // whatever label you passed\n  }\n}\n```\n\nNow you can build:\n- **AI-vs-human traffic ratio** over time\n- **Breakdown by agent** (Claude vs ChatGPT vs Perplexity vs Google-Extended)\n- **Top pages for agents** (what do they actually read?)\n- **Conversion funnels** from agent referral → human visit → sign-up\n- **Anomaly detection** when a new bot starts hammering your site\n\n\u003c/details\u003e\n\n---\n\n## Install\n\n```bash\nnpm install @apideck/agent-analytics\n# or\npnpm add @apideck/agent-analytics\n# or\nyarn add @apideck/agent-analytics\n```\n\nZero dependencies. Runs on Node 18+, Edge, Bun, and anywhere the Web Fetch API exists.\n\n## Quick start (60 seconds to your first event)\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd width=\"33%\" valign=\"top\"\u003e\n\n### 1. Pick an adapter\n\n```ts\nimport {\n  posthogAnalytics\n} from '@apideck/agent-analytics'\n\nconst analytics = posthogAnalytics({\n  apiKey: process.env.POSTHOG_KEY!\n})\n```\n\nShips with **PostHog**, **webhook**, and **custom** adapters. BYO analytics.\n\n\u003c/td\u003e\n\u003ctd width=\"33%\" valign=\"top\"\u003e\n\n### 2. Wire the middleware\n\n```ts\n// middleware.ts\nimport {\n  trackVisit\n} from '@apideck/agent-analytics'\n\nexport function middleware(req) {\n  void trackVisit(req, {\n    analytics,\n    source: 'page-view'\n  })\n  return NextResponse.next()\n}\n```\n\nWorks in any middleware that hands you a `Request`.\n\n\u003c/td\u003e\n\u003ctd width=\"33%\" valign=\"top\"\u003e\n\n### 3. Ship it\n\n```bash\nvercel --prod\n```\n\nHit any page with a spoofed UA:\n\n```bash\ncurl -A \"ClaudeBot/1.0\" \\\n  https://yoursite.com/\n```\n\nEvent lands in PostHog in seconds.\n\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## How it works\n\n```text\n                   Request                   Response (unchanged)\n   Agent ─────────────────────►  middleware ───────────────────► Agent\n                                      │\n                                      │ fire-and-forget\n                                      │ keepalive: true\n                                      ▼\n                            ┌──────────────────┐\n                            │  AnalyticsAdapter │\n                            │  (PostHog /       │\n                            │   webhook /       │\n                            │   custom fn)      │\n                            └──────────────────┘\n```\n\nThe middleware call:\n\n1. **Reads UA** from `req.headers.get('user-agent')`\n2. **Matches against** `AI_BOT_PATTERN` (ClaudeBot, GPTBot, PerplexityBot, Google-Extended, Applebot-Extended, CCBot, Bytespider, Amazonbot, Meta-ExternalAgent, MistralAI-User, Cursor, Windsurf, and more)\n3. **Hashes `ip:ua`** with djb2 → stable anon distinct_id (same bot from same network = same visitor, no PII)\n4. **Posts to your adapter** with `keepalive: true` so the request survives after the response returns\n5. **Swallows errors** — a downed analytics backend never breaks your response\n\nBy default every request is captured so coding-agent traffic (axios, curl, Electron, …) surfaces alongside branded crawlers. Pass `onlyBots: true` to restrict capture to UAs matching the built-in AI bot pattern.\n\n---\n\n## Who's detected out of the box\n\n\u003ctable\u003e\n\u003ctr\u003e\u003cth\u003eAgent\u003c/th\u003e\u003cth\u003eUA signature\u003c/th\u003e\u003cth\u003ebot_name label\u003c/th\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eAnthropic\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eClaudeBot\u003c/code\u003e, \u003ccode\u003eClaude-User\u003c/code\u003e, \u003ccode\u003eAnthropic-*\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eClaude\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eOpenAI\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eChatGPT-User\u003c/code\u003e, \u003ccode\u003eGPTBot\u003c/code\u003e, \u003ccode\u003eOAI-SearchBot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eChatGPT\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003ePerplexity\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003ePerplexityBot\u003c/code\u003e, \u003ccode\u003ePerplexity-User\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003ePerplexity\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eGoogle\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eGoogle-Extended\u003c/code\u003e, \u003ccode\u003eGooglebot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eGoogle\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eApple\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eApplebot-Extended\u003c/code\u003e, \u003ccode\u003eApplebot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eApple\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eMeta\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eMeta-ExternalAgent\u003c/code\u003e, \u003ccode\u003eFacebookBot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eMeta\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eAmazon\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eAmazonbot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eAmazon\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eBytedance\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eBytespider\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eBytespider\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eCommon Crawl\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eCCBot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eCommon Crawl\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eMistral\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eMistralAI-User\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eMistral\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eCohere\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003ecohere-ai\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eCohere\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eDuckDuckGo\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eDuckAssistBot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eDuckDuckGo\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eYou.com\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eYouBot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eYou.com\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eAI2\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eAI2Bot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eAI2\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eDiffbot\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eDiffbot\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eDiffbot\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cb\u003eCoding agents\u003c/b\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eCursor\u003c/code\u003e, \u003ccode\u003eWindsurf\u003c/code\u003e\u003c/td\u003e\u003ctd\u003e\u003ccode\u003eCursor\u003c/code\u003e / \u003ccode\u003eWindsurf\u003c/code\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003c/table\u003e\n\nNew agents appear every month. Patch releases ship as the list grows — watch the repo for updates. Raise a PR if you spot one we're missing.\n\n### Coding agents (loose detection — `coding_agent_hint: true`)\n\nCoding agents like Claude Code, Cline, Cursor, and Windsurf **don't identify themselves by name** in their user agent. They use whatever HTTP library they're built on, so detection is a loose heuristic — the UAs below are *also* used by legitimate curl scripts, CI jobs, and server-to-server traffic.\n\n`is_ai_bot` stays `false` for these so your strict AI-traffic segment is clean. The `coding_agent_hint` property is the wider net; pair it with other signals (path patterns, [JA4 fingerprints via Vercel Log Drains](https://vercel.com/docs/observability/log-drains), HEAD-then-GET request shape) when you need higher confidence.\n\n| Agent | Signature observed | `bot_name` |\n|---|---|---|\n| Claude Code | `axios/1.8.4` | `axios` |\n| Cline / Junie | `curl/8.4.0` | `curl` |\n| Cursor | `got (sindresorhus/got)` | `got` |\n| Windsurf | `colly` (Go) | `colly` |\n| VS Code | `Electron/` marker | `Electron` |\n| Other automation | `node-fetch`, `python-requests`, `Go-http-client`, `okhttp`, `aiohttp`, `Deno` | exact library name |\n\nPlaywright-based agents (Aider, OpenCode) spoof full Mozilla/Safari UAs and are **indistinguishable from real browsers by UA alone**. They'll show up as `bot_name: Browser`, `ua_category: browser`. Catching those needs TLS fingerprinting (JA4) or behavioural analysis.\n\nCredit: coding-agent signatures catalogued by [Addy Osmani](https://addyosmani.com/blog/agentic-engine-optimization/).\n\n---\n\n## Built-in adapters\n\n### `posthogAnalytics`\n\n```ts\nimport { posthogAnalytics } from '@apideck/agent-analytics'\n\nconst analytics = posthogAnalytics({\n  apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY!,\n  host: 'https://eu.i.posthog.com'        // optional; defaults to US cloud\n})\n```\n\nHost can be the PostHog cloud (`us.i.posthog.com`, `eu.i.posthog.com`) **or** your own reverse-proxy domain (e.g. `https://svc.yourdomain.com`) to dodge ad-blockers. Scheme is optional — both `'https://host'` and `'host'` work.\n\n### `webhookAnalytics`\n\n```ts\nimport { webhookAnalytics } from '@apideck/agent-analytics'\n\nconst analytics = webhookAnalytics({\n  url: 'https://collector.example.com/events',\n  headers: { Authorization: `Bearer ${process.env.TOKEN}` },\n  transform: (event) =\u003e ({              // optional: reshape for your backend\n    type: event.event,\n    user: event.distinctId,\n    ...event.properties\n  })\n})\n```\n\n### `customAnalytics`\n\n```ts\nimport { customAnalytics } from '@apideck/agent-analytics'\nimport { Mixpanel } from 'mixpanel'\n\nconst mp = Mixpanel.init(process.env.MIXPANEL_TOKEN!)\n\nconst analytics = customAnalytics((event) =\u003e {\n  mp.track(event.event, { distinct_id: event.distinctId, ...event.properties })\n})\n```\n\nAny `{ capture(event): Promise\u003cvoid\u003e | void }` object is a valid adapter. Compose multiple by fanning out in a custom callback.\n\n---\n\n## Advanced: Markdown mirror for docs sites\n\nContent-heavy sites should serve **clean Markdown** when an agent asks for it — that's what makes your docs actually useful to coding agents, not just indexable. The `/markdown` subpath exports the helpers that power [developers.apideck.com](https://developers.apideck.com)'s agent-readiness stack:\n\n```ts\nimport {\n  markdownServeDecision,   // decide if this request should get Markdown\n  markdownHeaders,         // Content-Type, Content-Signal, x-markdown-tokens\n  synthesizeMarkdownPointer // fallback for URLs without a mirror\n} from '@apideck/agent-analytics/markdown'\n```\n\nThree triggers, one decision helper:\n\n| Trigger | Example | `reason` |\n|---|---|---|\n| AI-bot UA on any URL | `curl -A ClaudeBot /docs/intro` | `ua-rewrite` |\n| `.md` suffix | `curl /docs/intro.md` | `md-suffix` |\n| `Accept: text/markdown` header | `curl -H \"Accept: text/markdown\" /docs/intro` | `accept-header` |\n\nFull middleware example: [`README.md → Markdown mirror helpers`](./README.md#markdown-mirror-helpers) section, or copy from [the reference implementation](https://github.com/apideck-io/developer-docs/blob/main/src/middleware.ts).\n\n---\n\n## Compared to…\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003cth\u003e\u003c/th\u003e\n\u003cth align=\"center\"\u003e@apideck/agent-analytics\u003c/th\u003e\n\u003cth align=\"center\"\u003eDIY middleware\u003c/th\u003e\n\u003cth align=\"center\"\u003eDark Visitors SaaS\u003c/th\u003e\n\u003cth align=\"center\"\u003eCloudflare AI Labyrinth\u003c/th\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eTracks agents in your analytics\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓ (after N hours of glue code)\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓ (external dashboard)\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗ (it blocks them instead)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eReuses your analytics backend\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓ PostHog / webhook / any\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗ (their dashboard)\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eZero runtime dependencies\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗ (SaaS)\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗ (Cloudflare)\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eShips maintained UA list\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eMarkdown-mirror helpers\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✓\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗\u003c/td\u003e\n\u003ctd align=\"center\"\u003e✗\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cb\u003eMonthly cost\u003c/b\u003e\u003c/td\u003e\n\u003ctd align=\"center\"\u003e$0\u003c/td\u003e\n\u003ctd align=\"center\"\u003e$0 + engineering time\u003c/td\u003e\n\u003ctd align=\"center\"\u003e$$$\u003c/td\u003e\n\u003ctd align=\"center\"\u003eRequires CF plan\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n---\n\n## FAQ\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWill this slow down my middleware?\u003c/b\u003e\u003c/summary\u003e\n\nNo. `trackVisit` returns a promise you don't await, and the underlying `fetch` uses `keepalive: true` — the browser / runtime guarantees the request completes after your response returns. Your critical path is: `req.headers.get('user-agent')` + a regex test + a `void fetch(...)`. Sub-millisecond.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhat if my analytics backend is down?\u003c/b\u003e\u003c/summary\u003e\n\nThe adapter call is wrapped in try/catch — `trackVisit` never throws, even if PostHog / your webhook / your custom callback crashes. You lose the event, not the response.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eDoes this create PostHog person profiles for every bot?\u003c/b\u003e\u003c/summary\u003e\n\nNo. The event includes `$process_person_profile: false`, which tells PostHog to skip profile creation. Distinct IDs are djb2 hashes of `ip:ua`, so same-bot-same-network collapses into one anonymous visitor for journey analysis, but no \"person\" row gets created.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eHow do I detect a bot I added to the UA list in my own code?\u003c/b\u003e\u003c/summary\u003e\n\n```ts\nimport { isAiBot, parseBotName } from '@apideck/agent-analytics'\n\nif (isAiBot(req.headers.get('user-agent'))) {\n  // serve Markdown, skip personalisation, add rate limits, etc.\n}\nparseBotName('ClaudeBot/1.0')  // → 'Claude'\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eCan I use this outside Next.js?\u003c/b\u003e\u003c/summary\u003e\n\nYes. The primary API takes a standard Web Fetch `Request` object. Works in Hono, Bun, Cloudflare Workers, Deno Deploy, Node 18+ HTTP handlers — anywhere you can get a `Request`.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWhy not just enable PostHog's bot filtering?\u003c/b\u003e\u003c/summary\u003e\n\nPostHog's bot filter excludes bots from your metrics. This library does the opposite: it makes bots *visible* so you can analyse them deliberately. Complementary — segment by `is_ai_bot` to split the populations.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eIs the UA list going to go stale?\u003c/b\u003e\u003c/summary\u003e\n\nAI crawlers keep appearing. We publish patch releases whenever the list changes — `npm update @apideck/agent-analytics` picks them up. If you spot a missing agent, send a PR with a link to the bot's official docs; merges ship the same day.\n\n\u003c/details\u003e\n\n---\n\n## Who uses this\n\n- **[developers.apideck.com](https://developers.apideck.com)** — extracted from and battle-tested on the Apideck developer documentation site\n- *Your company here — send a PR*\n\n---\n\n## Roadmap\n\n- [ ] Runtime UA list fetching (opt-in) so patches land without a dependency bump\n- [ ] First-class Mixpanel, Amplitude, Segment adapters\n- [ ] Vercel Marketplace one-click install\n- [ ] Pre-built PostHog dashboards (JSON export) for AI-vs-human, agent leaderboard, top-pages-per-agent\n- [ ] `createMarkdownMiddleware()` — a batteries-included Next.js middleware for the full agent-readiness stack\n\nFile a [feature request](https://github.com/apideck-libraries/agent-analytics/issues/new) if something's missing from your setup.\n\n---\n\n## Contributing\n\nPRs welcome — especially new UA signatures, adapters, and docs.\n\n```bash\ngit clone https://github.com/apideck-libraries/agent-analytics\ncd agent-analytics\nnpm install\nnpm test\n```\n\n### Releasing\n\nPublishing to npm is fully automated by two workflows:\n\n1. **`release.yml`** watches `package.json` on `main`. When the version field\n   bumps to something without a matching `v\u003cversion\u003e` tag, it creates the\n   GitHub Release.\n2. **`publish.yml`** fires on `release: published`, runs typecheck + tests +\n   build, and publishes to npm with `--provenance` via OIDC trusted\n   publishing (no secrets required).\n\nSo cutting a release is just:\n\n```bash\n# Pick a level (patch | minor | major), or edit package.json directly.\nnpm version patch\ngit push\n```\n\nThe push lands on main, `release.yml` notices the new version, cuts the\nrelease, and `publish.yml` publishes. No CLI juggling, no secrets to manage.\n\nOIDC trusted publishing is configured at\n[npmjs.com/package/@apideck/agent-analytics/access](https://www.npmjs.com/package/@apideck/agent-analytics/access)\n— the GitHub repo + `publish.yml` workflow are registered as the sole\ntrusted publisher.\n\n## Credits\n\nBuilt on learnings from:\n\n- [Agentic Engine Optimization](https://addyosmani.com/blog/agentic-engine-optimization/) by Addy Osmani — the case for making sites agent-ready\n- [contentsignals.org](https://contentsignals.org) — the Content-Signal spec\n- [darkvisitors.com](https://darkvisitors.com) — maintained catalogue of AI user-agents we cross-reference\n\n## License\n\n[MIT](./LICENSE) © [Apideck](https://apideck.com)\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\u003csub\u003eBuilt by \u003ca href=\"https://apideck.com\"\u003eApideck\u003c/a\u003e — the unified API platform for integrations.\u003c/sub\u003e\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapideck-libraries%2Fagent-analytics","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapideck-libraries%2Fagent-analytics","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapideck-libraries%2Fagent-analytics/lists"}