# Remix / React Router

Remix gives you one server entry point for every error the framework catches, and an
`ErrorBoundary` for what happens in the browser. Two client modules, one hook each.

## 1. Install

```bash
npm i bugboard
```

```dotenv
BUGBOARD_KEY_ID=bbk_xxxxxxxx
BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx
```

## 2. The server client

```ts
// app/lib/bugboard.server.ts
import { createClient } from 'bugboard';

export default createClient({
    keyId: process.env.BUGBOARD_KEY_ID, // bbk_…
    signingSecret: process.env.BUGBOARD_SIGNING_SECRET, // bb_sec_…
    environment: process.env.NODE_ENV,
});
```

The `.server.ts` suffix is enforced by the compiler — importing it from client code is a build
error, which is exactly the guard you want around a signing secret.

## 3. `handleError` in `entry.server`

```ts
// app/entry.server.tsx
import bugboard from './lib/bugboard.server';

export function handleError(error: unknown, { request }: { request: Request }) {
    if (request.signal.aborted) return; // user navigated away; not a bug

    bugboard.criticalHigh(
        `Server error: ${new URL(request.url).pathname}`,
        error,
        ['remix'],
    );
    void bugboard.flush();
}
```

The `request.signal.aborted` check filters out cancelled navigations, which would otherwise
show up as a steady stream of phantom errors on your board.

If your paths contain ids (`/orders/91847`), prefer a stable title over the raw pathname —
otherwise every order produces its own card. See
**[Deduplication](/docs/api-reference#deduplication-important-for-sdk-design)**.

## 4. The client `ErrorBoundary`

```ts
// app/lib/bugboard.client.ts
import { createClient } from 'bugboard';

export default createClient({
    apiKey: import.meta.env.VITE_BUGBOARD_API_KEY, // bb_pub_…
    environment: import.meta.env.MODE,
});
```

```ts
// app/root.tsx
import { useRouteError, isRouteErrorResponse } from '@remix-run/react';
import { useEffect } from 'react';
import bugboard from '~/lib/bugboard.client';

export function ErrorBoundary() {
    const error = useRouteError();

    useEffect(() => {
        if (!isRouteErrorResponse(error)) {
            bugboard.critical('Client route error', error, ['remix', 'client']);
        }
    }, [error]);

    return <p>Something went wrong</p>;
}
```

The `isRouteErrorResponse` guard skips thrown `Response`s — 404s and redirects your own loaders
threw deliberately, which are control flow, not bugs.

## 5. Loaders and actions

```ts
export async function action({ request }: ActionFunctionArgs) {
    try {
        return await processCheckout(await request.formData());
    } catch (err) {
        bugboard.criticalHigh('Checkout action failed', err, ['checkout']);
        throw json({ error: 'Checkout failed' }, { status: 500 });
    } finally {
        await bugboard.flush();
    }
}
```

The `finally` matters if you deploy to Vercel, Cloudflare or any other serverless target —
nothing else drains the queue there. On a long-running Node server it is nearly free, so
leaving it in keeps the code portable. See
**[Serverless & edge](/docs/frameworks/serverless)**.

## Next steps

- **[JavaScript SDK](/docs/sdks/javascript)** — the base guide and the full method surface.
- **[Vite SPA](/docs/frameworks/vite-spa)** — global browser handlers and the React error
  boundary pattern in more depth.
- **[API Reference](/docs/api-reference)** — every config option and the HTTP contract.
