@lightsage/agent-tracker
v0.1.19
Published
Track AI agent visits, and revenue attribution for your site. Works with Next.js, Vercel, Cloudflare, Express, and Netlify DNS proxy setups.
Maintainers
Readme
@lightsage/agent-tracker
Track AI coding agents visiting your documentation. Works with Next.js, Vercel, Cloudflare, Express, and Netlify DNS proxy setups.
New Agent Analytics keys start with ls_track_. Existing Unkey-issued
sap_ keys remain accepted during the compatibility migration; no SDK update
is required when an existing key continues to be used.
Installation
npm install @lightsage/agent-trackerQuick Start
Browser Visitor Identity
Add the browser helper once in your client app to identify repeat human visitors with Lightsage'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 "@lightsage/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: "ls_track_...",
label: "Complete sign up",
eventStage: "completed",
});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 Lightsage account ID.
Conversion Tracking
Use conversions for outcomes you care about: sign-up intent, sign-up complete, demo requested, checkout complete, API key created. Lightsage does not need every click on the site to close the attribution loop.
Programmatic conversion:
import { trackConversion } from "@lightsage/agent-tracker/browser";
await trackConversion(
"signup.completed",
{ plan: "pro" },
{
apiKey: "ls_track_...",
label: "Sign up",
eventStage: "completed",
},
);label and eventStage are required. The label is stored as first-class
conversion event data while eventName remains the stable aggregation key.
Declarative pixel for CTA clicks:
<script
type="module"
src="https://cdn.example.com/lightsage-browser.mjs"
data-lightsage-key="ls_track_..."
></script>
<button
data-lightsage-conversion="signup.click"
data-lightsage-conversion-stage="intent"
data-lightsage-label="Sign in"
>
Sign in
</button>For first-party deployments, point conversionEndpoint or
data-lightsage-endpoint at your own proxy and add the Lightsage API key on the
server instead of exposing it in the browser.
Visitor Journey Page Views
initLightsagePixel records page views for visitors attributed to an AI agent,
on page load and on client-side navigation, so each conversion shows the pages
that led to it. Visitors without agent attribution send nothing, and these page
views never change your visit counts. A visitor counts as agent-attributed when
their first, session, or current touch came from an agent.
Automatic capture runs when an apiKey or pageViewEndpoint is configured. Set
autoCapturePageViews: false to turn it off, and call trackPageView yourself
for custom routers:
import { trackPageView } from "@lightsage/agent-tracker/browser";
await trackPageView({ apiKey: "ls_track_..." });For first-party deployments, point pageViewEndpoint at your own proxy to
/api/v1/agent-tracking/page-view.
The data-sapient-* attributes remain deprecated compatibility aliases for
existing deployments. New markup should use data-lightsage-*.
Next.js
// middleware.ts
import { withAgentTracking } from "@lightsage/agent-tracker/next";
export default withAgentTracking({ apiKey: "ls_track_..." });
export const config = {
matcher: ["/((?!api|_next|admin).*)"],
};Vercel (Remix, SvelteKit, Astro, Nuxt)
// middleware.ts
import { withAgentTracking } from "@lightsage/agent-tracker/vercel";
export default withAgentTracking({ apiKey: "ls_track_..." });
export const config = {
matcher: "/((?!_next|api|favicon.ico|assets|.*\\..*).*)",
};Cloudflare Pages
// functions/_middleware.ts
import { onRequest as withTracking } from "@lightsage/agent-tracker/cloudflare";
export const onRequest = withTracking({ apiKey: "ls_track_..." });Cloudflare Workers
import { withAgentTracking } from "@lightsage/agent-tracker/cloudflare";
const handler = {
async fetch(request, env, ctx) {
return new Response("Hello!");
},
};
export default {
fetch: withAgentTracking({ apiKey: "ls_track_..." }, 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 "@lightsage/agent-tracker/netlify";
export default proxyWithAgentTracking();
export const config = {
path: "/*",
};Set these environment variables on the Netlify proxy site:
LIGHTSAGE_API_KEY=ls_track_...
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 "@lightsage/agent-tracker/express";
const app = express();
app.use(withAgentTracking({ apiKey: "ls_track_..." }));Configuration
interface LightsageConfig {
/** Your Lightsage Agent Analytics key (normally starts with ls_track_). 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 "@lightsage/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: "ls_track_...", // 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:
LIGHTSAGE_API_KEY=ls_track_...
# 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=framerSAPIENT_API_KEY and SAPIENT_TRACKING_KEY remain deprecated compatibility
aliases for existing deployments. New integrations should use
LIGHTSAGE_API_KEY.
Then initialize without config:
// Next.js
import { withAgentTracking } from "@lightsage/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 "@lightsage/agent-tracker/next";
import { NextResponse } from "next/server";
function myMiddleware(request) {
// your existing logic
return NextResponse.next();
}
export default withAgentTracking({ apiKey: "ls_track_..." }, myMiddleware);Express
// Tracking middleware runs before your routes
app.use(withAgentTracking({ apiKey: "ls_track_..." }));
app.use(yourOtherMiddleware);How It Works
The middleware intercepts requests and:
- Filters asset requests — Sends page views (
Accept: text/htmlortext/markdown) and any request from a non-browser client (a User-Agent not starting withMozilla/), except static assets such as.js,.cssand images - Classifies visitors — Lightsage names the agent on its side from the User-Agent, headers, provider signatures and published IP ranges, so detection improves without upgrading the SDK
- Identifies browsers — Reads Lightsage 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 |
| method | HTTP method |
| signature, signature_input, signature_agent | Web Bot Auth headers, when present |
| signals | Request signals (below) |
Request signals
Used only to tell agents from people. The SDK never reads cookie, authorization or other header values beyond this list.
| Signal | Source |
| ------ | ------ |
| header_names | Names of the headers the client sent (no values), excluding headers added by your platform |
| sec_ch_ua, sec_ch_ua_platform | Client hints, sent by Chromium browsers |
| sec_fetch_mode, sec_fetch_site, sec_fetch_dest | Fetch metadata, sent by current browsers |
| accept_language, accept_encoding, dnt, sec_gpc | Request headers |
| ja4 | TLS fingerprint from the platform: Vercel x-vercel-ja4-digest, Cloudflare Bot Management |
| asn | Network of the client: Vercel x-vercel-ip-as-number, Cloudflare request.cf.asn |
| scheme | https or http |
Lightsage stores signals only for requests it classifies as agents or bots.
Detection Logic
Detection runs on Lightsage's side. Each agent visit is labelled by how its identity was established:
| Label | Meaning | Examples |
| ----- | ------- | -------- |
| Verified | The provider vouched for the request | A Web Bot Auth signature (ChatGPT agent, Google-Agent), or an IP in the provider's published ranges (ChatGPT-User, Codex and Cursor cloud agents) |
| Declared | The agent named itself in its User-Agent | Claude Code, Codex CLI, Gemini CLI, Qwen Code, Aider, Crush |
| Inferred | Its headers match exactly one agent family | OpenCode's weighted Accept list, VS Code's fetch |
Clients that cannot be named are still counted: a command-line HTTP client reading docs, or a request whose User-Agent claims a browser but whose headers or TLS handshake no browser sends, is an unnamed coding agent on docs pages.
API Reference
Core Functions
import { trackVisit, init } from "@lightsage/agent-tracker";
// Initialize globally (optional - can pass config per-call)
init({ apiKey: "ls_track_..." });
// 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,
initLightsagePixel,
initVisitorIdentity,
trackConversion,
} from "@lightsage/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" },
{ label: "Complete sign up", eventStage: "completed" },
);
void initLightsagePixel({ apiKey: "ls_track_..." }); // enables data-lightsage-conversion clicks
clearUserIdentity();Platform Exports
Each platform export includes:
// Next.js
import {
withAgentTracking,
trackVisit,
init,
} from "@lightsage/agent-tracker/next";
// Vercel
import {
withAgentTracking,
trackRequest,
trackVisit,
init,
} from "@lightsage/agent-tracker/vercel";
// Cloudflare
import {
withAgentTracking,
onRequest,
trackRequest,
trackVisit,
init,
} from "@lightsage/agent-tracker/cloudflare";
// Netlify
import {
withAgentTracking,
proxyWithAgentTracking,
trackRequest,
trackProxyRequest,
trackVisit,
init,
} from "@lightsage/agent-tracker/netlify";
// Express
import {
withAgentTracking,
trackVisit,
init,
} from "@lightsage/agent-tracker/express";
// Browser
import {
clearUserIdentity,
getVisitorIdentity,
identifyUser,
initLightsagePixel,
initVisitorIdentity,
trackConversion,
} from "@lightsage/agent-tracker/browser";Types
import type {
LightsageConfig,
TrackOptions,
TrackResult,
} from "@lightsage/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
