BugBoardDocs

Vite SPA: React, Vue, Svelte

A pure client-side app is the simplest integration in the whole SDK: one publishable key, one module, and the browser's own lifecycle handles delivery.

1. Install

npm i bugboard

2. The client module

// src/lib/bugboard.ts
import { createClient } from 'bugboard';

export default createClient({
    apiKey: import.meta.env.VITE_BUGBOARD_API_KEY, // bb_pub_…
    environment: import.meta.env.MODE,
    release: __APP_VERSION__, // via vite.config define, if you want it
    enabled: import.meta.env.PROD, // don't report from local dev
});
VITE_BUGBOARD_API_KEY=bb_pub_xxxxxxxx

A publishable key is public by design and write-only — it is fine that anyone can read it out of your bundle; the worst they can do is create cards on your board. Never put a signing secret behind a VITE_ prefix; it would be compiled into the JavaScript you ship.

3. Global handlers

These catch what your components don't:

// src/main.ts
import bugboard from './lib/bugboard';

window.addEventListener('error', (event) => {
    bugboard.critical(`Uncaught: ${event.message}`, event.error, ['browser']);
});

window.addEventListener('unhandledrejection', (event) => {
    bugboard.critical('Unhandled promise rejection', event.reason, ['browser']);
});

Cross-origin scripts. Browsers report errors from a different origin as the opaque string "Script error." with no stack. To get real messages from a CDN-hosted bundle, serve it with Access-Control-Allow-Origin and add crossorigin to the <script> tag. Otherwise expect a pile of useless Script error. cards — and filter them in beforeSend in the meantime.

4. Framework-level handlers

React error boundary:

// src/ErrorBoundary.tsx
import { Component, type ErrorInfo, type ReactNode } from 'react';
import bugboard from './lib/bugboard';

export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
    state = { hasError: false };

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    componentDidCatch(error: Error, info: ErrorInfo) {
        bugboard.criticalHigh(`React error: ${error.message}`, error, ['react']);
        // componentStack is the useful part; append it to the description yourself
        // if you want it: `${error.stack}\n\n${info.componentStack}`
    }

    render() {
        return this.state.hasError ? <p>Something went wrong</p> : this.props.children;
    }
}

Vue:

const app = createApp(App);

app.config.errorHandler = (err, instance, info) => {
    bugboard.critical(`Vue error: ${info}`, err, ['vue']);
};

Using info — Vue's lifecycle hook name, a small fixed set — in the title rather than the error message keeps cards well grouped.

Svelte: use the window handlers above; Svelte has no component-level error hook.

5. Report from your own code

import bugboard from '@/lib/bugboard';

try {
    await payments.charge(order);
} catch (err) {
    bugboard.criticalHigh('Payment failed', err, ['payments', 'checkout']);
}

Delivery in the browser

The SDK listens for pagehide — the last reliable moment before a page goes away, and unlike unload it is respected by the bfcache and by mobile Safari. It fires the queued requests with keepalive: true, so the browser completes them after the page is gone.

keepalive requests are capped by the browser (64 KB total across in-flight keepalive requests), so this is best-effort for a large backlog rather than a guarantee. It is the right behaviour for the normal case of a handful of queued reports. Before a deliberate navigation you control, await bugboard.flush() first.

Note the hook only registers when document is defined — in a non-DOM worker context there is no automatic flush, so call flush() yourself.

Tuning for the browser

A page that errors inside a render loop can generate hundreds of reports:

createClient({
    apiKey: import.meta.env.VITE_BUGBOARD_API_KEY,
    maxQueueSize: 30, // bound memory; overflow drops the newest
    sampleRate: 0.5, // if you have real volume
});

Because dedup is server-side, sampling and dedup interact usefully: at sampleRate: 0.1 a bug that happens 1000 times still reliably produces its card — you just see an occurrence count of ~100. For a bug that happens twice, there's a good chance you see nothing. So sample when the problem is volume from known-noisy paths, not to save quota generally.

Filtering browser noise

beforeSend receives the payload about to be sent and returns it, or null to drop it:

createClient({
    apiKey: import.meta.env.VITE_BUGBOARD_API_KEY,
    beforeSend: (payload) => {
        // Drop browser noise you can't act on
        if (payload.title.includes('Script error.')) return null;
        if (payload.title.includes('ResizeObserver loop')) return null;

        // Scrub emails and bearer tokens out of descriptions
        payload.description = payload.description
            ?.replace(/[\w.+-]+@[\w-]+\.[\w.]+/g, '[email]')
            .replace(/Bearer\s+[\w-]+\.[\w-]+\.[\w-]+/g, 'Bearer [redacted]');

        return payload;
    },
});

ResizeObserver loop completed with undelivered notifications is a benign browser warning that fires constantly on some layouts — filtering it is almost always right.

Keep the hook fast and total: it runs synchronously inside the reporting call. If it throws, the report is lost and the error goes to the debug channel — your app is unaffected.

Want the payload unreadable in the browser's network tab? Set an encryption key and every body is sealed before it leaves the page. See Encrypting sensitive reports.

Next steps