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

@iorg1/trackking-next

v0.5.0

Published

Cookieless, server-side page-view tracking for Next.js. Drop-in middleware that beacons visits to a Trackking collector — no cookies, no client-side script.

Readme

@iorg1/trackking-next

Cookieless, server-side page-view tracking for Next.js.

@iorg1/trackking-next is a tiny Next.js middleware that beacons each page navigation to a Trackking collector over HTTPS. There is no cookie, no client-side script, and no consent banner machinery — the middleware runs on the server (Edge runtime), reads the request headers, and fires a fire-and-forget POST. Because it observes the document request, it also sees bots and AI crawlers (Googlebot, GPTBot, ClaudeBot, PerplexityBot) that JavaScript-based analytics never record.

Install

npm install @iorg1/trackking-next

Configure

Set two environment variables in your Next.js app:

TRACKKING_ENDPOINT=https://collector.example.com/collect
TRACKKING_API_KEY=your_site_api_key

Optional:

# Reverse proxies in front of your app — trust the right client IP (see Options).
TRACKKING_TRUSTED_PROXY_HOPS=1
# Enable the first-party client-event route (see "First-party client events").
TRACKKING_EVENT_ROUTE=/data/:event
# Collector event endpoint; defaults to TRACKKING_ENDPOINT with /collect → /event.
TRACKKING_EVENT_ENDPOINT=https://collector.example.com/event

Use

The whole middleware (simplest)

// middleware.ts
import { createTrackingMiddleware } from "@iorg1/trackking-next";

export const middleware = createTrackingMiddleware();

export const config = {
  // Track real page navigations; skip Next internals, the API, and static files.
  matcher: ["/", "/((?!_next/|api/|.*\\..*).*)"],
};

Inside an existing middleware

Already have a middleware.ts (locale negotiation, auth, …)? Call track() and return your own response:

import { NextResponse, type NextRequest, type NextFetchEvent } from "next/server";
import { track } from "@iorg1/trackking-next";

export function middleware(req: NextRequest, event: NextFetchEvent) {
  track(req, event); // fire-and-forget, never throws
  // ...your existing logic...
  return NextResponse.next();
}

First-party client events

Page-view tracking is fully server-side, but sometimes you want to record a client-triggered event — a signup click, a preview generated, a "real browser rendered this" ping. Instead of beaconing the collector's origin from the browser (blocked by many ad/tracker blockers, and needing CORS), expose a same-origin route and let the middleware forward the event server-side. Your API key never reaches the browser.

Set eventRoute (or TRACKKING_EVENT_ROUTE) and add the route to your matcher. When a request matches, handleEvent records a named event — the captured segment is the event name — and answers the client 200 {}; when it doesn't, it returns null so you fall through to normal tracking:

import { NextResponse, type NextRequest, type NextFetchEvent } from "next/server";
import { track, handleEvent } from "@iorg1/trackking-next";

export async function middleware(req: NextRequest, event: NextFetchEvent) {
  const res = await handleEvent(req, event, { eventRoute: "/data/:event" });
  if (res) return res; // it was a /data/<event> beacon → 200 {}

  track(req, event);
  return NextResponse.next();
}

export const config = {
  // Add the event route to the matcher so the middleware runs for it.
  matcher: ["/", "/data/:path*", "/((?!_next/|api/|.*\\..*).*)"],
};

createTrackingMiddleware() does this for you automatically when eventRoute / TRACKKING_EVENT_ROUTE is set — just remember to add the route to the matcher.

From the browser, POST to the route with an optional { path?, props? } body:

navigator.sendBeacon(
  "/data/signup",
  new Blob([JSON.stringify({ props: { plan: "pro" } })], { type: "application/json" }),
);

The collector records { name: "signup", path, props } (props are re-sanitized server-side). Route templates support :name (one path segment) and * (the rest of the path). For full control, pass resolveEvent: (req) => string | null instead of eventRoute — return the event name, or null to pass through.

Conversions

A conversion is an event that gets attributed to the campaign the visitor landed with. Page views already carry the landing query string, and the collector turns it into a campaign label (utm_campaign, Google's auto-appended gad_campaignid, a bare gclid, …). A conversion adds the request's client IP + User-Agent; the collector hashes them into the same cookieless daily visitor id the page view got, discards the IP, and looks up that visitor's landing campaign for the day. Nothing is stored on the device, so a click and its conversion are linked only when they happen on the same day.

Three ways to record one:

// 1. From a route handler or server action, where the action actually completes.
//    Use @iorg1/trackking-generic and pass the request headers:
import { headers } from "next/headers";
import { trackConversion } from "@iorg1/trackking-generic";
await trackConversion("report-ordered", { headers: await headers(), path: "/order" });

// 2. From your middleware — e.g. when the thank-you page is requested.
//    (A page request repeats on reload, so prefer 1 when you can.)
import { track, trackConversion } from "@iorg1/trackking-next";
export function middleware(req: NextRequest, event: NextFetchEvent) {
  if (req.nextUrl.pathname === "/order/confirmed") trackConversion(req, event, "order");
  track(req, event);
  return NextResponse.next();
}

// 3. From the browser, through the first-party event route:
navigator.sendBeacon("/data/signup?conversion=1");
// …or with a body: { conversion: true, props: { plan: "pro" } }

If your code already knows the campaign (carried in a hidden form field, say), pass campaign: "…" and the collector skips the lookup. Plain event beacons stay identity-free unless you set identifyEvents: true, in which case they, too, are attributed to the visitor's campaign.

Options

createTrackingMiddleware({
  endpoint: process.env.TRACKKING_ENDPOINT, // default
  apiKey: process.env.TRACKKING_API_KEY,    // default
  shouldTrack: (req) => !req.nextUrl.pathname.startsWith("/admin"),
  getClientIp: (req) => req.headers.get("cf-connecting-ip"),
  trustedProxyHops: 1,                       // trust the right x-forwarded-for entry (see below)
  eventRoute: "/data/:event",                // first-party client-event route (see above)
  resolveEvent: (req) => …,                  // …or fully custom event extraction (overrides eventRoute)
  identifyEvents: false,                     // attach visitor identity to plain event beacons too (see Conversions)
  eventEndpoint: process.env.TRACKKING_EVENT_ENDPOINT,
  debug: process.env.NODE_ENV !== "production", // verbose logging (see below)
});

The middleware automatically skips non-GET requests and Next.js prefetches (the event route is exempt — it handles the beacon method you send).

trustedProxyHops

x-forwarded-for is appended left→right as a request passes through proxies, and the leftmost entry is client-controlled (spoofable). This option is the number of reverse proxies you run in front of the app, so the middleware trusts the value your proxy appended:

  • 0 (default) — use the leftmost entry. Correct only on a platform that overwrites XFF for you (e.g. Vercel, Cloudflare).
  • 1+ — use the entry that many hops from the right. Behind your own Caddy/nginx/Traefik, set this (usually 1) so a spoofed leftmost entry can't poison the visitor hash or geo/ASN lookup.

Falls back to TRACKKING_TRUSTED_PROXY_HOPS. Ignored when getClientIp is set.

Debugging

Tracking is intentionally silent — it never throws and, by default, never logs, so a misconfiguration (wrong endpoint, missing key, an unexpected 4xx) looks like "nothing happens." To see what it's doing, set TRACKKING_DEBUG:

TRACKKING_DEBUG=1   # any truthy value; "0" / "false" / "" disable it

(or pass debug: true in the config, which overrides the env var). For each request the middleware then logs to the console via console.debug, prefixed with [trackking]:

  • the resolved config — endpoint, a masked API key (first 6 chars + length, never the full secret), and which config keys you passed;
  • the decision — tracked, or skipped with the reason (not configured, non-GET, prefetch, or shouldTrack() returned false);
  • the event payload that gets posted; and
  • the collector's response — POST <endpoint> → <status> <statusText>, plus the response body on a non-2xx, or the error if the request threw.
[trackking] GET /pricing { endpoint: 'https://…/collect', apiKey: 'tk_abc…(35 chars)', configKeys: [] }
[trackking] tracking event: { path: '/pricing', host: 'example.com', referrer: null, userAgent: '…', ip: '203.0.113.10', headers: { … } }
[trackking] POST https://…/collect → 204 No Content (/pricing)

Leave it unset in production: it logs request metadata (path, IP, user-agent) to your server logs.

What gets sent

Page views — a single JSON object: { path, query, host, referrer, userAgent, ip, headers }. The collector hashes ip into a daily-rotating visitor id and discards it — the raw IP is never stored. The referrer is truncated to its host. query is the landing query string; the collector derives the campaign label from it and stores it with secret-looking parameter values (token, code, email, …) and, by default, click-id values (gclid, fbclid, …) redacted. headers is a small bag of low-entropy request headers (Accept-Language, Accept, the Sec-Fetch-* fetch-metadata set, and Sec-CH-UA* client hints) that help the collector tell real browsers from bots — it is not a device fingerprint, and no cookies or client identifiers are involved.

Events (if you use the event route) — { name, path, props }, carrying no visitor identity. props is an optional small, flat bag of non-PII metadata, re-sanitized by the collector.

Conversions — the same, plus conversion: true and the request's ip + userAgent, which the collector hashes into the daily visitor id (IP discarded) to attribute the conversion to a campaign. Plain events carry identity only with identifyEvents: true.

No personal identifiers are persisted.

License

MIT