Next.js Full-Stack Capture
hakka-node’s /next subpath instruments the Next.js server runtime — Server Components, Route Handlers, and Server Actions — and streams captures to the same bridge hub the browser overlay connects to. Server and client requests appear in one UI, tagged by runtime.
Try it in two minutes
Section titled “Try it in two minutes”The repo ships a working example with eleven traffic generators and a checklist, so you can see server and client capture side by side before wiring anything into your own app.
-
Clone the repo and run the example:
Terminal git clone https://github.com/ansumanshah/hakka.gitcd hakka/examples/next-fullstacknpm installnpm run dev -
Open
http://localhost:3000. The page’s own Server Component fetchesapi.github.comduring render, so a request is already waiting for you before you click anything. -
Tap the round button in the bottom-right corner. That opens the inspector.
Generate traffic
Section titled “Generate traffic”Eleven buttons fire real requests through the dev server. Core flow is the everyday shape; Push it further hits the edges the everyday shape doesn’t reach.
Core flow
| Generator | Call | What it shows |
|---|---|---|
| Fetch products | GET /api/products |
Client calls a route handler, which makes its own upstream call: one click, two hops, tagged client then server. |
| Fetch cached route | GET /api/cached |
A hand-stamped x-vercel-cache header cycling HIT, HIT, STALE, so the cache-status badge has something to show without real Vercel infra. |
| Run server action | pingServerAction() |
A Server Action with no route handler in between, tagged server-action. It makes its own outbound fetch, so its trace gets a child hop too. |
Push it further
| Generator | Call | What it shows |
|---|---|---|
| Fail request | GET /api/demo/fail |
Always 500, the chili severity stripe. |
| Not found | GET /api/demo/missing |
Always 404, the turmeric stripe. |
| Slow request | GET /api/demo/slow |
About 2.5 seconds. Open it while it runs and read the timing waterfall phase by phase. |
| POST with a body | POST /api/demo/echo |
Sends a small JSON payload and gets it echoed back with a server timestamp. |
| Delete | DELETE /api/demo/echo |
Same URL as the POST above, a different method chip on the same row. |
| Large response | GET /api/demo/large |
About 100KB of JSON, 1,250 rows. Worth opening as a tree instead of a wall of text. |
| Burst | 8 requests at once, Promise.allSettled |
A mix of GET, POST, DELETE, a 404, and a 500, fired together. Good fodder for the Stats panel. |
| Emit logs | console.log / warn / error |
Writes one line at each level so the Logs tab has real content to filter. |
Work through the checklist
Section titled “Work through the checklist”The page ends with an eight-step checklist. Each step uses something you just generated, and points at the real button or tab:
- Open the inspector. Tap the round button in the bottom-right corner.
- Open Filters, then Runtime. Pick Server to see only this page’s outbound calls, then Client for the browser’s.
- Run Slow request, open it, and read the timing waterfall: DNS through download, phase by phase.
- Open any request and tap Mock this, a button in the Detail action bar. Run the same generator again and the row shows the mock, not a real call.
- Open Rules > Throttle > Slow 3G. Run Fetch products again and watch the duration climb.
- Open Rules > Breakpoints, and add one for
/api/demo. Run any generator above and it pauses before the network. - Run Emit logs, then open the Logs tab: an info line, a warning, and an error, side by side.
- Open a request and tap Copy as agent context. Paste the bundle into an AI coding agent.
Install
Section titled “Install”npm install -D hakka-nodenpm install hakka-browserpnpm add -D hakka-nodepnpm add hakka-browseryarn add --dev hakka-nodeyarn add hakka-browserbun add --dev hakka-nodebun add hakka-browserNext 15.3+
Section titled “Next 15.3+”-
Create
instrumentation.tsin your project root:instrumentation.ts export { register } from 'hakka-node/next' -
Create
instrumentation-client.tsin your project root:instrumentation-client.ts import 'hakka-node/next/client' -
Verify it works — run
next dev, open your app, make any request (page load counts). The Hakka overlay (bottom-right trigger) shows both server and client requests, each taggedserverorclient.

One user action, the whole chain: the browser’s /api/products call, the route
handler’s downstream fetch, and the Server Component’s own fetch — in one list,
each tagged with the runtime it ran on.
Older Next (no instrumentation-client.ts)
Section titled “Older Next (no instrumentation-client.ts)”If your Next.js version doesn’t support instrumentation-client.ts, drop a small client component into your root layout instead. The instrumentation.ts server file is identical to the above.
'use client'import { useEffect } from 'react'import { startHakkaClient } from 'hakka-node/next/client'
export function HakkaOverlay() { useEffect(() => startHakkaClient(), []) return null}Then render <HakkaOverlay /> inside your root layout.
What Hakka captures
Section titled “What Hakka captures”| Traffic | Captured | Body |
|---|---|---|
fetch — Server Components, Route Handlers, Server Actions |
Yes | Full request + response body |
Node http/https — axios, got, node-fetch, any SDK |
Yes | Request + headers only; response body is NOT captured |
Edge runtime (runtime: 'edge') |
fetch only |
Full body |
| Non-HTTP (Postgres/Prisma over TCP) | No | — |
Embedded bridge
Section titled “Embedded bridge”register starts a bridge hub inside the Next dev server process by default (embedBridge: true). The browser overlay connects to the same hub at ws://localhost:8989. If the port is already in use — another dev worker or a standalone hub — the embedded start is skipped and the client connects to the existing one.
See Bridge overview for how the hub works.
Dev-only
Section titled “Dev-only”register is a no-op in production (NODE_ENV === 'production') unless you override runtime explicitly. The client side (hakka-node/next/client) also self-skips in production and on the server — it runs only in a browser in dev.
Options
Section titled “Options”Pass options to register for custom behavior:
import { register as base } from 'hakka-node/next'
export const register = () => base({ runtime: 'server', // 'server' (default) | 'edge' captureHttp: true, // patch node http/https. No-op on edge runtime. captureFetch: true, // patch globalThis.fetch embedBridge: true, // run the hub in-process. false → use npx hakka-bridge separately. bridgeUrl: 'ws://localhost:8989', maxBodySize: 262144, // bytes; default 256 KB // Replaces the core default list entirely — it is not merged with it. // Spread the defaults explicitly to keep them alongside your own additions. redactHeaders: ['authorization', 'proxy-authorization', 'cookie', 'set-cookie', 'x-custom'], })| Option | Type | Default | Description |
|---|---|---|---|
runtime |
'server' | 'edge' |
'server' |
Tag applied to every captured record |
captureHttp |
boolean |
true |
Patch Node http/https. No-op on edge. |
captureFetch |
boolean |
true |
Patch globalThis.fetch |
embedBridge |
boolean |
true |
Host the bridge hub in-process |
bridgeUrl |
string |
ws://localhost:8989 |
Hub WebSocket URL |
maxBodySize |
number |
262144 |
Max captured body in bytes |
redactHeaders |
string[] |
core defaults | Sensitive header names to redact — replaces the core default list rather than merging with it |
Edge runtime
Section titled “Edge runtime”For routes running on the edge runtime, pass runtime: 'edge'. captureHttp is automatically disabled (no Node http module on edge):
import { register as base } from 'hakka-node/next'
export const register = async () => { if (process.env.NEXT_RUNTIME === 'edge') { const { startServerCapture } = await import('hakka-node/next/server') startServerCapture({ runtime: 'edge', captureHttp: false, embedBridge: false }) }}The default register export handles this automatically — NEXT_RUNTIME === 'edge' triggers fetch-only capture with no embedded hub.
Runtime filter
Section titled “Runtime filter”The overlay runtime filter lets you isolate server, client, or edge traffic. Requests captured by hakka-node/next carry the tag set in runtime (default 'server'); browser traffic carries 'client'.
Request Insights (span waterfall)
Section titled “Request Insights (span waterfall)”Passing traceSpans: true to register/startServerCapture bridges Next’s own OpenTelemetry request-tree spans (BaseServer.handleRequest, AppRouteRouteHandlers.runHandler, Render.getServerSideProps, …) into the inspector as FrameworkSpan records, so a request’s server-side waterfall shows up alongside its network captures — without hakka-node shipping any OTel SDK code itself. Opt-in; zero cost when off. hakka-node/next/server’s startServerCapture defaults it to true in development.
import { registerOTel } from '@vercel/otel'import { hakkaSpanProcessor } from 'hakka-node'import { register as base } from 'hakka-node/next'
export async function register() { registerOTel({ spanProcessors: [hakkaSpanProcessor()] }) await base({ traceSpans: true })}Why two attach paths exist. @opentelemetry/sdk-trace-base 1.x lets a processor attach to an already-registered provider via provider.addSpanProcessor(); 2.x removed that method — processors are constructor-only there. hakkaSpanProcessor() is the 2.x-safe path: you construct it and hand it to your SDK’s own processor list (registerOTel({ spanProcessors: [...] }) for @vercel/otel, or the equivalent for any other SDK). A duck-typed attach() fallback also runs automatically from enableTraceSpans() for 1.x setups that never call hakkaSpanProcessor() explicitly — it reads trace.getTracerProvider(), unwraps one ProxyTracerProvider.getDelegate() layer (the standard registration path always wraps the real provider), and calls addSpanProcessor if present. If a hakkaSpanProcessor() instance already exists in the process, the fallback no-ops rather than installing a second processor that would double-emit every span.
@opentelemetry/api is an optional peer. Nothing in spanProcessor.ts imports it statically — only via a dynamic import() inside the 1.x fallback — so a consumer who never calls enableTraceSpans() needs nothing installed and pays zero cost. @opentelemetry/sdk-trace-base (which declares the real SpanProcessor/ReadableSpan types) is never imported at all, even as a type; the module declares a minimal structural subset instead and duck-types against whatever the runtime hands back.
Request kind classification. Root spans (no parent) get a requestKind of 'rsc', 'route-handler', 'document', or 'server-action', derived from (in priority order) an inbound header hint (next-action / rsc: 1, read at the same point trace propagation reads the incoming trace header) and Next’s documented next.rsc span attribute. The AppRender.fetch span type is dropped at the source — it’s the exact same outbound fetch hakka-core’s own fetch interceptor already captures with full headers/body, so emitting both would draw one operation twice.