@nreactive/next
v0.2.0
Published
Next.js integration for @nreactive/core — App Router & Pages Router request context, server actions, and a React provider that catches client errors
Maintainers
Readme
@nreactive/next
Next.js integration for @nreactive/core — App Router & Pages Router request context, server actions, and a React provider that catches client errors for the nreactive AI error-fix pipeline.
- Server: three small wrappers (
withNreactive,withNreactiveAction,withNreactivePagesApi) that open anAsyncLocalStoragecontext for the request and capture-and-rethrow on error. Method, URL, request ID, and scrubbed headers are attached to every event. - Client: a single
<NreactiveProvider>that's a strict superset of the standalone browser script — runtime exceptions, unhandled rejections, fetch + XHR failures,console.error/console.warn, resource load errors, CSP violations, and long tasks — plus a React Error Boundary so render-time errors reach the dashboard too. - Works with Next.js 14, 15, and 16 on Node ≥ 20. React 18 or 19.
Install
npm install @nreactive/core @nreactive/next
# or: pnpm add @nreactive/core @nreactive/next
# or: yarn add @nreactive/core @nreactive/next
# or: bun add @nreactive/core @nreactive/nextAI-assisted setup
Paste the prompt below into Claude Code, Cursor, Copilot, or any agent with file-write access. It has enough information to finish the integration end-to-end — instrumentation.ts, the route wrappers, the server-action wrappers, and the React provider — with no manual wiring.
You are adding the
@nreactive/core+@nreactive/nextSDKs to this Next.js project.nreactiveis a production error-monitoring service that opens AI-generated pull requests to fix the errors it catches.
- Detect the package manager from the lockfile (
package-lock.json→ npm,pnpm-lock.yaml→ pnpm,yarn.lock→ yarn,bun.lockb→ bun) and install@nreactive/coreand@nreactive/next.- Create or update
instrumentation.tsat the project root (orsrc/instrumentation.tsif the project uses asrc/layout). Itsregisterfunction should callinitfrom@nreactive/coreonly whenNEXT_RUNTIME === "nodejs".- Wrap every App Router route handler under
app/api/**withwithNreactivefrom@nreactive/next.- Wrap every server action that throws on user input with
withNreactiveActionfrom@nreactive/next.- Wrap every Pages Router API handler under
pages/api/**withwithNreactivePagesApifrom@nreactive/next.- In
app/layout.tsx(orpages/_app.tsx), wrap{children}with<NreactiveProvider appId={process.env.NEXT_PUBLIC_NREACTIVE_APP_ID!}>imported from@nreactive/next/client. If the project already includes<script src="https://nreactive.com/integration.js">, REMOVE it — running both would double-fire events.- Add
NREACTIVE_APP_ID=andNEXT_PUBLIC_NREACTIVE_APP_ID=to.env.example. The user obtains their App ID from https://nreactive.com/dashboard/apps.- Restart the dev server and confirm both server and client events appear in the dashboard.
Do not invent additional configuration. If the project uses Edge Runtime for some routes, leave those routes unwrapped —
withNreactivedepends on Node'sasync_hooks. Stop and ask if the project structure doesn't match these assumptions.
Quick start
instrumentation.ts
Next runs this once per process at startup. Calling init here is the cheapest way to get uncaughtException / unhandledRejection capture and outbound HTTP failure capture.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME !== "nodejs") return;
const { init } = await import("@nreactive/core");
init({
appId: process.env.NREACTIVE_APP_ID!,
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
});
}App Router route handler
// app/api/users/route.ts
import { withNreactive } from "@nreactive/next";
export const GET = withNreactive(async (request) => {
const users = await db.users.findAll();
return Response.json(users);
});
export const POST = withNreactive(async (request) => {
const body = await request.json();
const user = await db.users.create(body);
return Response.json(user, { status: 201 });
});Server action
// app/actions.ts
"use server";
import { withNreactiveAction } from "@nreactive/next";
export const submitContact = withNreactiveAction(async (formData: FormData) => {
const email = String(formData.get("email") ?? "");
if (!email) throw new Error("email required");
await db.contacts.insert({ email });
});Pages Router API route
// pages/api/echo.ts
import { withNreactivePagesApi } from "@nreactive/next";
export default withNreactivePagesApi(async (req, res) => {
res.status(200).json({ method: req.method });
});Client provider
// app/layout.tsx
import { NreactiveProvider } from "@nreactive/next/client";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<NreactiveProvider appId={process.env.NEXT_PUBLIC_NREACTIVE_APP_ID!}>
{children}
</NreactiveProvider>
</body>
</html>
);
}The provider is a strict superset of the standalone <script src="https://nreactive.com/integration.js"> tag — if your app already includes that script, remove it when you switch to the provider. Running both would double-fire every event.
Get your App ID from the nreactive dashboard.
How it works
withNreactivewraps an App Router route handler. It opens a freshAsyncLocalStorageframe, populates it with{ method, url, requestId, headers }, and runs the handler inside. AnycaptureExceptionduring the handler's async subtree picks up the request context. Errors thrown from the handler are captured and rethrown so Next still returns its 500.withNreactiveActionwraps a server action. Server actions don't get aRequest, so we read the request-timenext/headersinstead — wrapped in a try/catch so the wrapper still works in tests wherenext/headersisn't available. Captured events get akind: "server-action"tag for dashboard filtering.withNreactivePagesApiwraps a Pages Router API handler. Same context shape as the App Router wrapper, just driven from the Node-stylereq/respair.<NreactiveProvider>is a"use client"island that installs the eight standard browser capture surfaces and wrapschildrenin a React Error Boundary. Render-time errors that React catches are shipped to the dashboard witherrorType: "ReactRenderError".- Request IDs are sourced in this order:
X-Request-IDheaderX-Correlation-IDheader- Generated
randomUUID()
API
withNreactive(handler, options?)
Wraps an App Router route handler (request: Request, ctx?) => Response | Promise<Response>.
interface NreactiveOptions {
/** Header names to redact. Merged with @nreactive/core defaults. */
scrubHeaders?: string[];
/** Query param names to redact. Merged with @nreactive/core defaults. */
scrubQueryParams?: string[];
/** Emit a breadcrumb per invocation. Default: true. */
breadcrumbs?: boolean;
/** Decide whether to capture a given thrown error. Default: always true. */
shouldCapture?: (err: unknown) => boolean;
}withNreactiveAction(action, options?)
Wraps a server action (...args) => R | Promise<R>. Same options as withNreactive.
withNreactivePagesApi(handler, options?)
Wraps a Pages Router API handler (req, res) => void | Promise<void>. Same options as withNreactive.
<NreactiveProvider>
interface NreactiveProviderProps {
appId: string;
endpoint?: string; // default "https://nreactive.com"
release?: string;
fallback?: (err: Error) => ReactNode;
disable?: Partial<{
onerror: boolean;
unhandledrejection: boolean;
resourceErrors: boolean;
cspViolations: boolean;
consoleError: boolean;
consoleWarn: boolean;
fetch: boolean;
xhr: boolean;
longTasks: boolean;
}>;
children: ReactNode;
}useNreactive()
interface ManualCaptureApi {
captureException(err: unknown, severity?: "critical" | "error" | "warn"): void;
captureMessage(msg: string, severity?: "critical" | "error" | "warn"): void;
/** Accepted for forward compatibility — currently a no-op. */
addBreadcrumb(b: { category: string; message: string; level?: "debug" | "info" | "warn" | "error" }): void;
}"use client";
import { useNreactive } from "@nreactive/next/client";
export default function ContactButton() {
const { captureException } = useNreactive();
return (
<button onClick={async () => {
try {
await fetch("/api/contact", { method: "POST" });
} catch (err) {
captureException(err);
}
}}>
Contact
</button>
);
}Interaction with Next's error.tsx
Next renders a route-segment error.tsx whenever a render error escapes a Server Component. The provider's error boundary sits above every route segment, so:
- A render error inside a route segment is first caught by that segment's
error.tsx(if present). The user sees your fallback UI. - A render error that bubbles past every
error.tsx(e.g. an error inapp/layout.tsx) is caught by the provider's boundary. The user sees the provider'sfallback(ornull, by default). - In both cases, the provider's boundary records the event because
componentDidCatchruns on every ancestor boundary, not just the topmost. So you don't need to instrumenterror.tsxseparately.
Edge runtime
The server wrappers depend on node:async_hooks, which isn't available on Edge Runtime. If a route uses export const runtime = "edge", leave it unwrapped — init() from instrumentation.ts plus the provider on the client will still capture errors there, you just won't get per-request server context. Mix-and-match is fine.
Why the provider is a superset of the script tag
Three reasons:
- One source of capture per page. If you use both
<script src="…/integration.js">and<NreactiveProvider>, every event fires twice. Picking one is the cleaner mental model. - The script tag can't catch render errors. React render errors don't bubble to
window.onerror; only an Error Boundary sees them. The provider has one built in. - The provider unmounts cleanly. HMR, tests, and the React StrictMode double-mount all need a deterministic teardown — the singleton client is reference-counted and restores
window.fetch,XMLHttpRequest,console.error/warn, andwindow.onerrorto their originals when the last provider unmounts.
If you currently include the script tag, remove it when you adopt this package.
Links
- Core SDK:
@nreactive/core - Express adapter:
@nreactive/express - Fastify adapter:
@nreactive/fastify - Full documentation: https://nreactive.com/docs
- Dashboard: https://nreactive.com/dashboard
License
PROPRIETARY. See LICENSE.
