Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ Server logs have the data, but turning them into analytics is a pipeline project
## What you get

```ts
import { trackDocView, posthogAnalytics } from '@apideck/agent-analytics'
import { trackVisit, posthogAnalytics } from '@apideck/agent-analytics'

const analytics = posthogAnalytics({ apiKey: process.env.POSTHOG_KEY! })

export function middleware(req: NextRequest) {
void trackDocView(req, { analytics }) // ← that's the whole thing
void trackVisit(req, { analytics }) // ← that's the whole thing
return NextResponse.next()
}
```
Expand Down Expand Up @@ -121,11 +121,11 @@ Ships with **PostHog**, **webhook**, and **custom** adapters. BYO analytics.
```ts
// middleware.ts
import {
trackDocView
trackVisit
} from '@apideck/agent-analytics'

export function middleware(req) {
void trackDocView(req, {
void trackVisit(req, {
analytics,
source: 'page-view'
})
Expand Down Expand Up @@ -366,14 +366,14 @@ Full middleware example: [`README.md → Markdown mirror helpers`](./README.md#m
<details>
<summary><b>Will this slow down my middleware?</b></summary>

No. `trackDocView` 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.
No. `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.

</details>

<details>
<summary><b>What if my analytics backend is down?</b></summary>

The adapter call is wrapped in try/catch — `trackDocView` never throws, even if PostHog / your webhook / your custom callback crashes. You lose the event, not the response.
The 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.

</details>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@apideck/agent-analytics",
"version": "0.2.0",
"version": "0.3.0",
"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.",
"keywords": [
"ai",
Expand Down
2 changes: 1 addition & 1 deletion src/bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export interface AgentClassification {
/**
* One-stop classification of a user-agent. Combines {@link isAiBot},
* {@link isHttpClient}, and {@link parseBotName} into a single structured
* result. Used internally by `trackDocView` to populate event properties;
* result. Used internally by `trackVisit` to populate event properties;
* useful in consumer code when you need all signals at once.
*/
export function classifyAgent(userAgent: string | null | undefined): AgentClassification {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { trackDocView } from './track.js'
export { trackVisit } from './track.js'
export {
AI_BOT_PATTERN,
HTTP_CLIENT_PATTERN,
Expand All @@ -16,5 +16,5 @@ export { customAnalytics } from './adapters/custom.js'
export type {
AnalyticsAdapter,
CaptureEvent,
TrackDocViewOptions
TrackVisitOptions
} from './types.js'
6 changes: 3 additions & 3 deletions src/track.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { classifyAgent, isAiBot } from './bots.js'
import { hashId } from './hash.js'
import type { TrackDocViewOptions } from './types.js'
import type { TrackVisitOptions } from './types.js'

/**
* Capture an event describing the incoming request. Fire-and-forget: awaits
Expand All @@ -10,9 +10,9 @@ import type { TrackDocViewOptions } from './types.js'
* When `onlyBots` is true (the default), skips capture unless the UA matches
* {@link AI_BOT_PATTERN}. Set `onlyBots: false` to track every visit.
*/
export async function trackDocView(
export async function trackVisit(
req: Request,
opts: TrackDocViewOptions
opts: TrackVisitOptions
): Promise<void> {
const userAgent = req.headers.get('user-agent') || ''

Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface AnalyticsAdapter {
capture(event: CaptureEvent): Promise<void> | void
}

export interface TrackDocViewOptions {
export interface TrackVisitOptions {
analytics: AnalyticsAdapter
/**
* Label describing how the request arrived (e.g. `'page-view'`, `'md-suffix'`,
Expand Down
22 changes: 11 additions & 11 deletions test/track.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { customAnalytics } from '../src/adapters/custom.js'
import { trackDocView } from '../src/track.js'
import { trackVisit } from '../src/track.js'
import type { CaptureEvent } from '../src/types.js'

function makeRequest(
Expand All @@ -10,14 +10,14 @@ function makeRequest(
return new Request(url, { headers })
}

describe('trackDocView', () => {
describe('trackVisit', () => {
it('captures when the UA is a known AI bot', async () => {
const captured: CaptureEvent[] = []
const analytics = customAnalytics((e) => {
captured.push(e)
})

await trackDocView(
await trackVisit(
makeRequest('https://example.com/docs/intro', {
'user-agent': 'ClaudeBot/1.0',
'x-forwarded-for': '1.2.3.4',
Expand Down Expand Up @@ -46,7 +46,7 @@ describe('trackDocView', () => {

it('sets bot_name to Browser for human traffic when onlyBots is false', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', {
'user-agent': 'Mozilla/5.0 (Macintosh) Chrome/120'
}),
Expand All @@ -61,7 +61,7 @@ describe('trackDocView', () => {

it('sets coding_agent_hint and ua_category for HTTP-library UAs (onlyBots: false)', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/docs/intro', { 'user-agent': 'curl/8.4.0' }),
{ analytics: customAnalytics(spy), onlyBots: false }
)
Expand All @@ -76,7 +76,7 @@ describe('trackDocView', () => {

it('skips capture when UA is not a bot and onlyBots is on (default)', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', {
'user-agent': 'Mozilla/5.0 (Macintosh) Chrome/120'
}),
Expand All @@ -87,7 +87,7 @@ describe('trackDocView', () => {

it('captures every request when onlyBots is false', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', {
'user-agent': 'Mozilla/5.0 (Macintosh) Chrome/120'
}),
Expand All @@ -100,7 +100,7 @@ describe('trackDocView', () => {

it('honours a custom event name', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', { 'user-agent': 'ClaudeBot/1.0' }),
{ analytics: customAnalytics(spy), eventName: 'agent_fetch' }
)
Expand All @@ -113,7 +113,7 @@ describe('trackDocView', () => {
throw new Error('downstream offline')
})
await expect(
trackDocView(
trackVisit(
makeRequest('https://example.com/page', { 'user-agent': 'ClaudeBot' }),
{ analytics }
)
Expand All @@ -122,7 +122,7 @@ describe('trackDocView', () => {

it('uses the first x-forwarded-for value when multiple are present', async () => {
const spy = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', {
'user-agent': 'ClaudeBot',
'x-forwarded-for': '203.0.113.1, 10.0.0.1'
Expand All @@ -133,7 +133,7 @@ describe('trackDocView', () => {
const b = (
await (async () => {
const spy2 = vi.fn()
await trackDocView(
await trackVisit(
makeRequest('https://example.com/page', {
'user-agent': 'ClaudeBot',
'x-forwarded-for': '203.0.113.1, 10.0.0.2'
Expand Down
Loading