diff --git a/docs/onboarding/error-tracking/nextjs.tsx b/docs/onboarding/error-tracking/nextjs.tsx
index 9474fe83e09b..d368755c8916 100644
--- a/docs/onboarding/error-tracking/nextjs.tsx
+++ b/docs/onboarding/error-tracking/nextjs.tsx
@@ -15,10 +15,27 @@ export const getNextJSSteps = (ctx: OnboardingComponentsContext): StepDefinition
<>
{dedent`
- PostHog can automatically capture unhandled exceptions in your Next.js app using the JavaScript Web SDK.
-
- You can enable exception autocapture for the JavaScript Web SDK in the **Error tracking** section of [your project settings](https://us.posthog.com/settings/project-error-tracking#exception-autocapture).
-
+ PostHog can automatically capture unhandled exceptions in your Next.js app using the JavaScript Web SDK. Turn it on with \`capture_exceptions: true\` in your \`posthog.init\` call, and add \`tracing_headers\` so requests to your own server carry the user's identity to the server-side capture below:
+ `}
+
+
+
+ {dedent`
It is also possible to manually capture exceptions using the \`captureException\` method:
`}
@@ -43,7 +60,7 @@ export const getNextJSSteps = (ctx: OnboardingComponentsContext): StepDefinition
{dedent`
- Next.js uses [error boundaries](https://nextjs.org/docs/app/building-your-application/routing/error-handling#using-error-boundaries) to handle uncaught exceptions by rendering a fallback UI instead of the crashing components. To set one up, create a \`error.tsx\` file in any of your route directories. This triggers when there is an error rendering your component and should look like this:
+ Next.js uses [error boundaries](https://nextjs.org/docs/app/building-your-application/routing/error-handling#using-error-boundaries) to handle uncaught exceptions by rendering a fallback UI instead of the crashing components. To set one up, create a \`error.tsx\` file in any of your route directories. This triggers when there is an error rendering your component. An error boundary catches the error before autocapture sees it, so add the capture to **every** \`error.tsx\` your app has. It should look like this:
`}
{dedent`
- You can also create a [Global Error component](https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-global-errors) in your root layout to capture unhandled exceptions in your root layout.
+ You can also create a [Global Error component](https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-global-errors) in your root layout to capture unhandled exceptions in your root layout. A custom \`global-error.tsx\` replaces the default root boundary, so errors it catches reach PostHog only through its own capture. It also captures render errors that happen before exception autocapture has loaded. Before Next.js 15.2, every client render error is caught by a boundary, so capture in \`global-error.tsx\` as well as in each \`error.tsx\`.
`}
{dedent`
- To capture errors that occur in your server-side code, you can set up an [\`instrumentation.ts\`](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) file at the root of your project. This provides a \`onRequestError\` hook that you can use to capture errors.
+ To capture errors that occur in your server-side code, you can set up an [\`instrumentation.ts\`](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) file at the root of your project. On Next.js 15 and later, it provides an \`onRequestError\` hook that Next.js calls when a Server Component, Route Handler, Server Action, or middleware throws, in both the Node.js and the edge runtime.
Importantly, you need to:
- 1. Set up a \`posthog-node\` client in your server-side code. See our doc on [setting up Next.js server-side analytics](https://posthog.com/docs/libraries/next-js#server-side-analytics) for more.
- 2. Check the request is running in the \`nodejs\` runtime to ensure PostHog works. You can call \`posthog.debug()\` to get verbose logging.
- 3. Get the \`distinct_id\` from the cookie to connect the error to a specific user.
+ 1. Set up a \`posthog-node\` client in your server-side code, and create it in \`register()\` so uncaught exceptions are captured from the moment the server starts. See our doc on [setting up Next.js server-side analytics](https://posthog.com/docs/libraries/next-js#server-side-analytics) for more.
+ 2. Capture in both runtimes: \`posthog-node\` loads its edge build in the edge runtime. Use \`captureExceptionImmediate\` so the event is sent before the request ends.
+ 3. Link the error to the user and their session: read the \`X-POSTHOG-DISTINCT-ID\` and \`X-POSTHOG-SESSION-ID\` headers that \`tracing_headers\` adds first, then the PostHog cookie. Use these values for analytics only, never to authenticate a user.
This looks like this:
`}
@@ -296,40 +315,66 @@ export const getNextJSSteps = (ctx: OnboardingComponentsContext): StepDefinition
blocks={[
{
language: 'javascript',
- file: 'JavaScript',
+ file: 'instrumentation.js',
code: dedent`
- // instrumentation.js
+ import { getPostHogServer } from './lib/posthog-server'
+
export function register() {
- // No-op for initialization
+ getPostHogServer()
}
- export const onRequestError = async (err, request, context) => {
- if (process.env.NEXT_RUNTIME === 'nodejs') {
- const { getPostHogServer } = require('./lib/posthog-server')
- const posthog = getPostHogServer()
- let distinctId = null
- if (request.headers.cookie) {
- // Normalize multiple cookie arrays to string
- const cookieString = Array.isArray(request.headers.cookie)
- ? request.headers.cookie.join('; ')
- : request.headers.cookie
- const postHogCookieMatch = cookieString.match(/ph_phc_.*?_posthog=([^;]+)/)
- if (postHogCookieMatch && postHogCookieMatch[1]) {
- try {
- const decodedCookie = decodeURIComponent(postHogCookieMatch[1])
- const postHogData = JSON.parse(decodedCookie)
- distinctId = postHogData.distinct_id
- } catch (e) {
- console.error('Error parsing PostHog cookie:', e)
- }
+
+ const SESSION_IDLE_MS = 30 * 60 * 1000
+ const SESSION_MAX_MS = 24 * 60 * 60 * 1000
+
+ // The browser's identity: tracing headers first, then the PostHog cookie
+ function getIdentity(headers) {
+ const header = (name) => (Array.isArray(headers[name]) ? headers[name].join('; ') : headers[name])
+ let distinctId = header('x-posthog-distinct-id')
+ let sessionId = header('x-posthog-session-id')
+ const cookieName = \`ph_\${process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN}_posthog=\`
+ const cookie = header('cookie')?.split('; ').find((c) => c.startsWith(cookieName))
+ if (cookie) {
+ try {
+ const data = JSON.parse(decodeURIComponent(cookie.slice(cookieName.length)))
+ distinctId ??= data.distinct_id
+ // $sesid is [last activity, session ID, session start]
+ const [lastActivity, id, start] = data.$sesid ?? []
+ const now = Date.now()
+ if (!sessionId && id && now - lastActivity < SESSION_IDLE_MS && now - start < SESSION_MAX_MS) {
+ sessionId = id
}
+ } catch (e) {
+ // A malformed cookie leaves the error without a user
}
- await posthog.captureException(err, distinctId || undefined)
}
+ return { distinctId, sessionId }
+ }
+
+ export const onRequestError = async (err, request, context) => {
+ const { distinctId, sessionId } = getIdentity(request.headers)
+ const path = request.path.split('?')[0]
+ await getPostHogServer().captureExceptionImmediate(err, distinctId, {
+ $session_id: sessionId,
+ $current_url: \`https://\${request.headers.host}\${path}\`,
+ $request_method: request.method,
+ $request_path: path,
+ nextjs_route_type: context.routeType,
+ nextjs_route_path: context.routePath,
+ })
}
`,
},
]}
/>
+
+ {dedent`
+ A few errors never reach \`onRequestError\`:
+
+ - **Functions passed to \`after()\`**: Next.js logs their errors and moves on. Capture inside the callback: wrap its body in \`try\`/\`catch\`, \`await getPostHogServer().captureExceptionImmediate(error)\`, and rethrow.
+ - **\`proxy.ts\` on Next.js 16**: Next.js does not call \`onRequestError\` for errors thrown in the proxy. Wrap the proxy body the same way.
+ - **Next.js 14 and earlier**: there is no \`onRequestError\`, and \`instrumentation.ts\` runs only with \`experimental: { instrumentationHook: true }\` in \`next.config.js\`. Upgrade to Next.js 15, or capture in your route handlers and server actions with \`getPostHogServer().captureException(error)\`.
+ `}
+
{dedent`
You can find a full example of both this and client-side error tracking in our [Next.js error monitoring tutorial](https://posthog.com/tutorials/nextjs-error-monitoring).