# Next.js

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

| Entry | Runs where | Gives you |
| --- | --- | --- |
| `@newinstance/bugwatch/next` | Server only (route handlers, serverless functions) | `withBugWatchRouteHandler` |
| `@newinstance/bugwatch/next/client` | Browser 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.

```bash
npm install @newinstance/bugwatch
```


## Server side

### 1. Create the client once, in a shared module

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.

```ts
// 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 handlers

```ts
// 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 matters

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:

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

### 4. Attaching the user

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.

```ts
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`.

```ts
// 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 browser

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:

```ts
// 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 side

### 1. Initialize in a client component

`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.

```tsx
// 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:

```tsx
// 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 boundary

`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:

```tsx
// 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.tsx`

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:

```tsx
// 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.


## Tracing

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

```ts
// 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:

```ts
// 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.


## Troubleshooting

**`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`.
