BugBoardDocs

Next.js

Next.js runs your code in three places — the server, the client bundle, and possibly a serverless or edge runtime — and each needs different treatment. The pattern is two client modules and one guard.

1. Install

npm i bugboard

2. Two modules, one guard

// lib/bugboard.server.ts
import 'server-only'; // build error if this is ever imported from a client component
import { createClient } from 'bugboard';

export default createClient({
    keyId: process.env.BUGBOARD_KEY_ID, // bbk_…
    signingSecret: process.env.BUGBOARD_SIGNING_SECRET, // bb_sec_…
    environment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
    release: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7),
});
// lib/bugboard.client.ts
'use client';
import { createClient } from 'bugboard';

export default createClient({
    apiKey: process.env.NEXT_PUBLIC_BUGBOARD_API_KEY, // bb_pub_…
    environment: process.env.NEXT_PUBLIC_VERCEL_ENV,
});
BUGBOARD_KEY_ID=bbk_xxxxxxxx
BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx
NEXT_PUBLIC_BUGBOARD_API_KEY=bb_pub_xxxxxxxx

The server-only package (npm i server-only) turns a leaked secret key into a build failure. Use it — never put a secret key behind a NEXT_PUBLIC_ prefix; anything with a client-exposed prefix ends up in the bundle, and the SDK cannot detect this for you.

3. Route handlers and server actions

Flush before returning, because these may run on a serverless or edge target where nothing else will drain the queue:

// app/api/checkout/route.ts
import bugboard from '@/lib/bugboard.server';

export async function POST(request: Request) {
    try {
        return Response.json(await processCheckout(await request.json()));
    } catch (err) {
        bugboard.criticalHigh('Checkout API failed', err, ['api', 'checkout']);
        return Response.json({ error: 'Checkout failed' }, { status: 500 });
    } finally {
        await bugboard.flush();
    }
}

flush() on an empty queue is effectively free, so an unconditional finally is the right shape — you don't need to track whether you reported anything.

4. Error boundaries

App Router gives you two files, and you want both.

// app/error.tsx — recoverable errors within a route segment
'use client';
import { useEffect } from 'react';
import bugboard from '@/lib/bugboard.client';

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string };
    reset: () => void;
}) {
    useEffect(() => {
        bugboard.critical(`Route error: ${error.message}`, error, ['nextjs', 'client']);
    }, [error]);

    return (
        <div>
            <h2>Something went wrong</h2>
            <button onClick={reset}>Try again</button>
        </div>
    );
}
// app/global-error.tsx — root layout errors; must render <html> and <body>
'use client';
import { useEffect } from 'react';
import bugboard from '@/lib/bugboard.client';

export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
    useEffect(() => {
        bugboard.criticalHigh(`Root error: ${error.message}`, error, ['nextjs', 'fatal']);
    }, [error]);

    return (
        <html>
            <body>
                <h2>Something went wrong</h2>
            </body>
        </html>
    );
}

In production, a server component error reaching the client is redacted to a generic message plus a digest. So error.message from error.tsx is often unhelpful, and the useful report is the server-side one — which is exactly what the next hook is for.

5. instrumentation.ts — the server errors you'd otherwise miss

This catches server errors Next.js handles internally, before they are redacted:

// instrumentation.ts (Next.js 15+)
export async function onRequestError(err, request, context) {
    const bugboard = (await import('@/lib/bugboard.server')).default;

    bugboard.criticalHigh(
        `Server error: ${context.routePath ?? request.path}`,
        err,
        ['nextjs', 'server', context.routerKind],
    );

    await bugboard.flush();
}

Use context.routePath (the pattern) over request.path (the URL), so repeated failures deduplicate into one card instead of one per id. See Deduplication.

Pages Router, if that's what you're on: use _error.tsx for client errors and wrap API routes in the same try/finally as above.

6. Real file names instead of chunk names

A Next.js build minifies and chunks both halves of your app, so out of the box cards say .next/server/chunks/456.js:1 rather than app/api/checkout/route.ts:42. Two changes fix that, and neither uploads a source map anywhere.

Install the build plugin. It writes the real file and line into each call at build time, before chunking happens, and it covers client and server code alike.

Turbopack — the default in Next 16, and in Next 15 with --turbopack:

// next.config.js
module.exports = {
    turbopack: {
        rules: {
            '*.ts': { loaders: ['bugboard/webpack'] },
            '*.tsx': { loaders: ['bugboard/webpack'] },
        },
    },
};

Do not add an as field to those rules. Turbopack appends it to the filename, so as: '*.tsx' makes it look for page.tsx.tsx and the build fails.

webpack — Next 15's default, or next build --webpack:

// next.config.js
module.exports = {
    webpack(config) {
        config.module.rules.push({
            test: /\.[cm]?[jt]sx?$/,
            exclude: /node_modules/,
            enforce: 'pre', // must precede SWC, while line numbers are original
            use: 'bugboard/webpack',
        });
        return config;
    },
};

On Next 16 a webpack key with no turbopack key is a hard build error, because Turbopack is the default. Use the Turbopack form, or pass --webpack explicitly.

Either way, cards then read app/api/checkout/route.ts:42 and app/page.tsx:18 — including calls written inline in JSX, like onClick={() => bugboard.minorLow('…')}.

What this does not fix

file_name and line_number become exact, but the stack inside a caught error's description stays as chunk coordinates on the Next server. That is not something the SDK can change: Next installs its own Error.prepareStackTrace (next/dist/server/patch-error-inspect.js) to power its error overlay, which replaces the formatter Node applies source maps in. Enabling source maps has no effect on error.stack while that patch is installed — verified on Next 16 with bugboard/preload, with NODE_OPTIONS=--enable-source-maps, and with experimental.serverSourceMaps: true; all three leave the stack text unchanged.

So on Next, rely on file_name/line_number for location. bugboard/preload is still worth adding for non-Next Node processes in the same codebase, but it will not change what Next's server puts in a stack.

With both in place, server stacks — including the stack inside a caught error's description — name your real source files.

Delivery notes

  • On Vercel, route handlers, server actions and onRequestError all run in an environment that can freeze the instant you return — the await flush() is not optional there.
  • On a long-running Node server (next start in a container), the automatic beforeExit hook covers normal shutdown, but not SIGTERM — see the Node guide.
  • In the browser, the SDK flushes on pagehide with keepalive, so client reports survive the page going away.

Next steps

  • JavaScript SDK — the base guide and the full method surface.
  • Serverless & edgewaitUntil and tuning for pay-per-millisecond runtimes.
  • Vite SPA — the browser-side global handlers, which apply to the Next.js client bundle too.
  • API Reference — every config option and the HTTP contract.