Node SDK
Express, Hono and Next.js middleware for server-side pageviews.
@metrikstack/sdk-node turns HTML page responses from your own server into
pageview events, without ever blocking the response. It has zero runtime
dependencies (node:crypto and global fetch only) and ships ESM with
TypeScript types, for Node 20+.
Install
bun add @metrikstack/sdk-nodeQuick start
import { createMetrikStack } from '@metrikstack/sdk-node';
export const analytics = createMetrikStack({
token: process.env.METRIKSTACK_TOKEN!,
endpoint: 'https://api.example.com', // your ingest host, pass it explicitly
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 | Placeholder default; always pass your own ingest base URL. |
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 doesn't look like an asset (.js, .css,
.png, .jpg, .svg, .ico, .woff2, .map, .json, .xml, .txt, …
or the /_next/, /static/, /assets/ prefixes).
Batches are POST {endpoint}/v1/batch with { "events": [...] }, at most
1000 events per request. A failed batch is retried once after retryDelayMs
and then dropped and reported to onError: analytics must never grow
unbounded in memory or delay 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 await next()s, then reads c.res for the status. On runtimes that
expose one (Cloudflare Workers, Deno Deploy) the flush is handed to
c.executionCtx.waitUntil(); elsewhere the event is simply enqueued.
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
enqueues the event (status defaults to 200, since middleware runs before
the route does). The SDK never imports next/server; it works with the
standard Request / Headers types, so it also fits any other Web-standard
runtime.
Errors
analytics.captureException(), errorHandler() (Express),
honoOnError() (Hono) and installGlobalHandlers() report exceptions
alongside pageviews, joined by request_id when a request is passed. See
Errors for the full API, scrubbing rules, and what is and
isn't 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 must count
once. The SDK generates a UUIDv7 per HTML response and exposes 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.
UUIDv7 is used rather than a random id because it's time-ordered, so
pageviews from several sources can be matched. analytics.requestId()
exposes the generator if you need to stamp an id yourself.