Next.js

BugWatch with Next.js (App Router)link

@newinstance/bugwatch ships two separate Next.js entries, and picking the right one per file is the whole game:

EntryRuns whereGives you
@newinstance/bugwatch/nextServer only (route handlers, serverless functions)withBugWatchRouteHandler
@newinstance/bugwatch/next/clientBrowser only (client components)initBrowser, getBrowserClient, BugWatchErrorBoundary, captureRenderError

Never import @newinstance/bugwatch/next (or /node) from a client component. That entry pulls in Node built-ins such as AsyncLocalStorage; the /next/client entry deliberately pulls in none.

npm install @newinstance/bugwatch

Server sidelink

1. Create the client once, in a shared modulelink

The client is a singleton for your server process. Put it in one module and import it everywhere else, so route handlers never each build their own.

// lib/bugwatch.ts  (server only)
import { createClient } from "@newinstance/bugwatch";

export const client = createClient({
	projectKey: process.env.BUGWATCH_KEY!,
	serviceName: "storefront-web",
	release: process.env.VERCEL_GIT_COMMIT_SHA,
});

BUGWATCH_KEY is a server secret. Do not prefix it with NEXT_PUBLIC_, and never import this module from a "use client" file.

2. Wrap your route handlerslink

// app/api/checkout/route.ts
import type { NextRequest } from "next/server";
import { withBugWatchRouteHandler } from "@newinstance/bugwatch/next";
import { client } from "@/lib/bugwatch";

export const POST = withBugWatchRouteHandler(client, async (req: NextRequest) => {
	const body = await req.json();
	return Response.json({ ok: true, id: await checkout(body) });
});

What the wrapper does on every invocation:

  1. Opens a per-request AsyncLocalStorage scope around your handler, so concurrent requests never mix identities.
  2. Records method and url from the first argument onto that scope.
  3. Reads an inbound traceparent header (via req.headers.get("traceparent")) into the request scope, joining the caller's distributed trace. If no header is present, a fresh trace ID is synthesized for the request.
  4. On a thrown error: captures it at level 50 with method and url tags, awaits client.flush(), then re-throws so Next.js error.tsx and your own error handling still run.

3. Why the flush before the re-throw matterslink

On Vercel Functions, AWS Lambda, and every other serverless runtime, the execution environment is frozen the instant the response is returned. Any events still sitting in the SDK's in-memory batch queue are not sent; they are sent later at best, or lost when the container is recycled. withBugWatchRouteHandler awaits the flush before re-throwing, which is exactly the window in which the runtime is still alive.

The same wrapper works for plain AWS Lambda handlers, Vercel Functions, and Netlify Functions. Anything that must deliver before it returns.

If you capture something manually on a success path in a serverless handler, flush it yourself:

client.captureMessage("checkout completed with fallback pricing", 30);
await client.flush();

4. Attaching the userlink

Two options, and they compose.

getUser option for identity that middleware.ts forwards on the request (a header or a cookie). It is resolved lazily at capture time, not at wrap time.

export const GET = withBugWatchRouteHandler(
	client,
	async (req: NextRequest) => Response.json(await listInvoices()),
	{ getUser: (req) => ({ id: req.headers.get("x-user-id") ?? "anonymous" }) },
);

setRequestUser for identity resolved inside the handler, which is the usual App Router shape. An explicit setRequestUser always wins over getUser.

// app/api/invoices/route.ts
import { setRequestUser, setRequestTag, setRequestContext } from "@newinstance/bugwatch/node";

export const POST = withBugWatchRouteHandler(client, async (req: NextRequest) => {
	const session = await auth();
	setRequestUser({ id: session.user.id, email: session.user.email });
	setRequestTag("plan", session.user.plan);
	setRequestContext("invoice", { batch: "2026-08" });

	throw new Error("boom"); // captured with user, tags, context, and trace ID attached
});

These three helpers only apply inside an open request scope, which the wrapper provides. They write that request's scope only, never the process-global scope.

5. The session mint endpoint for the browserlink

The browser must never hold your project key. Instead your server mints short-lived, ingest-only session tokens. One route handler is all it takes:

// app/api/bugwatch/session/route.ts
import { mintBrowserSession } from "@newinstance/bugwatch";

// The browser SDK fetches sessionUrl with GET, so export GET here.
export async function GET() {
	const session = await mintBrowserSession({ projectKey: process.env.BUGWATCH_KEY! });
	return Response.json(session); // { token, expiresAt }
}

Your server calls POST https://api.newinstance.cloud/api/v1/bugwatch/browser-session with your x-api-key header and returns the resulting token to the browser. Browser events then go to https://api.newinstance.cloud/api/v1/bugwatch/ingest/browser with an x-bugwatch-session header. Your secret never leaves the server.


Browser sidelink

1. Initialize in a client componentlink

initBrowser is one import and one call. It is SSR safe (with no window it returns the existing client, or a no-op), idempotent (a global singleton, so HMR and double imports are fine), and it never throws. A bad config or an unreachable session URL degrades to a working no-op client plus a single console.warn, so the crash reporter can never crash your app.

// app/bugwatch-init.tsx
"use client";
import { initBrowser } from "@newinstance/bugwatch/next/client";

initBrowser({
	sessionUrl: "/api/bugwatch/session",
	release: process.env.NEXT_PUBLIC_RELEASE,
});

export function BugWatchInit() {
	return null;
}

Mount it once in the root layout, which is a server component, so the client boundary stays at the leaf:

// app/layout.tsx
import { BugWatchInit } from "./bugwatch-init";

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<html lang="en">
			<body>
				<BugWatchInit />
				{children}
			</body>
		</html>
	);
}

A relative sessionUrl is resolved against window.location.href for you. Use getBrowserClient() anywhere else to reach the same singleton. On Pages Router, put the same initBrowser call at the top of pages/_app.tsx.

Only ever pass sessionUrl in the browser. Passing projectKey ships your secret to every visitor. If that ever happens, rotate the key in the BugWatch dashboard under Project settings, API keys.

2. Catch render errors with the error boundarylink

BugWatchErrorBoundary catches errors thrown in the render tree below it, including the component stack, and renders fallback instead of the crashed subtree. It takes the client as a prop:

// app/providers.tsx
"use client";
import { initBrowser, BugWatchErrorBoundary } from "@newinstance/bugwatch/next/client";

const client = initBrowser({ sessionUrl: "/api/bugwatch/session" });

export function Providers({ children }: { children: React.ReactNode }) {
	return (
		<BugWatchErrorBoundary client={client} fallback={<p>Something went wrong.</p>}>
			{children}
		</BugWatchErrorBoundary>
	);
}

Omit fallback and it renders nothing when there is an error.

3. Report from error.tsxlink

Next.js has its own error boundary per route segment. Hook captureRenderError into it so those errors reach BugWatch with the Next.js digest, which is what lets you line up a client report with the server log entry:

// app/error.tsx
"use client";
import { useEffect } from "react";
import { captureRenderError } from "@newinstance/bugwatch/next/client";

export default function Error({
	error,
	reset,
}: {
	error: Error & { digest?: string };
	reset: () => void;
}) {
	useEffect(() => {
		captureRenderError(error, { digest: error.digest });
	}, [error]);

	return <button onClick={reset}>Try again</button>;
}

captureRenderError uses the browser singleton, tags the event source: next.error-boundary plus digest, triggers a flush, and returns the event ID. If initBrowser never ran it still returns a fresh event ID rather than throwing, so your error page always renders.


Tracinglink

Server side, wrap an operation in a span to place it on the trace waterfall. Everything captured inside the callback links to it:

// app/api/checkout/route.ts
export const POST = withBugWatchRouteHandler(client, async (req: NextRequest) => {
	const cart = await client.withSpan(
		"db.query load-cart",
		async (span) => {
			span.setAttr("db.system", "postgresql");
			return loadCart(req.nextUrl.searchParams.get("cart")!);
		},
		{ kind: 3 },
	);
	return Response.json(cart);
});

For outbound HTTP, wrapFetch opens a client-kind span per request, injects a traceparent header so the downstream service joins the trace, tags the response status, and marks 5xx and network failures as error spans:

// lib/bugwatch.ts
import { wrapFetch } from "@newinstance/bugwatch";
export const tracedFetch = wrapFetch(client);

An existing traceparent header on a request is never overwritten. Span creation is a projectKey mode feature: the browser sessionUrl mode captures errors and logs but creates no spans, while wrapFetch still injects the traceparent header so the server you call joins the trace.


Troubleshootinglink

BugWatchResolutionError, or every export is undefined. You have @newinstance/bugwatch/* entries in compilerOptions.paths in tsconfig.json. Next.js honours paths in its bundler, so it rewrites the runtime import to the .d.ts declaration stub and every export becomes undefined, surfacing as (void 0) is not a function. The SDK detects this and logs a named BugWatchResolutionError with the fix. Delete those paths entries. Since v0.2.0 the package ships a typesVersions map, so even "moduleResolution": "node" resolves every subpath's types with no aliases at all.

Changes to the entry you import are not taking effect. Next.js caches aggressively. Delete .next and restart the dev server. This is the usual cause when you have just switched a file from @newinstance/bugwatch/next to @newinstance/bugwatch/next/client and still see a Node built-in resolution error in the browser console.

"Module not found: can't resolve 'node:async_hooks'" in the browser bundle. A client component is importing the server entry, directly or through a shared module that also exports your server client. Keep lib/bugwatch.ts server only and import @newinstance/bugwatch/next/client from client components.

No browser events arrive. Check that GET /api/bugwatch/session returns { token, expiresAt } in the browser network tab. The SDK fetches the token on first use, caches it, and refreshes it before expiry and on a 401.