@browsonic/sdk
v3.22.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 / FID / INP.
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/sdkPublished as ESM + CJS + types, with five subpath entries — . (default plugin set), ./core (no default plugins), ./widget, ./replay, ./atlas; package.json#exports is the authoritative list. Your bundler picks the format. Node 20+ for build/test.
There is no CDN. There is a script-tag path: since 3.16.0 the tarball also ships a self-hosted browser IIFE, dist/browser/browsonic.iife.es2015.js (built by scripts/build-iife.mjs), for legacy toolchains — webpack 4 / terser 4, the Angular 8 era — that cannot parse the ES2020 dist. You serve that one file yourself and it auto-initializes from a window.Browsonic.config block. Consumption pattern: docs/examples/angular-legacy.
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 console-warns if it sees an sk_ key in any client runtime (window or Web Worker); server-side usage is not flagged.
Defaults & opt-in features
Everything you need for error tracking + RUM is on out of the box — but read the sampleRate row: errors ship from every session, RUM only from a sampled 10%. 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 | XMLHttpRequest timing with 4xx/5xx capture | fetch is captured unconditionally (separate plugin) — captureXHR: false drops only the XHR half. Headers allow/block-listed, bodies redacted before egress. |
| networkTelemetry | On | Network requests recorded as breadcrumbs | Gates the breadcrumb write for both fetch and XHR; error capture is unaffected. |
| 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 — drops the visitor ID to strategy none: a fresh, unlinkable UUID per call, stored nowhere | A hasConsented() gate returning false (or throwing) does the same. Without GPC the default visitorIdStrategy is session (sessionStorage, cleared on tab close) — no persistent cookie is written. |
| sampleRate | 0.1 | Head-based session sampling. Errors and fatals are always sent; non-error events (page views, Web Vitals, breadcrumb-only events) are dropped for sessions that lose the roll | So ~10% of sessions report RUM out of the box. Rolled once per session at boot. A per-app rate set in the dashboard (cached, and pushed live via the X-Browsonic-Sample-Rate response header) overrides the value you pass, floored by minSampleRate. |
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 / FID / INP | 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).
App Atlas: one switch (SDK ≥ 3.14)
sdk.init({ /* … */ apiEndpoint: '…', apiKey: '…', atlas: true });atlas: true is everything Atlas needs from the core: page views, screen titles and nav triggers flow with their defaults (explicit opt-outs like captureDocumentTitle: false still win), and route templates hand over automatically — organic URL-based tracking runs from the first paint, and the moment a framework adapter sends its first trackPageView(template) (Angular/Vue installRouterInstrumentation, React useTrackPageView, Next.js installPagesRouterAtlas), the adapter owns page views. No double counting, no flag pairing. With atlas: true, the Angular/Vue router instrumentation turns its template tracking on by itself.
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.
On SDK ≥ 3.14 prefer atlas: true (above) — the hand-off makes the flag below unnecessary. The legacy contract still works: 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 }). sampleRate is a plugin option too (0..1, default 1), but it is not the last word: a rate set per app in the dashboard overrides it, and both are capped by the plan's replay ceiling — a plan without replay records nothing regardless of what you pass.
Privacy
Browsonic is masked by default. Session Replay masks all text (rrweb maskTextSelector: '*') and all input values, and blocks media — but masking is all-or-nothing: maskAllText: false / maskAllInputs: false lift it wholesale (password inputs stay masked either way), and there is no per-element opt-back-in selector. Network headers pass an allowlist/blocklist and bodies are scrubbed for JWTs, emails, credit-card-shaped digit runs, and long opaque secrets before they leave the browser. Passwords, tokens, API keys, and cookie values whose names match redactKeys are redacted at the collector layer; visitor input breadcrumbs record a value's length and shape only, never the value. Cookie values and localStorage/sessionStorage capture are off by default. Under Global Privacy Control (GPC) the visitor ID becomes a fresh, unlinkable UUID that is stored nowhere.
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 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.
