Next.js
Start @unhandled/nextjs from instrumentation.ts to send traces, logs, metrics and server errors from a Next.js 15+ app to Unhandled.
@unhandled/nextjs sends traces, logs, metrics and server errors from a Next.js app to Unhandled. It starts @unhandled/node from instrumentation.ts and reports errors through Next's onRequestError hook. Next's own request spans become the SERVER spans that Unhandled groups by route. The resource carries unhandled.framework.name=nextjs.
Requirements
- Next.js 15 or later, with React 19.
- The Node.js runtime for the routes you want traced. On the edge runtime the package does nothing.
- Node.js 24 or later.
- A service DSN. See Services.
Install
npm install @unhandled/nextjs@unhandled/node is installed with it.
Set up the app
Set UNHANDLED_DSN in the server environment, then make two changes.
Add instrumentation.ts at the project root, or in src/ when your app uses one:
export { register, onRequestError } from '@unhandled/nextjs'register starts the SDK with options from the environment only. To pass options, call registerUnhandled from your own register:
import { registerUnhandled } from '@unhandled/nextjs'
export const register = () => {
registerUnhandled({ serviceName: 'storefront' })
}
export { onRequestError } from '@unhandled/nextjs'Wrap your config in next.config.ts:
import { withUnhandledConfig } from '@unhandled/nextjs'
export default withUnhandledConfig({
reactStrictMode: true,
})There is no --import register module for Next.js. Next calls register() from instrumentation.ts before it serves requests.
Options
registerUnhandled(options?) takes every Node SDK option, such as dsn, environment, serviceName, samplingRatio, http, instrumentations and integrations. It returns the running client or null.
It starts the SDK only when NEXT_RUNTIME is nodejs. On the edge runtime, the package resolves to a build where register, registerUnhandled, onRequestError, setUser and withServerAction do nothing. Edge routes and the edge compile of instrumentation.ts load no Node.js code and send nothing.
Routes
Next opens a SERVER span for every request and sets http.route to the route pattern, such as /api/orders/[id] or /checkout. The span is named after it, for example GET /api/orders/[id], and also carries next.route. Unhandled uses that span as the request, so routes need no extra code. Requests groups by that route.
Users
import { setUser } from '@unhandled/nextjs'
export const GET = async (request: Request) => {
const user = await currentUser(request)
setUser({ id: user.id, email: user.email })
return Response.json(user)
}setUser({ id, email?, name? }) writes user.id, user.email and user.name on the active span and on the request's SERVER span.
Server actions
Next names a server action request after the page that posts it, such as POST /checkout, so two actions on one page look the same. Wrap an action to give it a span of its own:
'use server'
import { withServerAction } from '@unhandled/nextjs'
export const submitOrder = withServerAction('submitOrder', async (form: FormData) => {
await placeOrder(form)
})The action runs inside a server action submitOrder span with the attribute nextjs.server_action. If it throws, the error is reported once with mechanism nextjs.server_action, then rethrown, so Next still renders its error. A redirect() or notFound() from the action passes through and does not mark the span as failed.
Route handlers need no wrapper. Next already names their spans by route, and onRequestError already reports their errors.
Errors
onRequestError reports errors thrown by route handlers, Server Components, server actions and the proxy, with mechanism nextjs.on_request_error and handled: false.
| Error | Reported |
|---|---|
| Errors thrown while handling or rendering a request | Yes |
notFound(), redirect(), permanentRedirect(), forbidden() and unauthorized() | No. They are control flow. |
| Dynamic rendering bailouts | No. They are control flow. |
Matched by ignoreExceptions, ignoreStatuses or ignoreCodes | No |
| Errors on the edge runtime | No. The package does nothing there. |
Each report carries these tags:
| Tag | Value |
|---|---|
nextjs.route_path | The route, for example /api/boom. |
nextjs.route_type | route, render, action or proxy. |
nextjs.router_kind | App Router or Pages Router. |
nextjs.render_source | Set while rendering, for example react-server-components. |
nextjs.revalidate_reason | on-demand or stale during revalidation. |
nextjs.digest | The digest that Next shows in place of the message in production. |
Verify the setup
Add a route handler that throws, in
app/api/boom/route.ts:export const GET = () => { throw new Error('Refund provider is unavailable') }Build and start the app with
next buildandnext start, withUNHANDLED_DSNset, and request/api/boomonce.In Unhandled, open Traces. The request shows as
GET /api/boom.Open Exceptions. The error shows with mechanism
nextjs.on_request_errorand thenextjs.route_pathtag.
Exports
| Export | Effect |
|---|---|
registerUnhandled(options?) | Starts the SDK on the Node.js runtime. Takes NextUnhandledOptions, the Node SDK options. |
register() | registerUnhandled() with options from the environment, ready to re-export from instrumentation.ts. |
onRequestError | Next's error hook. Re-export it from instrumentation.ts. |
withUnhandledConfig(config?) | Adds the SDK packages to serverExternalPackages. |
withServerAction(name, action) | Wraps a server action in its own span. |
setUser(user) | Sets the user on the request span. |
@unhandled/nextjs does not re-export the rest of the Node SDK. Import captureException, captureMessage, track, record and the decorators from @unhandled/node.