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 withAccess-Control-Allow-Originand addcrossoriginto the<script>tag. Otherwise expect a pile of uselessScript error.cards — and filter them inbeforeSendin 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
- JavaScript SDK — the base guide and the full method surface.
- Next.js · Nuxt · SvelteKit — if your SPA also has a server.
- API Reference — every config option and the HTTP contract.