@browsonic/react
v1.4.3
Published
React adapter for @browsonic/sdk — error boundary, hooks, HOC, React Router instrumentation. Apache-2.0.
Downloads
680
Maintainers
Readme
@browsonic/react
React adapter for @browsonic/sdk. Catches the render errors that
window.onerrorcannot see.
An Error Boundary anywhere above the throw catches a render-time exception and renders a fallback tree — the error never reaches window, so the global handlers a plain @browsonic/sdk install relies on never see it. This adapter wires React's Error Boundary primitive to Browsonic so those errors get reported, with the React component stack attached. (With no boundary at all above the throw, React 19 does re-report the error to window via reportError — but the component stack is lost either way, and the tree still unmounts.)
npm install @browsonic/sdk @browsonic/reactimport { getBrowsonic } from '@browsonic/sdk';
import { BrowsonicErrorBoundary } from '@browsonic/react';
// Use getBrowsonic(), not `new Browsonic()`: it publishes the singleton on
// `window.Browsonic`, which is where this adapter's hooks — and a
// <BrowsonicErrorBoundary> with no `sdk` prop — look it up. A hand-constructed
// instance is invisible to them and they silently report nothing.
const sdk = getBrowsonic();
sdk.init({
// Origin only — the SDK appends `/v1/events` itself.
apiEndpoint: 'https://your-ingest.example.com',
appKey: 'your-app-key',
// Publishable key, safe in the browser. NOT optional in practice: page-view
// tracking is on by default and `init()` returns false with
// "apiKey is required for page view tracking" unless you also set
// `trackPageViews: false`.
apiKey: 'pk_live_...',
});
function App() {
return (
<BrowsonicErrorBoundary
sdk={sdk}
fallback={(error, reset) => (
<div role="alert">
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)}
>
<YourApp />
</BrowsonicErrorBoundary>
);
}What this adapter ships
<BrowsonicErrorBoundary>— render-time error capture; the requiredfallbackmay be a node or(error, reset) => node, andreset()clears the error state and re-renders the children. An error thrown inside a<Suspense>subtree below it (alazy()chunk that renders and throws) reaches it too.useBrowsonic()— singleton instance hook (resolved once at mount, stable for the lifetime of the component). ReturnsBrowsonic | null—nullwhenever the SDK is unreachable, so guard before calling into it.useUser(user | null)— sets the user context on mount and again whenever the user's fields change (value-compared, not reference-compared). It does not clear on unmount — that would race with a sibling remount — so passnullwhen you want it cleared.useCaptureError()— stable callback for try/catch sites and event handlers.withBrowsonic(Component)— HOC that injectssdkas a prop, for class components that cannot consume hooks.useTrackPageView(template, name?, navKey?)+routeTemplateFromMatches(matches)— React Router v6 / v7 page-view instrumentation with the authoritative route template (shipped in 1.3.0;navKeysince 1.4.0). See Router instrumentation.
import { BrowsonicErrorBoundary, useBrowsonic, useUser, useCaptureError } from '@browsonic/react';
function App({ currentUser }) {
// Stamps the user context onto events from here on. Unmounting does NOT
// clear it — pass `null` on logout.
useUser(currentUser ?? null);
return (
<BrowsonicErrorBoundary fallback={<ErrorScreen />}>
<Checkout />
</BrowsonicErrorBoundary>
);
}
function Checkout() {
const captureError = useCaptureError();
const sdk = useBrowsonic();
const buy = async () => {
try {
await api.buy();
} catch (err) {
// Event handlers don't reach Error Boundaries — capture manually.
captureError(err as Error);
}
};
return <button onClick={buy}>Buy</button>;
}Router instrumentation (React Router v6 / v7)
The Atlas backend prefers an authoritative parameterized route template (/users/:id) over URL normalization — the regex normalizer fundamentally cannot recover templates for alphabetic slugs. React Router knows the template; routeTemplateFromMatches + useTrackPageView (in src/atlas.ts) hand it to the SDK without this package depending on react-router (structural shapes only, no new peer dependency).
// React Router 6+: compute matches once at the layout root.
import { matchRoutes, useLocation } from 'react-router-dom';
import { routeTemplateFromMatches, useTrackPageView } from '@browsonic/react';
function AtlasPageViews({ routes }: { routes: RouteObject[] }) {
const location = useLocation();
const matches = matchRoutes(routes, location);
// location.key changes per NAVIGATION — without it, /products/1 →
// /products/2 (same /products/:id template) would be deduped away.
useTrackPageView(routeTemplateFromMatches(matches), undefined, location.key);
return null;
}routeTemplateFromMatches(matches)joins matched route paths into the parameterized template (/users/:id/orders); layout wrappers and index routes are skipped,:paramsand*splats pass through. Returns''when nothing matched — Atlas then falls back to URL normalization.useTrackPageView(template, name?, navKey?)firestrackPageViewwhenever the (template, name, navigation) triple changes; re-renders at the same route send nothing. The optionalnamefeeds the SDK ≥ 3.13 screen-name channel. PassnavKey(react-router'slocation.keyis ideal) so consecutive same-template navigations still count. No-op when the SDK singleton is unreachable; never throws.- Pairing with the SDK: either init with
manualPageViews: true, or on SDK ≥ 3.14 just init withatlas: true— the first templated page view takes the channel over from organic URL tracking automatically, with no double counting.
This package is versioned independently of @browsonic/sdk — it is on 1.x while the SDK is on 3.x, so there is no @browsonic/react release that matches an SDK version number. Widening the @browsonic/sdk peer range ships here as a patch release; see CHANGELOG.md.
Compatibility
| Surface | Versions |
| ----------------- | ---------- |
| React | 18.x, 19.x |
| @browsonic/sdk | ≥ 3.12.0 |
| Node (build/test) | ≥ 20 |
The authoritative ranges live in package.json peerDependencies.
Privacy
The adapter does not collect data on its own — it forwards to the SDK, which carries Browsonic's privacy-first defaults. See PRIVACY.md in the SDK repo.
On a render error caught by the boundary — and only then — the adapter adds two things of its own, both scoped to that single capture via the SDK's withScope so they do not stick to later events:
- a
reactcontext bucket: React'sversionstring, plus the component stack truncated to 1024 chars; - that same truncated component stack again as
componentStackevent metadata.
The component stack is React's own string — component names and source locations — and never includes prop or state values.
