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.