# React

This page covers wiring `@newinstance/bugwatch` into a client-rendered React app (Vite, CRA, React Router, TanStack Router). If you are on Next.js, use `@newinstance/bugwatch/next/client` instead.

Fetching data with TanStack Query? Add `@newinstance/bugwatch-tanstack` on top of this and every failed query and mutation is reported automatically, with a breadcrumb trail and a span per request. See [TanStack Query](/tanstack-query).

```sh
npm install @newinstance/bugwatch
```

## Read this first: the browser security model

Everything you ship to the browser is public. Your `projectKey` (`<keyId>:<secret>`) is a secret and must never appear in React code, in a `VITE_` env var, or in any bundle. Anyone who reads it can ingest arbitrary events under your account.

The browser SDK authenticates with a short-lived **session token** instead:

1. Your backend holds the `projectKey` and exposes a mint endpoint, for example `GET /bugwatch/session`, returning `{ token, expiresAt }`.
2. The browser SDK is configured with `sessionUrl` pointing at that endpoint.
3. On first send the SDK fetches a token, caches it, refreshes it shortly before expiry and again on a `401`, and posts events to `https://api.newinstance.cloud/api/v1/bugwatch/ingest/browser` with the `x-bugwatch-session` header.

Your secret never leaves your server. Full details are in the SDK README under [Browser Ingest](/browser-ingest) page.

```tsx
// WRONG. Never do this in a React app.
const client = createClient({ projectKey: "sk_live_abc123:my-secret" });
```

If a key has already shipped to a browser, rotate it in the dashboard under Project settings, API keys.

## Initialize once at the app entry

Call `initBrowser` in `src/main.tsx`, at module scope, before you render. It creates the client, installs the `window` `error` and `unhandledrejection` handlers, and is safe to import anywhere.

```tsx
// src/main.tsx
import { initBrowser } from "@newinstance/bugwatch";

export const bugwatch = initBrowser({
	sessionUrl: "/bugwatch/session",
	release: import.meta.env.VITE_RELEASE,
	environment: import.meta.env.MODE,
});
```

What `initBrowser` guarantees:

- **Never throws.** Bad config or an unreachable session endpoint degrades to a working no-op client plus one `console.warn`. A crash reporter must not crash your app.
- **Idempotent.** The client lives on a global symbol. A second call (hot reload, a stray import, two bundles on one page) returns the first client and ignores the new options.
- **SSR safe.** With no `window` it returns the existing client or a no-op, so prerendering or a test runner without a DOM will not blow up.
- **Relative `sessionUrl` is resolved** against `window.location.href` before the client is created, so `/bugwatch/session` works from any route depth.

Anywhere else in the app, read the singleton rather than re-initializing:

```tsx
import { getBrowserClient } from "@newinstance/bugwatch";

const client = getBrowserClient(); // BugWatchClient | undefined before init
```

## Catch render errors with BugWatchErrorBoundary

`BugWatchErrorBoundary` is a class error boundary that catches errors thrown while rendering the subtree below it. Import it from the `/react` subpath and give it the client.

```tsx
import { BugWatchErrorBoundary } from "@newinstance/bugwatch/react";
```

Props: `client` (required), `fallback` (optional `ReactNode`), `children`.

On a caught error it calls `captureException(error, { level: 50, tags: { componentStack } })` and flushes immediately, so the event survives the user closing the tab. The React component stack is stored as the `componentStack` tag and **truncated to the first 1000 characters**, which is enough for the top frames of the tree. If the capture itself fails it is swallowed: the boundary never rethrows.

Rendering behaviour: once an error is caught, the boundary renders `fallback`. Without a `fallback` prop it renders `null`, so the crashed subtree simply disappears with no error surfaced to the user. Always pass a `fallback` for anything a user looks at.

The error state is sticky for the life of that boundary instance. To offer a retry, remount the boundary by changing its `key` (for example keying it on the route path), which is also why per-route boundaries are usually better than a single one at the root.

```tsx
// One boundary per route subtree, remounted on navigation.
<BugWatchErrorBoundary
	key={location.pathname}
	client={bugwatch}
	fallback={<RouteCrashed />}
>
	<Outlet />
</BugWatchErrorBoundary>
```

## Capture handled errors and messages

Anything you catch yourself will not reach a boundary, so capture it explicitly.

```tsx
import { getBrowserClient } from "@newinstance/bugwatch";

export function useCheckout() {
	return async function checkout(order: Order) {
		const client = getBrowserClient();
		try {
			await payFor(order);
			client?.captureMessage("checkout completed", 30, {
				tags: { orderId: order.id },
			});
		} catch (err) {
			client?.captureException(err, {
				level: 50, // 30 info, 40 warn, 50 error, 60 fatal
				tags: { orderId: order.id, step: "pay" },
			});
			throw err;
		}
	};
}
```

`captureException(err, hint?)` accepts `level`, `tags`, `user`, `traceId`, `spanId`, all optional, and returns the event ID string. `captureMessage(message, level?, hint?)` defaults to level 30.

## Identify the user after login

Call `setUser` on the browser client once authentication resolves. It applies to every later event. Pass `null` on logout so the next visitor is not attributed to the previous one.

```tsx
useEffect(() => {
	const client = getBrowserClient();
	if (!client) return;
	client.setUser(
		user ? { id: user.id, email: user.email, username: user.name } : null,
	);
}, [user]);
```

The global setter is correct here: a browser tab has exactly one user. (The concurrency warning in the README applies to servers, not to SPAs.)

## Breadcrumbs for UI events

Breadcrumbs are a bounded ring of the last 50 entries attached to every captured event, rendered as a timeline on the occurrence view. Record the handful of actions that explain a crash, not every render.

```tsx
getBrowserClient()?.addBreadcrumb({
	category: "ui",
	message: "clicked Place order",
	level: 30,
	data: { items: cart.length },
});
```

Fields: `category`, `type`, `level` (numeric), `message`, `data` (keep it small), `timestamp` (ms, defaults to now).

## What browser mode does not do

`sessionUrl` mode captures errors and logs. It does **not** export spans: the span exporter drops spans when there is no `projectKey`, which by design there never is in a browser. So no waterfall is produced from the page itself.

Distributed tracing still works in one direction. `wrapFetch` creates a span object locally, and even though that span is never exported it produces a valid `traceparent` header that is injected into your outbound request. Your traced backend picks it up and joins the same trace, so server-side errors and logs from that call are grouped under the trace the page started. An existing `traceparent` on the request is never overwritten.

```tsx
import { wrapFetch, getBrowserClient } from "@newinstance/bugwatch";

const client = getBrowserClient();
export const tracedFetch = client ? wrapFetch(client) : fetch;
```

## Complete example

```tsx
// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { initBrowser } from "@newinstance/bugwatch";
import { BugWatchErrorBoundary } from "@newinstance/bugwatch/react";
import { App } from "./App";

export const bugwatch = initBrowser({
	sessionUrl: "/bugwatch/session", // YOUR backend, no projectKey
	release: import.meta.env.VITE_RELEASE,
	environment: import.meta.env.MODE,
});

createRoot(document.getElementById("root")!).render(
	<StrictMode>
		<BugWatchErrorBoundary
			client={bugwatch}
			fallback={
				<main>
					<h1>Something went wrong.</h1>
					<button onClick={() => window.location.reload()}>Reload</button>
				</main>
			}
		>
			<App />
		</BugWatchErrorBoundary>
	</StrictMode>,
);
```

The matching backend route, which the SPA proxies or shares an origin with:

```tsx
// server: mints the short-lived browser token. Express one-liner.
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";

app.get(
	"/bugwatch/session",
	bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY! }),
);
```

On any other stack, call `mintBrowserSession({ projectKey })` from your own handler and return the resulting `{ token, expiresAt }` as JSON. The SDK fetches this URL with `GET`, so mount it on `GET`. In development, proxy `/bugwatch/session` to your API server (Vite `server.proxy`) so the relative URL resolves to a real endpoint.

## Troubleshooting

**`BugWatchResolutionError` in the console, or SDK exports are `undefined`.** Your `tsconfig.json` has `compilerOptions.paths` entries aliasing `@newinstance/bugwatch/*` to `.d.ts` files, and the bundler rewrote the runtime import to a type-declaration stub. Remove those `paths` entries. Since v0.2.0 subpath types resolve without aliases even on `"moduleResolution": "node"`, and `initBrowser` from the package root needs no subpath import at all. The SDK detects this state and prints the fix once.

**No events, no errors in the console.** Ad blockers and tracking-protection lists block requests to third-party ingest hosts, so `api.newinstance.cloud` may be dropped before it leaves the browser. Check the Network tab for a blocked or cancelled request. Serving the mint endpoint from your own origin does not help here since ingest goes direct; test with blocking disabled to confirm, and treat browser event volume as best effort.

**The session endpoint is missing or returns 401.** Nothing breaks: token refresh fails, the SDK logs `session fetch failed` under `debug: true`, and the pending batch is dropped. There is no retry storm and no thrown error. On a `401` from ingest the cached token is cleared and refetched on the next bounded retry, then the batch is abandoned. Confirm the endpoint is deployed, reachable from the page origin, mounted on `GET`, and that the server-side `projectKey` is valid.

**Events arrive without a user or tags.** `setUser`, `setTag`, and `addBreadcrumb` only affect captures made after them. Set the user in an effect that runs on auth state change, not after the capture you care about.

**Verify quickly.** Add `debug: true` to `initBrowser` and throw from a button handler. Failures print `[bugwatch]` diagnostics in the console; on success the event appears in the dashboard.
