BugBoardDocs

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

npm i bugboard
BUGBOARD_KEY_ID=bbk_xxxxxxxx
BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx

2. The server client

// 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

// 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.

4. The client ErrorBoundary

// 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,
});
// 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 Responses — 404s and redirects your own loaders threw deliberately, which are control flow, not bugs.

5. Loaders and actions

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.

Next steps

  • JavaScript SDK — the base guide and the full method surface.
  • Vite SPA — global browser handlers and the React error boundary pattern in more depth.
  • API Reference — every config option and the HTTP contract.