@usesapient/agent-tracker
v0.1.10
Published
Track AI coding agents visiting your site. Works with Next.js, Vercel, Cloudflare, Express, and Netlify DNS proxy setups.
Downloads
983
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
Browser Visitor Identity
Add the browser helper once in your client app to identify repeat human visitors with Sapient's first-party browser fingerprint. It writes visitor/session and first-touch attribution cookies that the server middleware below automatically attaches to tracking events.
import {
identifyUser,
initVisitorIdentity,
trackConversion,
} from "@usesapient/agent-tracker/browser";
void initVisitorIdentity();
// After login/signup, attach your own application user ID.
void identifyUser("user_123");
// Track explicit outcomes when they happen.
void trackConversion("signup.completed", { plan: "pro" }, {
apiKey: "sap_...",
});The helper has no third-party dependency. It falls back to storage/random IDs
when browser fingerprinting is unavailable. The userId value is your customer
application's user ID, not a Sapient account ID.
Conversion Tracking
Use conversions for outcomes you care about: sign-up intent, sign-up complete, demo requested, checkout complete, API key created. Sapient does not need every click on the site to close the attribution loop.
Programmatic conversion:
import { trackConversion } from "@usesapient/agent-tracker/browser";
await trackConversion(
"signup.completed",
{ plan: "pro" },
{
apiKey: "sap_...",
eventStage: "completed",
},
);Declarative pixel for CTA clicks:
<script
type="module"
src="https://cdn.example.com/sapient-browser.mjs"
data-sapient-key="sap_..."
></script>
<button
data-sapient-conversion="signup.click"
data-sapient-conversion-stage="intent"
data-sapient-label="Sign in"
>
Sign in
</button>For first-party deployments, point conversionEndpoint or
data-sapient-endpoint at your own proxy and add the Sapient API key on the
server instead of exposing it in the browser.
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";
/** Apex domain used to ignore self-referrals across subdomains. */
siteDomain?: string;
}Browser Identity Options
import { initVisitorIdentity } from "@usesapient/agent-tracker/browser";
void initVisitorIdentity({
cookieDomain: ".example.com", // optional
siteDomain: "example.com", // recommended when using subdomains
userId: "user_123", // optional, can also call identifyUser later
apiKey: "sap_...", // optional, only needed for direct browser conversions
conversionEndpoint: "/api/sapient/conversion", // optional first-party proxy
visitorMaxAgeDays: 365,
sessionMaxAgeMinutes: 30,
cookieSameSite: "Lax",
});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
- Identifies browsers — Reads Sapient visitor/session cookies produced by the browser helper
- Preserves attribution — Captures current, session, and first-touch attribution while ignoring configured same-site referrers
- Sends async — Tracking is non-blocking via
waitUntil()or fire-and-forget - Auto-creates sites — Sites are automatically registered when first event is received
- Records outcomes — Browser helpers can send explicit conversion milestones tied to the same visitor/user
What Gets Tracked
| Field | Source |
| ------------------------ | ------------------------------ |
| host | Request hostname |
| path | Request pathname |
| user_agent | User-Agent header |
| accept | Accept header |
| country | Geo header (platform-specific) |
| visitor_id | Browser identity cookie/header |
| session_id | Browser identity cookie/header |
| fingerprint_hash | Browser identity cookie/header |
| fingerprint_source | Browser identity cookie/header |
| fingerprint_confidence | Browser identity cookie/header |
| external_user_id | Browser identity cookie/header |
| attribution_source | First-touch cookie/header |
| attribution_medium | First-touch cookie/header |
| attribution_campaign | First-touch cookie/header |
| attribution_referrer | First-touch cookie/header |
| attribution_landing_page | First-touch cookie/header |
| first_touch | First valid acquisition touch |
| session_touch | Source that began the session |
| current_touch | Source resolved from this request |
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",
visitorId: "visitor_...",
sessionId: "session_...",
externalUserId: "user_123",
attributionSource: "chatgpt",
attributionCampaign: "agent_analytics_demo",
});
// => { ok: true, category: "coding-agent", agent: "claude-code" }Browser Functions
import {
clearUserIdentity,
getVisitorIdentity,
identifyUser,
initSapientPixel,
initVisitorIdentity,
trackConversion,
} from "@usesapient/agent-tracker/browser";
const identity = await getVisitorIdentity({ siteDomain: "example.com" });
// => { visitorId, sessionId, firstTouch, sessionTouch, currentTouch, ... }
void initVisitorIdentity(); // fire-and-forget cookie initializer
await identifyUser("user_123");
await trackConversion("signup.completed", { plan: "pro" });
void initSapientPixel({ apiKey: "sap_..." }); // enables data-sapient-conversion clicks
clearUserIdentity();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";
// Browser
import {
clearUserIdentity,
getVisitorIdentity,
identifyUser,
initSapientPixel,
initVisitorIdentity,
trackConversion,
} from "@usesapient/agent-tracker/browser";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;
}Browser identity cookies can also be supplied as headers when you proxy requests:
x-sapient-visitor-id, x-sapient-session-id,
x-sapient-fingerprint-hash, x-sapient-fingerprint-source, and
x-sapient-fingerprint-confidence.
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
