From 170b2ca5c2ef3bda711acb008b98faee5aa30341 Mon Sep 17 00:00:00 2001 From: ablaszkiewicz Date: Wed, 28 Oct 2026 21:54:10 +0100 Subject: [PATCH] feat(error-tracking): capture in existing boundaries, version-aware server hooks, server identity The capture task now adds capture to every error boundary the app already has (each Next.js error.tsx, a custom global-error.tsx, a Pages Router _error or _app boundary), checks the installed framework version before wiring a hook that version may not have (Next.js 14 has no onRequestError), starts the server client at boot, and links server errors to the browser user through tracing headers and the PostHog cookie. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FkuJAD3n8f6Ra7L9p2UmPw --- .../error-tracking/capture-exceptions.md | 46 ++++++++++++++++--- .../error-tracking-step/description.md | 4 +- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/context/agents/error-tracking/capture-exceptions.md b/context/agents/error-tracking/capture-exceptions.md index 2bfd887d..e33bcf9e 100644 --- a/context/agents/error-tracking/capture-exceptions.md +++ b/context/agents/error-tracking/capture-exceptions.md @@ -24,9 +24,10 @@ never manual capture calls sprinkled across files. An app that runs code on a server as well as in the browser has two runtimes, and each needs its own capture. Cover every runtime the docs cover for this framework and the app actually uses: the server side takes the SDK's server -library, where the framework hands you its errors (for Next.js, -`onRequestError` in `instrumentation.ts`). A browser-only setup leaves every -server error uncaptured. An Electron app has the same split: its main process +library, where the framework hands you its errors (for Next.js 15 and later, +`onRequestError` in `instrumentation.ts`, in both runtimes Next.js calls it +from: Node.js and edge). A browser-only setup leaves every server error +uncaptured. An Electron app has the same split: its main process runs Node, and its uncaught errors never reach the renderer's capture, so the main process takes the server library. A packaged app ships without the developer's `.env`, so a runtime `process.env` lookup in the main process is @@ -34,6 +35,16 @@ always empty there: read the token the way the bundler inlines it at build time (electron-vite: `import.meta.env.MAIN_VITE_*`). A fully static app has no server runtime to cover. +A framework hook exists only from the version that introduced it, so read the +installed framework version from the lockfile before you wire one. Next.js 14 +never calls `onRequestError`, and runs `instrumentation.ts` only with +`experimental.instrumentationHook: true`. When the installed version has no +hook for its server errors, do not export one it will never call: name the +errors that stay uncaptured in your handoff, and the upgrade that adds the +hook. Create the server client when the server starts (in Next.js, from +`register()`), not on the first failed request, so its process-level +autocapture listens from boot. + A page often shows two setups side by side: a global initializer, and an explicit client you hold. Follow one of them the whole way through. Taking the wiring from one and the calls from the other reads fine and compiles nowhere — @@ -79,6 +90,20 @@ environment, so a build that cannot find an SDK (`SDK location not found`) failed because of this shell, not your edit: do not retry it, and say so in your handoff. +## Name the user on server errors + +A server error links to the person and the session replay only when its +capture carries the browser's identity. Where the browser calls the app's own +server (one app such as Next.js, or a separate API), send it: set +`tracing_headers` in the browser init to the hostnames the browser calls +(`window.location.hostname` for the app's own requests). On the server, take +the identity from the `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-SESSION-ID` +request headers first, then from the PostHog cookie: its distinct ID, and the +session ID from `$sesid` while that session is fresh. Pass the distinct ID to +the capture call and the session ID as `$session_id`, following the docs' +pattern. These values are analytics context: never use them to authenticate +anyone. + ## Do not copy the SDK's own listeners Exception autocapture already registers the global handlers: `window.onerror` @@ -96,7 +121,11 @@ global handler fires. Express error middleware, Fastify `setErrorHandler`, Hono `onError`, Vue `app.config.errorHandler`, Angular `ErrorHandler`, SvelteKit `handleError`, a React error boundary, Next.js `global-error` — hook those, because the framework swallows the error and the global listener never hears -of it. NestJS is one more layer up: its exception filters answer every request +of it. The boundaries the app already has are such hooks too: add capture to +every one that exists — each Next.js `error.tsx`, a custom `global-error.tsx`, +a Pages Router `_error` or `_app` boundary — since each swallows what it +catches. Creating new boundaries per route is the hand-wrapping to avoid; +capturing in the ones the app has is not. NestJS is one more layer up: its exception filters answer every request error before Express error middleware runs, so capture goes in a global filter (the app's existing `@Catch()` filter when it has one), not in `setupExpressErrorHandler`. A platform with no autocapture at all, such as Go, @@ -111,6 +140,9 @@ and every PostHog library in the manifest is imported. No global error listener of your own sits beside the SDK's autocapture. The project builds as well as it did before your edits. You installed nothing but the server library your capture imports, and did not run tests, search outside the project, or read -through the whole app or hand-wrap individual components or routes. Your handoff names the files you changed, the capture mechanism, and -the build command you ran with its result, so the report can explain it to the -user. +through the whole app or hand-wrap individual components or routes. Every +error boundary the app already had now captures, and server captures carry the +browser's distinct ID and session ID. Your handoff names the files you changed, +the capture mechanism, the server errors that stay uncaptured on the installed +version (none, when every hook exists), and the build command you ran with its +result, so the report can explain it to the user. diff --git a/context/skills/integration-v2/error-tracking-step/description.md b/context/skills/integration-v2/error-tracking-step/description.md index 352bbea7..dbf5af9c 100644 --- a/context/skills/integration-v2/error-tracking-step/description.md +++ b/context/skills/integration-v2/error-tracking-step/description.md @@ -6,7 +6,9 @@ reach PostHog — one handler, not hand-wrapped across files. Follow the framework's own mechanism for a global error handler, using the reference example and the docs for the exact pattern. Find the init or app entry, add the handler there. Do not read through the whole app or wrap individual -components or routes by hand. +components or routes by hand. Error boundaries the app already has (a Next.js +`error.tsx`, a custom `global-error.tsx`) swallow what they catch: add capture +to each of them. If the app already has natural exception boundaries — a server-side API error handler, a critical flow with its own try/catch — capturing there too is worth a