# 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

```bash
npm i bugboard
```

## 2. The client module

```ts
// 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
});
```

```dotenv
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:

```ts
// 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`](#filtering-browser-noise)** in the meantime.

## 4. Framework-level handlers

**React error boundary:**

```ts
// 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:**

```ts
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

```ts
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:

```ts
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:

```ts
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](/docs/security/encryption)**.

## Next steps

- **[JavaScript SDK](/docs/sdks/javascript)** — the base guide and the full method surface.
- **[Next.js](/docs/frameworks/nextjs)** · **[Nuxt](/docs/frameworks/nuxt)** ·
  **[SvelteKit](/docs/frameworks/sveltekit)** — if your SPA also has a server.
- **[API Reference](/docs/api-reference)** — every config option and the HTTP contract.
