npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-tracker

Quick 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_proxy

Netlify 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=framer

SAPIENT_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 env

Wrapping 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:

  1. Filters asset requests — Sends page views (Accept: text/html or text/markdown) and any request from a non-browser client (a User-Agent not starting with Mozilla/), except static assets such as .js, .css and images
  2. 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
  3. Identifies browsers — Reads Lightsage visitor/session cookies produced by the browser helper
  4. Preserves attribution — Captures current, session, and first-touch attribution while ignoring configured same-site referrers
  5. Sends async — Tracking is non-blocking via waitUntil() or fire-and-forget
  6. Auto-creates sites — Sites are automatically registered when first event is received
  7. 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