punchline!

Install

Install on Next.js

Shows how much of your site's traffic is AI agents, and which ones. This package runs in your Next.js proxy (or middleware), and for each page request it sends a small event to your Punchline Analytics collector, after the response has gone out.

  • No cookies, no browser script, nothing added to your pages.
  • Doesn't slow pages down. The event is sent after the response; our code adds about 1 ms.
  • Never breaks your site. If Punchline is misconfigured or unreachable, requests continue normally.

Install

npm install @punchline-analytics/next

Set one environment variable. We send you the value when your site is set up:

PUNCHLINE_DSN=https://<site-id>:<secret>@<your-collector>/api/collect

Next.js 16: create proxy.ts at the project root, or in src/ if you use one:

import { punchline } from "@punchline-analytics/next";

export const proxy = punchline();

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

Next.js 15, or to run on the edge runtime: use middleware.ts with export const middleware = punchline(); and the same config.

Next.js reads matcher at build time, so it has to be written out in your file as shown.

Already have a proxy or middleware?

Call the tracker inside it (your proxy can be async). To serve AI agents markdown as well, answer with serveMarkdown first:

import { COUNTED_TIMING, createTracker } from "@punchline-analytics/next";
import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";

const tracker = createTracker();

export async function proxy(request: NextRequest, event: NextFetchEvent) {
  if (await tracker.isInternal(request)) return NextResponse.next();
  const markdown = await tracker.serveMarkdown(request, event);
  if (markdown) return markdown;
  const { overheadMs } = tracker.track(request, event);
  // …your existing logic…
  const response = NextResponse.next();
  // With the browser snippet installed too, this stops it counting the page load again.
  if (overheadMs !== null) response.headers.set("Server-Timing", COUNTED_TIMING);
  return response;
}

What gets sent

For each page view (GET/HEAD for pages, plus .txt, .md and .xml files such as robots.txt and llms.txt), the package sends one event, server to server:

  • The host, the path and utm_* values. Every other query parameter, and the #fragment, are removed before sending.
  • The referrer's host only.
  • The Accept header, cut to 200 characters.
  • Whether the browser sent Sec-GPC: 1.
  • The country code your host provides (x-vercel-ip-country on Vercel).
  • Web Bot Auth signature headers, when an agent sends them.
  • The visitor's IP address and user agent. The collector uses these in memory to classify the request and to compute a daily visitor hash, then discards them; they're never stored or logged.

Not tracked: images, scripts, styles, other assets, or Next.js link prefetches.

Options

punchline({
  dsn: "...",            // default: process.env.PUNCHLINE_DSN
  timeoutMs: 3000,       // give up sending after this long
  serverTiming: true,    // add the overhead to Server-Timing: `punchline;dur=…` (default: PUNCHLINE_SERVER_TIMING=1)
  onError: (err) => {},  // configuration or network errors (default: ignored)
});

Troubleshooting

  • No events arriving. Check PUNCHLINE_DSN is set for the environment you deployed. Pass onError: console.error once to see why sends are failing.
  • Measuring the overhead. Set PUNCHLINE_SERVER_TIMING=1 and look at the Server-Timing response header.

Ask the docs

Answers come from these docs only, written by Claude (Anthropic). Your question is sent to answer it, and isn't stored.