Skip to content
bucker

@bucker/browser

Browser capture. Named exports only, integrations importable from their own entry points, no side effects at import time — the package is tree-shakable by construction and a test enforces its byte budget in CI.

Distribution note. The package is build-ready, not yet published. It carries a publishConfig (pointing at dist/, split per subpath) alongside a source-pointing top-level main/types/exports — the split matters because several other workspace packages import the SDKs by source with no build step, while a published consumer cannot compile ./src and needs the compiled dist/. pnpm build / prepack run scripts/build-package.mjs, which refuses to finish if the built output does not match what publishConfig.exports promises. Inside the monorepo it resolves through workspace:*; create-bucker already emits the install command for the published name. What remains is an actual npm publish, a human decision covered in sdks/PUBLISHING.md — not code readiness.

Init

import { init } from '@bucker/browser'

init({
  dsn: import.meta.env.VITE_BUCKER_DSN,
  environment: import.meta.env.MODE,
  release: __APP_VERSION__,
})

Call it once, as early as your entry allows. init() returns the Client and also installs it as the current client, so the module-level helpers work anywhere after.

Options

Everything on ClientOptions, plus the browser-specific ones:

Option Default Meaning
dsn — Required
environment, release, dist, serverName, platform — Stamped on every event
sampleRate 1 Applied per event before the envelope is built; drops are accounted for in the client report
maxBreadcrumbs SDK default Ring-buffer size
maxValueLength SDK default Caps any single captured string. Bounds sizes, never semantics
beforeSend — Last chance to mutate or drop an event; a drop is accounted for
beforeBreadcrumb — Same, per breadcrumb
inAppInclude / inAppExclude — Substring matchers forcing frames in or out of in_app; include wins
integrations defaultIntegrations() Replaces the default set entirely
autoUnloadFlush true Set false to skip the pagehide/visibilitychange beacon flush
maxBufferBytes SDK default Transport queue ceiling
random, now Math.random, Date.now Testing seams for deterministic sampling and timestamps

Default integrations

defaultIntegrations() returns [globalHandlersIntegration(), breadcrumbsIntegration()].

globalHandlersIntegration — window.onerror and unhandledrejection. Both are recorded with handled: false and a distinct mechanism type, because "the app crashed" and "someone caught this and reported it" group differently and the distinction cannot be recovered later. An onerror that fires with no Error object (cross-origin scripts) still produces an event built from filename/line/column — a location-only crash beats no crash.

breadcrumbsIntegration — console, fetch, XMLHttpRequest, history navigation and DOM clicks. Every source is a monkey patch and every patch is reversible: the integration keeps the original and restores it on teardown.

DOM click crumbs carry a micro-DOM chain (microDomChain, exported from @bucker/browser/microdom) — a compact ancestor description rather than a full selector dump.

Transport

Two send paths share one queue:

  • normal — fetch with keepalive;
  • unloading — navigator.sendBeacon, the only delivery a browser guarantees after the page is gone. installUnloadFlush flips a flag on pagehide / visibilitychange; the transport reads it rather than swapping implementations, because there is no time to negotiate anything at that point.

sendBeacon caps a payload at 64 KB (BEACON_MAX_BYTES), so splitForBeacon splits oversized queues rather than losing them.

Envelopes ride two priority lanes. Errors take the critical lane and flush first; logs, spans, replay and attachments take bulk. A bulk item never shares an envelope with an error item — one oversized attachment causing a 413 that also kills the error event is a footgun we deliberately do not inherit.

Rate limits (429 + Retry-After / X-Sentry-Rate-Limits) are honoured per category, and everything dropped is reported back as a client_report with a reason (ratelimit_backoff, queue_overflow, sample_rate, before_send, …).

Capturing by hand

import { captureException, captureMessage, addBreadcrumb, setUser, setTag,
         setContext, setExtra, flush, close } from '@bucker/browser'

captureException(error, { mechanism: { type: 'checkout', handled: true } })
captureMessage('cart reconciliation skipped', 'warning')
addBreadcrumb({ category: 'checkout', message: 'coupon applied', level: 'info' })
setUser({ id: 'u_123' })          // pass null to clear
setTag('tier', 'pro')
await flush(2000)                 // true when the queues drained in time

Scope is exported for advanced use; getCurrentScope() returns the active one.

Session replay

Its own entry point, @bucker/browser/replay, so an application that never calls replayIntegration() does not pay for it — the root bundle stays under the CI-enforced byte budget:

import { init, defaultIntegrations } from '@bucker/browser'
import { replayIntegration } from '@bucker/browser/replay'

init({
  dsn,
  integrations: [...defaultIntegrations(), replayIntegration()],
})

It is error-triggered, not always-on: a ring buffer holds the last 30 seconds / 200 events of interaction (windowMs, maxEvents) and nothing leaves the page until an error is captured, at which point the slice is sent on its own bulk-lane envelope so a 413 on the replay bytes can never take the error event down with it. Segments are rate-limited to one per minSegmentIntervalMs (default 5000) however many errors fire in that window, and sampleRate (default 1) thins which errors get a segment at all.

Option Default Meaning
maskAllText true Mask all page text and input values. false still never captures credential-shaped fields
maxTextLength 48 Cap on captured text before it is clipped
sampleRate 1 Share of errors that get a replay segment
minSegmentIntervalMs 5000 Minimum gap between uploaded segments
windowMs / maxEvents 30s / 200 Ring buffer bounds — how far back and how much
console / fetch / xhr / history / dom true Per-source capture toggles
rageClickThreshold / rageClickWindowMs 3 / 1000 Rage-click detection
maxOverheadRatio 0.02 Recorder disables itself past this share of wall time

Masking beyond maskAllText cannot be turned off: type="password", autocomplete credential tokens, and name/id/aria-label patterns matching password/token/cvv/ssn/… shapes are always redacted. The explicit opt-out is the data-bucker-mask attribute (also data-private, data-sentry-mask), which hides a whole subtree, not just the element carrying it. See replay-privacy.md for the full capture-to-storage pipeline, including what reaches the server and where server-side scrubbing happens.

Not built yet

The breadcrumb DSL's richer compaction and offline event caching are not implemented. Local-variable capture is Node-only in design and not implemented there either — see sdk-node.md.