BugBoardDocs

Nuxt

Nuxt's runtimeConfig already splits public from private for you, which maps cleanly onto the two key types: public reaches the browser and takes the publishable key, everything else stays on the server and takes the secret key.

1. Install

npm i bugboard

2. Configure runtimeConfig

// nuxt.config.ts
export default defineNuxtConfig({
    runtimeConfig: {
        bugboardKeyId: '', // NUXT_BUGBOARD_KEY_ID — server only
        bugboardSigningSecret: '', // NUXT_BUGBOARD_SIGNING_SECRET — server only
        public: {
            bugboardApiKey: '', // NUXT_PUBLIC_BUGBOARD_API_KEY — client
        },
    },
});
NUXT_BUGBOARD_KEY_ID=bbk_xxxxxxxx
NUXT_BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx
NUXT_PUBLIC_BUGBOARD_API_KEY=bb_pub_xxxxxxxx

Never put the signing secret under public — anything there is serialized into the page.

3. The client plugin

This also wires up Vue's error handler:

// plugins/bugboard.client.ts
import { createClient } from 'bugboard';

export default defineNuxtPlugin((nuxtApp) => {
    const config = useRuntimeConfig();

    const bugboard = createClient({
        apiKey: config.public.bugboardApiKey,
        environment: import.meta.dev ? 'development' : 'production',
    });

    nuxtApp.hook('vue:error', (error, instance, info) => {
        bugboard.critical(`Vue error: ${info}`, error, ['nuxt', 'client']);
    });

    return { provide: { bugboard } };
});

Using info — Vue's lifecycle hook name, a small fixed set — in the title rather than the raw error message keeps cards well grouped instead of producing one per message variant.

4. The server plugin

// server/plugins/bugboard.ts
import { createClient } from 'bugboard';

export default defineNitroPlugin((nitroApp) => {
    const config = useRuntimeConfig();

    const bugboard = createClient({
        keyId: config.bugboardKeyId,
        signingSecret: config.bugboardSigningSecret,
        environment: process.env.NODE_ENV,
    });

    nitroApp.hooks.hook('error', async (error, { event }) => {
        bugboard.criticalHigh(
            `Server error: ${event?.path ?? 'unknown'}`,
            error,
            ['nuxt', 'server'],
        );
        await bugboard.flush(); // Nitro may be deployed to a serverless target
    });
});

The await flush() here is deliberate: Nitro targets Node, Vercel, Cloudflare and Deno from the same code, and on the serverless targets nothing else will drain the queue. On a Node target the flush is nearly free, so leaving it in costs you nothing.

5. Report from components

<script setup lang="ts">
const { $bugboard } = useNuxtApp();

async function submit() {
    try {
        await $fetch('/api/checkout', { method: 'POST' });
    } catch (err) {
        $bugboard.major('Checkout request failed', err, ['checkout']);
    }
}
</script>

Server routes

In a server route or API handler, resolve the server client and flush in a finally:

// server/api/checkout.post.ts
export default defineEventHandler(async (event) => {
    try {
        return await processCheckout(await readBody(event));
    } catch (err) {
        bugboard.criticalHigh(`Checkout API failed: ${event.path}`, err, [
            'api',
        ]);
        throw createError({
            statusCode: 500,
            statusMessage: 'Checkout failed',
        });
    } finally {
        await bugboard.flush();
    }
});

Keep the title stable — event.path on a route with a parameter (/api/orders/91847) creates a card per order. Prefer a fixed string, or the route pattern, and let the description carry the specifics. See Deduplication.

Real file names instead of chunk names

Nuxt bundles both halves of your app, so by default cards name a chunk rather than the component or handler you wrote. Two changes fix it, and neither uploads a source map.

The build plugin — register it twice. Nuxt builds your app with Vite, but Nitro builds server/ with its own Rollup pipeline, which never sees vite.plugins. Miss the second registration and every server route still reports a chunk:

// nuxt.config.ts
import bugboard from 'bugboard/vite';

export default defineNuxtConfig({
    vite: { plugins: [bugboard()] },
    nitro: { rollupConfig: { plugins: [bugboard()] } },
});

This covers .vue components too — the plugin rewrites their <script> blocks before the Vue compiler runs, which is the only point at which the file still has the line numbers you wrote. A call placed directly in markup (@click="() => bugboard.minor('…')") is left alone, because by then its line numbers belong to the compiler's output; move it into <script> for an exact location.

Nitro's server stacks — Node's own source-map support. Add the preload import at the top of your server plugin, above every other import, and make sure Nitro emits server maps:

// server/plugins/bugboard.ts — line 1
import 'bugboard/preload';
// nuxt.config.ts
export default defineNuxtConfig({
    nitro: {
        sourceMap: true,
    },
});

Node only maps modules compiled after support is switched on, which is why the import has to come first. Full detail in the JavaScript SDK guide.

Next steps