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

@addilytics/nextjs

v0.0.1

Published

Next.js server events and bundled App Router navigation tracking.

Readme

@addilytics/nextjs

Server-side analytics for Next.js App Router Route Handlers. The wrapper waits for the handler's real Response, records eligible HTML responses, and returns that same response without reading its body.

It does not track page.tsx renders through Proxy or Middleware. Those APIs run before rendering and cannot see the final status or content type, so treating them as pageviews would produce bad data.

Install

pnpm add @addilytics/nextjs

Wrap a Route Handler

// app/report/[id]/route.ts
import { withAddilytics } from '@addilytics/nextjs';
import type { NextRequest } from 'next/server';

async function report(request: NextRequest, context: { params: Promise<{ id: string }> }) {
	const { id } = await context.params;
	return new Response(`<h1>Report ${id}</h1>`, {
		headers: { 'content-type': 'text/html; charset=utf-8' }
	});
}

export const GET = withAddilytics(report, {
	endpoint: process.env.ADDILYTICS_ENDPOINT!,
	siteKey: process.env.ADDILYTICS_KEY!
});

The wrapper keeps the request type, context arguments, and response subtype. It also exposes its core client and a track method.

Client-side navigation

Server mode is the default and records only requests that reach a wrapped Route Handler. Next does not expose the final rendered response for normal App Router pages, so browser navigation uses client mode. The browser owns both the initial page and later committed navigations.

Create one server-only instance and expose its relay at the matching path:

// lib/analytics.server.ts
import { createNextAddilytics } from '@addilytics/nextjs';

export const analytics = createNextAddilytics({
	endpoint: process.env.ADDILYTICS_ENDPOINT!,
	mode: 'client',
	siteKey: process.env.ADDILYTICS_KEY!
});
// app/%5F_addilytics/route.ts
import { analytics } from '../../lib/analytics.server';

export const POST = analytics.relay;

Mount the browser component once in the root layout. Suspense is required because the component uses Next's usePathname and useSearchParams committed-navigation hooks.

import { AddilyticsBrowser } from '@addilytics/nextjs/browser';
import { Suspense } from 'react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body>
				{children}
				<Suspense fallback={null}>
					<AddilyticsBrowser mode="client" />
				</Suspense>
			</body>
		</html>
	);
}

Next treats a route folder beginning with _ as private. Naming the folder %5F_addilytics exposes the default /__addilytics URL. Client mode disables automatic pageviews in wrapped Route Handlers, which prevents double-counting. The server and browser modes must both be client. If you set relayPath on the server, pass the same path as endpoint to AddilyticsBrowser and place the Route Handler there.

Your app bundles the browser helper; Addilytics does not load a hosted or CDN script. The browser sends only a generated event ID, timestamp, path, campaign query, and referrer to the same-origin relay. The site key stays in the server module. The relay rejects cross-origin requests, unexpected methods and content types, malformed fields, oversized bodies, and stale timestamps.

Next does not expose an HTTP status for committed client navigations. Those pageviews use status 0 to mean unknown instead of being mislabeled as successful responses. trackStatuses applies only to wrapped Route Handler responses.

Back-forward cache restores are recorded. Hash-only changes are ignored unless you set trackHashChanges: true. The lower-level useAddilyticsNavigation hook is available when a component needs access to the browser tracker.

Custom events

Create one adapter when a route needs pageviews and custom events:

// app/checkout/route.ts
import { createNextAddilytics } from '@addilytics/nextjs';

const analytics = createNextAddilytics({
	endpoint: process.env.ADDILYTICS_ENDPOINT!,
	siteKey: process.env.ADDILYTICS_KEY!
});

export const POST = analytics.withRouteHandler(async (request) => {
	await analytics.track(request, 'checkout_completed', {
		props: { plan: 'pro' },
		userId: 'your-stable-user-id'
	});

	return Response.json({ ok: true });
});

Addilytics hashes userId with the site key before delivery. You can also set identify in the adapter options to resolve a user for every event.

Background delivery

Without a scheduler, the wrapper waits for analytics delivery before returning. Pass waitUntil when the deployment runtime has it:

import { waitUntil } from '@vercel/functions';
import { createNextAddilytics } from '@addilytics/nextjs';

const analytics = createNextAddilytics({
	endpoint: process.env.ADDILYTICS_ENDPOINT!,
	siteKey: process.env.ADDILYTICS_KEY!,
	waitUntil
});

Next 15.1 and newer also expose after. Adapt its callback API like this:

import { after } from 'next/server';
import { createNextAddilytics } from '@addilytics/nextjs';

const analytics = createNextAddilytics({
	endpoint: process.env.ADDILYTICS_ENDPOINT!,
	siteKey: process.env.ADDILYTICS_KEY!,
	waitUntil: (promise) => after(() => promise)
});

Check the hosting provider's support before choosing either API. If waitUntil throws synchronously, the adapter falls back to awaiting delivery. Analytics failures never replace a valid application response.

What counts

The default policy records GET responses with an HTML content type and either a 2xx or 404 status. It rejects bot traffic, redirects, server errors, JSON responses, /_next assets, prefetches, and React Server Component requests. ignorePaths, trackBots, and trackStatuses extend or replace parts of that policy.

A thrown Route Handler error has no final response, so the adapter rethrows it and records nothing. If you intentionally render an error response and want it counted, opt in with trackStatuses.

Runtime and caching limits

The package currently supports and tests Next.js 16. It uses only Request, Response, fetch, and Web Crypto through the core client, so the wrapper runs in both Node.js and Edge Route Handlers.

Keep the collector endpoint and site key in server-only modules. Client Components should import only @addilytics/nextjs/browser.

Route Handler execution must happen for every request you want to count. Cached or statically generated handlers can serve responses without running the wrapper. On Next versions or deployments that cache a GET Route Handler, opt out in that route:

export const dynamic = 'force-dynamic';

Server mode does not claim automatic pageview coverage for App Router pages, the Pages Router, Proxy, or Middleware. Those layers cannot see a final rendered response. Client mode covers committed App Router navigations through the browser component and same-origin relay.