@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 atdist/, split per subpath) alongside a source-pointing top-levelmain/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./srcand needs the compileddist/.pnpm build/prepackrunscripts/build-package.mjs, which refuses to finish if the built output does not match whatpublishConfig.exportspromises. Inside the monorepo it resolves throughworkspace:*;create-buckeralready emits the install command for the published name. What remains is an actualnpm publish, a human decision covered insdks/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 —
fetchwithkeepalive; - unloading —
navigator.sendBeacon, the only delivery a browser guarantees after the page is gone.installUnloadFlushflips a flag onpagehide/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.