@usesapient/agent-tracker
v0.1.7
Published
Track AI coding agents visiting your site. Works with Next.js, Vercel, Cloudflare, Express, and Netlify DNS proxy setups.
Maintainers
Readme
@usesapient/agent-tracker
Track AI coding agents visiting your documentation. Works with Next.js, Vercel, Cloudflare, Express, and Netlify DNS proxy setups.
Installation
npm install @usesapient/agent-trackerQuick Start
Next.js
// middleware.ts
import { withAgentTracking } from "@usesapient/agent-tracker/next";
export default withAgentTracking({ apiKey: "sap_..." });
export const config = {
matcher: ["/((?!api|_next|admin).*)"],
};Vercel (Remix, SvelteKit, Astro, Nuxt)
// middleware.ts
import { withAgentTracking } from "@usesapient/agent-tracker/vercel";
export default withAgentTracking({ apiKey: "sap_..." });
export const config = {
matcher: "/((?!_next|api|favicon.ico|assets|.*\\..*).)",
};Cloudflare Pages
// functions/_middleware.ts
import { onRequest as withTracking } from "@usesapient/agent-tracker/cloudflare";
export const onRequest = withTracking({ apiKey: "sap_..." });Cloudflare Workers
import { withAgentTracking } from "@usesapient/agent-tracker/cloudflare";
const handler = {
async fetch(request, env, ctx) {
return new Response("Hello!");
},
};
export default {
fetch: withAgentTracking({ apiKey: "sap_..." }, handler.fetch),
};Netlify DNS + Framer/Webflow Proxy
// For Framer/Webflow sites whose custom domain is managed in Netlify DNS:
// netlify/edge-functions/sapient-proxy.ts
import { proxyWithAgentTracking } from "@usesapient/agent-tracker/netlify";
export default proxyWithAgentTracking();
export const config = {
path: "/*",
};Set these environment variables on the Netlify proxy site:
SAPIENT_API_KEY=sap_...
ORIGIN_URL=https://your-site.framer.website
SITE_PROVIDER=framer
SAPIENT_TRACKING_SOURCE=netlify_edge_proxyNetlify DNS-only does not run tracking code. This setup works by pointing the custom domain at a small Netlify proxy site, then forwarding requests to the Framer/Webflow origin.
See examples/netlify-dns-proxy for a minimal Netlify proxy site starter.
Do not set ORIGIN_URL to the custom domain; the proxy rejects that loop.
Express
import express from "express";
import { withAgentTracking } from "@usesapient/agent-tracker/express";
const app = express();
app.use(withAgentTracking({ apiKey: "sap_..." }));Configuration
interface SapientConfig {
/** Your Sapient API key (starts with sap_). Required. */
apiKey: string;
/** Override the track endpoint (for self-hosting). */
endpoint?: string;
/** Where the event was captured. Defaults to sdk. */
trackingSource?: "sdk" | "cloudflare_worker" | "netlify_edge_proxy";
/** Site builder/provider for no-code custom-domain setups. */
provider?: "framer" | "webflow" | "netlify" | "static" | "custom";
}Environment Variables
Instead of passing apiKey directly, you can set:
SAPIENT_API_KEY=sap_...
# or
SAPIENT_TRACKING_KEY=sap_...
# Optional for Cloudflare Worker tracking
SAPIENT_TRACKING_SOURCE=cloudflare_worker
# Optional for Netlify DNS proxy tracking
SAPIENT_TRACKING_SOURCE=netlify_edge_proxy
ORIGIN_URL=https://your-site.framer.website
SITE_PROVIDER=framerThen initialize without config:
// Next.js
import { withAgentTracking } from "@usesapient/agent-tracker/next";
export default withAgentTracking({} as any); // reads from envWrapping Existing Middleware
All platform wrappers accept an optional second argument to wrap existing middleware:
Next.js
import { withAgentTracking } from "@usesapient/agent-tracker/next";
import { NextResponse } from "next/server";
function myMiddleware(request) {
// your existing logic
return NextResponse.next();
}
export default withAgentTracking({ apiKey: "sap_..." }, myMiddleware);Express
// Tracking middleware runs before your routes
app.use(withAgentTracking({ apiKey: "sap_..." }));
app.use(yourOtherMiddleware);How It Works
The middleware intercepts requests and:
- Filters non-page requests — Only tracks requests with
Accept: text/htmlorAccept: text/markdown - Classifies visitors — Detects AI coding agents (Claude Code, Codex, OpenCode) by their unique headers
- Sends async — Tracking is non-blocking via
waitUntil()or fire-and-forget - Auto-creates sites — Sites are automatically registered when first event is received
What Gets Tracked
| Field | Source |
| ------------ | ------------------------------ |
| host | Request hostname |
| path | Request pathname |
| user_agent | User-Agent header |
| accept | Accept header |
| country | Geo header (platform-specific) |
Detection Logic
AI coding agents are identified by requesting text/markdown in their Accept header — browsers never do this.
| Agent | Detection |
| ----------- | -------------------------------------- |
| Claude Code | axios in UA + text/markdown |
| Codex | chatgpt-user in UA + text/markdown |
| OpenCode | Quality weights in Accept (q=0.9) |
API Reference
Core Functions
import { trackVisit, init } from "@usesapient/agent-tracker";
// Initialize globally (optional - can pass config per-call)
init({ apiKey: "sap_..." });
// Track a visit manually
const result = await trackVisit({
host: "docs.example.com",
path: "/getting-started",
userAgent: "...",
accept: "text/html,text/markdown",
country: "US",
});
// => { ok: true, category: "coding-agent", agent: "claude-code" }Platform Exports
Each platform export includes:
// Next.js
import {
withAgentTracking,
trackVisit,
init,
} from "@usesapient/agent-tracker/next";
// Vercel
import {
withAgentTracking,
trackRequest,
trackVisit,
init,
} from "@usesapient/agent-tracker/vercel";
// Cloudflare
import {
withAgentTracking,
onRequest,
trackRequest,
trackVisit,
init,
} from "@usesapient/agent-tracker/cloudflare";
// Netlify
import {
withAgentTracking,
proxyWithAgentTracking,
trackRequest,
trackProxyRequest,
trackVisit,
init,
} from "@usesapient/agent-tracker/netlify";
// Express
import {
withAgentTracking,
trackVisit,
init,
} from "@usesapient/agent-tracker/express";Types
import type {
SapientConfig,
TrackOptions,
TrackResult,
} from "@usesapient/agent-tracker";
interface TrackResult {
ok: boolean;
category?: "coding-agent" | "browsing-agent" | "bot" | "human";
agent?: string;
skipped?: string; // "not-page-view" if filtered
error?: string;
}Performance
- Non-blocking — Uses
waitUntil()on edge platforms, fire-and-forget on Express - Timeout — 2.5s max, silently fails on timeout
- Filtered early — Asset requests (images, CSS, JS) are skipped before any network call
License
MIT
