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.

Next steps