@browsonic/sdk
v3.12.0
Published
Privacy-first browser RUM and error tracking SDK. Lightweight (~17 KB core / ~25 KB full, gzipped), framework-agnostic, no PII by default.
Maintainers
Readme
@browsonic/sdk
Privacy-first browser error tracking + RUM. The framework-agnostic core of the Browsonic SDK — the one piece of code that runs inside your app, so it is fail-safe, bundle-obsessive, and masked-by-default.
@browsonic/sdk captures what breaks in the browser and how users got there:
- Errors — uncaught exceptions and unhandled promise rejections, deduplicated and fingerprinted.
- Telemetry breadcrumbs — console,
fetch/XHR (with 4xx/5xx capture), and navigation, replayed as the timeline leading up to an error. - Page views — route-change page-view events that power the dashboard's App Atlas route map.
- Session Replay — opt-in rrweb recording of the DOM, masked by default.
- Web Vitals — opt-in LCP / FCP / CLS / TTFB.
Errors in the SDK never crash the host app: every public path is wrapped and a circuit breaker pauses collection after repeated internal failures.
Install
npm install @browsonic/sdkThe SDK is npm-only (ESM + CJS + types) — no UMD, no CDN, no script tag. Your bundler emits the right format. Node 20+ for build/test.
Quickstart
import { getBrowsonic, webVitalsPlugin } from '@browsonic/sdk';
import { sessionReplayPlugin } from '@browsonic/sdk/replay';
const sdk = getBrowsonic();
// Register opt-in plugins BEFORE init(). Once the SDK is initialized,
// register() is ignored (the SDK logs a warning).
sdk.register(webVitalsPlugin());
sdk.register(sessionReplayPlugin());
sdk.init({
apiEndpoint: 'https://api.browsonic.com',
appKey: 'web',
apiKey: 'pk_live_...', // publishable key — safe to ship in the browser
clientVersion: '1.0.0', // must equal the release your source maps upload under
});Register plugins before
init().webVitalsPlugin()andsessionReplayPlugin()are opt-in; they only collect if they are registered before the SDK boots. Aregister()call afterinit()is ignored (the SDK logs a warning).
Keys
appKey identifies your application (sent as X-APP-KEY). apiKey authenticates the batch (sent as X-API-KEY) — use a publishable pk_live_... key: it is ingest-only and safe in client code, the same way a Sentry DSN is. Never put a secret sk_live_... key in browser code; those grant read access and must stay server-side. The SDK warns in the console if it sees an sk_ key in a browser.
Defaults & opt-in features
Everything you need for error tracking + RUM is on out of the box; the privacy-sensitive and advanced knobs are off until you opt in.
On by default
| Feature | Default | What it does | Notes |
| ------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Error tracking | On | Uncaught exceptions + unhandled rejections, deduped & fingerprinted | The SDK's reason to exist — not configurable off. |
| Telemetry breadcrumbs | On | Console timeline leading up to an error | includeTelemetry (default true); verbs preserved (console.debug stays debug). |
| captureXHR | On | fetch + XHR timing with 4xx/5xx capture | Headers allow/block-listed, bodies redacted before egress. |
| networkTelemetry | On | Network requests recorded as breadcrumbs | Pairs with captureXHR. |
| trackNavigation | On | Route-change navigation breadcrumbs | — |
| trackPageViews | On when apiKey set | Page-view events that power the dashboard's App Atlas route map | Needs apiKey to authenticate; set trackPageViews: false if you have no key. |
| normalizePageViewRoutes | On | Collapses page-view URLs to a stable route key at source (/users/123 → /users/:id; drops query + fragment; opaque/base64 blobs → :id) | Lowers Atlas route cardinality and keeps query/hash PII off the wire. Set false for raw (still-redacted) URLs. |
| respectGPC | On | Honors Global Privacy Control — demotes the visitor ID to an ephemeral per-session UUID | — |
Opt-in (off by default)
| Feature | Default | What it does | Notes |
| ----------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| captureStorage.local / captureStorage.session | Off | Snapshots localStorage / sessionStorage alongside an error | Privacy — enable with consent. |
| captureCookieValues | Off | Includes cookie values (names are always visible) | Privacy — enable with consent. |
| trackVisitor | Off | Click / input visitor interaction breadcrumbs | Privacy — enable with consent. |
| compress | Off | Gzips the ingest body via CompressionStream | Advanced — needs backend Content-Encoding: gzip support. |
| persistQueue | Off | Persists the offline event queue to localStorage across reloads | Advanced. |
| internalDiagnostics | Off | POSTs the SDK's OWN metrics (init/flush latency, queue depth, dropped-event counts) to /v1/diagnostics — populates the dashboard's SDK Health / Diagnostics pages | Advanced — adds ~1 POST/min per instance. Enable with internalDiagnostics: true (tune cadence via internalDiagnosticsIntervalMs). |
| captureAsyncStack | Off | Longer async stack traces ('manual' / 'global') | Advanced. |
| webVitalsPlugin() | Opt-in | LCP / FCP / CLS / TTFB | register() before init(). |
| sessionReplayPlugin() | Opt-in | rrweb DOM recording, masked by default | import { sessionReplayPlugin } from '@browsonic/sdk/replay'; register() before init(). |
| setUser / setContext / addMetadata / setExtra / addBreadcrumb | Host-called | Attach user, context, metadata, and manual breadcrumbs to events | Enrichment you call after init(); nothing is added unless you call them. |
Page-view route controls
The App Atlas route map is fed by page-view URLs. normalizePageViewRoutes (on by default) already collapses ids/uuids/opaque segments and drops query + fragment at source. For app-specific shapes, four optional knobs let you control routes without a backend change:
sdk.init({
// …
// Rewrite app-specific shapes the generic normalizer can't template:
pageViewRoutePatterns: [{ test: /\/products\/[^/?#]+/, replace: '/products/:slug' }],
// Final transform / drop hook (return null to drop the page view):
beforeSendPageView: (route) => (route.includes('/admin') ? null : route),
// Hash-router apps: promote `#/users/123` into the route:
hashRouting: true,
// Sample NON-initial page views on high-traffic SPAs (initial is always sent):
pageViewSampleRate: 0.5,
});Your redactPatterns also apply to the page-view URL + referrer (not just error events).
Explicit route templates (framework routers)
The generic normalizer can't template a purely-alphabetic slug (/products/winter-boots looks like a static route) — only your router knows it's /products/:slug. Feed that template with trackPageView(route); the backend App Atlas prefers it over URL normalization.
To avoid the organic history-driven view double-counting, set manualPageViews: true (the collector stays live for trackPageView, but stops auto-firing on load/history) and drive page views from your router:
sdk.init({ /* … */ apiEndpoint: '…', apiKey: '…', manualPageViews: true });
const bs = getBrowsonic();| Framework | Wire it |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| React Router | in a useEffect on useMatches(): bs.trackPageView(matches.at(-1)?.pathname ?? location.pathname) (or the route's path) |
| Vue Router | router.afterEach((to) => bs.trackPageView(to.matched.at(-1)?.path)) |
| Angular | on NavigationEnd, walk to the deepest ActivatedRoute and send route.routeConfig?.path |
| Remix | bs.trackPageView(useMatches().at(-1)?.id) (the route id is the file-route template) |
| Plain / other | call bs.trackPageView('/orders/:id') yourself on each navigation |
trackPageView(route) is also a no-op-safe global: import { trackPageView } from '@browsonic/sdk'.
The ./replay subpath keeps rrweb out of the main bundle
Session Replay ships behind the @browsonic/sdk/replay subpath on purpose. The rrweb recorder is an order of magnitude larger than the core SDK, so it is only pulled into your bundle when you actually import from /replay. Apps that never record replays never pay for the recorder.
Recording volume defaults. A recorded session auto-stops after 2 min of no user interaction (idleTimeoutMs) or a hard 10 min cap per session (maxDurationMs) — killing the abandoned/marathon tail nobody watches. Mouse/scroll/input are throttled and every DOM snapshot is slimmed (slimDOMOptions), so recordings are much smaller with no visible playback loss. Tune any of these on sessionReplayPlugin({ idleTimeoutMs, maxDurationMs, flushIntervalMs, checkoutEveryNms }); the fraction of sessions recorded (sampleRate) is set per app in the dashboard.
Privacy
Browsonic is masked by default. Session Replay masks all text and input values unless you explicitly opt elements back in. Network headers pass an allowlist/blocklist and bodies are scrubbed for JWTs, emails, credit-card numbers, and OAuth secrets before they leave the browser. Passwords, tokens, API keys, and input values are redacted at the collector layer. Cookie values and localStorage/sessionStorage capture are off by default. Under Global Privacy Control (GPC) the visitor ID falls back to an ephemeral per-session UUID.
Full contract, GDPR/CCPA/HIPAA posture, and the redaction rules: PRIVACY.md. Config reference and wire format: INTEGRATION.md.
Framework adapters
Using a framework? Install the matching adapter alongside @browsonic/sdk — it catches the render-time errors that window.onerror cannot see and adds framework-native navigation breadcrumbs.
| Framework | Package |
| --------- | ---------------------------------------------------------------------------------------------- |
| React | @browsonic/react |
| Vue 3 | @browsonic/vue |
| Svelte | @browsonic/svelte |
| Angular | @browsonic/angular |
| Astro | @browsonic/astro |
| Next.js | @browsonic/nextjs |
| Remix | @browsonic/remix |
Source maps
Minified production stacks are de-minified server-side from source maps you upload at build time. Upload them with @browsonic/build-tools (Vite / Webpack / Rollup / esbuild plugins) or the @browsonic/cli.
The one rule that makes symbolication work: the release you upload maps under must equal the clientVersion you pass to init(). That pairing is how the backend matches a minified frame to its map. Emit hidden source maps (sourcemap: 'hidden' for Vite/Rollup, devtool: 'hidden-source-map' for Webpack) so the .map files are written to disk for upload but never advertised to end users.
