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.

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.