Node SDK
Server-side analytics for Node.js with middleware for Express, Hono and Next.js that records pageviews without slowing down responses.
@metrikstack/sdk-node turns the HTML pages your server sends into
pageview events without blocking the response. It has no runtime
dependencies (only node:crypto and the global fetch) and ships as ESM
with TypeScript types, for Node 20+.
Install
Not published yet. The package is not on npm yet, so the command below does not work today. This page describes how it will work. Until then the browser script and the edge and log sources cover the same traffic.
bun add @metrikstack/sdk-nodeQuick start
import { createMetrikStack } from '@metrikstack/sdk-node';
export const analytics = createMetrikStack({
token: process.env.METRIKSTACK_TOKEN!,
endpoint: 'https://api.metrikstack.com', // always pass the endpoint
trustProxy: true, // behind a CDN / reverse proxy
onError: (err) => console.warn('analytics:', err),
});| Option | Default | Notes |
|---|---|---|
token | none | Required. Sent as Authorization: Bearer <token>. |
endpoint | https://api.example.com | The default is a placeholder, so always pass https://api.metrikstack.com. |
flushIntervalMs | 2000 | Background flush interval. The timer is unref()ed. |
maxBatch | 500 | Flush early once this many events are buffered. |
trustProxy | false | Read the client IP from x-forwarded-for / cf-connecting-ip (and x-forwarded-proto / x-forwarded-host for the URL). |
shouldTrack | none | (req) => boolean, applied after the built-in filtering. |
fetch | globalThis.fetch | Override for tests or a proxy agent. |
onError | no-op | Called when a batch is dropped or a middleware throws. |
retryDelayMs | 1000 | Delay before the single retry. |
What gets tracked
Only requests that are GET or HEAD, whose Accept header contains
text/html, and whose path isn't an asset (.js, .css,
.png, .jpg, .svg, .ico, .woff2, .map, .json, .xml, .txt, …
or the /_next/, /static/, /assets/ prefixes).
Batches are sent to POST {endpoint}/v1/batch as { "events": [...] },
at most 1000 events per request. A failed batch is retried once after
retryDelayMs, then dropped and reported to onError, so the buffer can't
grow without limit or slow down your app.
Express
import express from 'express';
import { analytics } from './analytics.ts';
const app = express();
app.use(analytics.express());The middleware sets the response headers immediately and hooks
res.on('finish') to enqueue the event with the final status code.
Hono
import { Hono } from 'hono';
import { analytics } from './analytics.ts';
const app = new Hono();
app.use(analytics.hono());It awaits next(), then reads the status from c.res. On runtimes that
have c.executionCtx.waitUntil() (Cloudflare Workers, Deno Deploy) the
flush is handed to it; elsewhere the event is added to the queue.
Next.js (middleware.ts)
import { NextResponse } from 'next/server';
import { analytics } from './analytics.ts';
export function middleware(req: Request) {
const res = NextResponse.next();
analytics.next(req, res);
return res;
}
export const config = {
matcher: ['/((?!_next/|api/|.*\\..*).*)'],
};analytics.next(req, res, { status }) sets both headers on the response
and queues the event. The status defaults to 200, because middleware runs
before the route. The SDK doesn't import next/server; it uses the standard
Request / Headers types, so it also works on other runtimes built on web
standards.
Errors
analytics.captureException(), errorHandler() (Express),
honoOnError() (Hono) and installGlobalHandlers() report exceptions next
to pageviews, linked by request_id when you pass the request. See
Errors for the full API, the scrubbing rules and what is
collected.
Shutdown
process.on('SIGTERM', () => void analytics.shutdown());shutdown() flushes what's buffered and stops the timer, which is
unref()ed so it never keeps a process alive on its own.
Request ids: one pageview from two sources
A page load seen by both the browser script and this SDK is counted
once. The SDK generates a UUIDv7 per HTML response and sets it as
X-MetrikStack-Request-Id and Server-Timing: metrikstack;desc=<id>. The
browser script reads it back and sends the same id with its own event. See
How merging works for the merge rules.
The id is a UUIDv7 because it is time-ordered, which helps match pageviews
from several sources. analytics.requestId() gives you the generator if you
need to set an id yourself.