Next.js server errors
Server-side errors — Server Components, Route Handlers, Server Actions — reported from instrumentation.ts, without an adapter package.
onRequestError in instrumentation.ts. This page wires that hook to Userz with the server-side API client. There is no adapter package for this, and none is needed.Install
pnpm add @userz-ai/api@userz-ai/api is the typed server-side client for the Userz REST API. It authenticates with an sk_ key from the dashboard, which is why this runs on the server and nowhere else.
The recipe
Create instrumentation.ts at the project root (or under src/). Export onRequestError; guard on the Node runtime so the Edge runtime, which has no key, does nothing.
// instrumentation.ts
import type { Instrumentation } from 'next';
export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
if (process.env.NEXT_RUNTIME !== 'nodejs') return;
const { createUserzApi } = await import('@userz-ai/api');
const userz = createUserzApi({ apiKey: process.env.USERZ_API_KEY! }); // sk_..., server-side only
const error = err instanceof Error ? err : new Error(String(err));
// Next may replace the thrown error during Server Components rendering; `digest` survives and
// identifies the underlying error, so it is the idempotency key: one Signal per distinct error.
const digest = 'digest' in error && typeof error.digest === 'string' ? error.digest : undefined;
await userz.signals.create({
appId: process.env.USERZ_APP_ID!,
type: 'bug',
severity: context.routeType === 'render' ? 'high' : 'medium',
text: `${error.name}: ${error.message}\n\n${context.routeType} ${request.method} ${request.path}\nroute ${context.routePath} (${context.routerKind})`,
sourceUrl: absoluteUrl(request.path),
sourceExternalId: digest ? `next-server-error:${digest}` : undefined,
sourceMeta: {
digest,
method: request.method,
path: request.path,
routerKind: context.routerKind,
routePath: context.routePath,
routeType: context.routeType,
renderSource: context.renderSource,
revalidateReason: context.revalidateReason,
renderType: context.renderType,
stack: error.stack,
},
});
};
function absoluteUrl(path: string): string | undefined {
const origin = process.env.NEXT_PUBLIC_SITE_URL;
return origin ? new URL(path, origin).toString() : undefined;
}Two environment variables, both server-side: USERZ_API_KEY (an sk_ key) and USERZ_APP_ID (the App the Signals belong to, from GET /v1/apps). Neither carries a NEXT_PUBLIC_ prefix, so neither reaches a browser bundle.
What each field does
| Name | Type | Description |
|---|---|---|
| sourceExternalId | next-server-error:<digest> | The idempotency key. A second report with the same key resolves to the existing Signal instead of creating another — a route that throws on every request produces one Signal, not one per request. Errors without a digest opt out of dedup. |
| text | string | What the reviewer reads first: the error, then the request and route. Keep it to the facts; the structured copy is in sourceMeta. |
| sourceMeta | Record<string, unknown> | onRequestError's whole context — router, route file, phase (render / route / action / proxy), render source, revalidation reason — plus the stack. Attached to the Signal for the agent that works it. |
| severity | 'low' | 'medium' | 'high' | Your call. The recipe marks render failures high (a user saw an error page) and everything else medium. |
| sourceUrl | string (absolute) | The page the error happened on, if you can name your origin. Optional; the request path is in sourceMeta regardless. |
Behaviour to know
Request headers are not forwarded
request.headers carries cookies and authorization. The recipe does not copy them into the Signal, and you should not: a Signal is read by people and by an agent, and neither needs a session cookie.
Rate limits and failures
POST /v1/signals is rate-limited per key (a burst of about sixty, one per second sustained). A throttled or failed report throws from signals.create; Next logs a rejected onRequestError promise and continues serving. If you would rather never throw from the hook, wrap the call in a try/catch and log locally.
Development
The hook fires in next dev too. Point USERZ_APP_ID at a development App, or return early when process.env.NODE_ENV !== 'production'.
What about @userz-ai/node?
@userz-ai/node mints private-mode user tokens and verifies outbound webhook signatures. It has no error intake, and this recipe does not use it. Browser-side errors are the browser SDK's, captured automatically once telemetry is enabled.