@bugsee/browser
v0.1.0-beta.4
Published
Browser platform: fetch transport, IndexedDB storage, error handlers, stack-trace parser
Readme
@bugsee/browser
The browser platform build of the Bugsee JavaScript SDK: global error and unhandled-rejection capture, console and network capture, user-interaction breadcrumbs, session replay, and IndexedDB-backed persistence.
Part of the Bugsee JavaScript SDK (public beta, 0.1.0-beta.x).
This is an internal building block of the Bugsee SDK, not intended to be installed directly. Install @bugsee/bugsee instead. It selects this build automatically in the browser and adds performance monitoring (web vitals, navigation and interaction transactions), trace propagation and OpenTelemetry on top of it.
The framework adapters (@bugsee/react, @bugsee/vue, @bugsee/angular, @bugsee/svelte, @bugsee/solid, and the Next.js, Nuxt, Remix, SvelteKit and Astro adapters), @bugsee/webworker, @bugsee/webview and @bugsee/electron also build on it.
What it contains:
launch(appToken, options)andlaunchCore, the browser composition root (launchreturns aPromiseof the client).- Capture sources: window errors, unhandled rejections, interactions, navigation, input, system events and traces, DOM view hierarchy.
- The report dialog, and the wiring of the fetch transport and IndexedDB bundle storage.
App version and build
Reports carry the version and build of your app. The most direct way is to pass them to launch:
await launch('<app-token>', { appVersion: '2.4.1', appBuild: '381' });Bugsee can also pick them up at build time. The Bugsee Vite and webpack plugins inject the constants __BUGSEE_APP_VERSION__ and __BUGSEE_APP_BUILD__ automatically when you give appVersion and appBuild to the plugin, or when BUGSEE_APP_VERSION and BUGSEE_APP_BUILD are set in the build environment. You can also define those two constants yourself in your bundler, or set __BUGSEE_APP_VERSION__ and __BUGSEE_APP_BUILD__ as globals before launching (for example window.__BUGSEE_APP_VERSION__ = '2.4.1'). Options passed to launch always win over the build-time values.
If no version can be found, Bugsee logs a single warning to the console and reports the version as 0.0.0 and the build as 0. A page can also declare them in its HTML:
<meta name="bugsee:app-version" content="2.4.1">
<meta name="bugsee:app-build" content="381">When more than one source is present, the order is: launch options, then build-time constants, then meta tags.
Links
- Repository: https://github.com/bugsee/bugsee-javascript/tree/main/packages/browser
- Bugsee: https://bugsee.com
Persistence
By default the rolling capture window and the queue of reports waiting to upload are kept in IndexedDB, so, where the browser supports it, the recent past survives a page reload or a browser restart, and a report interrupted mid-upload is sent on the next visit. Data is stored in per-app databases named bugsee-<hash>, bugsee-capture-<hash> and bugsee-markers-<hash>.
Pass persist: false to keep everything in memory instead. The default applies only when the page exposes both an IndexedDB object and Web Locks (navigator.locks, which needs a secure https:// context; Safari before 15.4 and some embedded WebViews lack it): without locks a previous visit's data can never be recovered or cleaned up, so the SDK stays in memory instead. Pass persist: true to store anyway. If the browser refuses to store data (over quota, some private modes), writes fail best-effort and are reported to onError; the SDK does not switch to memory at that point. Any persist value other than true or false counts as off and is reported.
The url trace
The url system trace records the address of the page the user is on, sanitized, as a held step curve: written at start and whenever the URL changes (the Navigation API's currententrychange where present, otherwise history.pushState/replaceState, popstate and hashchange). It is on by default and independent of the performance extension. captureUrl: false turns it off (any non-boolean value also turns it off and is reported). It needs captureSystemTraces. The navigation source behind it is shared with the performance extension through the carrier (one source per carrier, normally one per window: with several windows on ONE carrier the first window's source wins, so give each window its own carrier), and it never removes a history wrapper that another library installed after launch: when the SDK stops while such a wrapper sits on top of its own, its wrapper stays in the chain as an inert pass-through.
What the default sanitizer does to the address:
- redacts the password of userinfo, and the values of sensitive keys in the query, the fragment, the hash-router query (
#/route?code=..., also after a real query) and;key=valuematrix path segments, plus secret-shaped tokens (JWT, AWS, Stripe, GitHub); - the sensitive keys are the shared denylist (substring match, such as
token,password,auth,api_key) plus, for the page address only, these keys matched exactly (case and-_.ignored;code[],code%5B%5Dand+codematch too):code,state,session,sid,sig,ticket. Being a substring list, the shared one already catchessessionid,signature,otpandcode_verifier, and over-redacts harmless keys that merely contain a listed word (for example?footprint=).keyandtare deliberately not redacted (too common, and a false redaction loses the screen's identity), so a secret under a key outside the list is not caught; - sanitizes a URL or a path with a query nested in a query value or in the fragment (
?next=https://x/?token=...,?returnTo=%2Freset%3Ftoken%3D...,#next=https%3A%2F%2F..., also half-encoded or double-encoded, and with invalid escapes tolerated one escape at a time), to two levels, forfile:pages too; a third level is not scanned. Each level costs at most about 3 times its input, which is why the depth is fixed; - by scheme:
data:,javascript:andvbscript:become<scheme>:%3Credacted%3E;blob:andfilesystem:keep only the inner origin (blob:https://a.com);file:becomesfile:///.../<last segment>(its query and fragment are kept and redacted like http's); any other scheme is handled like an http URL; - reads a raw string the way a browser would (
https:/u:[email protected],https:\\u:[email protected]\x,file:C:\Users\bob\a.html); - clips an address longer than 8192 characters FIRST (before any scan, and marks it
%3Ctruncated%3E; a cut inside a percent-escape drops the partial escape), so a pathological URL cannot cost the app main-thread time inside its ownpushState.
What it does NOT do: it does not redact ids, emails, long base64 strings or other values in a path segment (/users/42, /verify/9f8a7b6c; only token shapes and sensitive ;key=value segments are), and file: paths keep their last segment. Query keys are kept, so two screens stay distinguishable. If your paths carry secrets, add a urlSanitizer.
urlSanitizer: (url) => string | null is your own hook. It runs after the default and receives its result; the string it returns is written as given (clipped at 2048 characters, marked %3Ctruncated%3E), so it is up to you not to put back what the default removed. It runs synchronously inside the navigation, so keep it cheap. Return null to write nothing for that URL. A throw, or a result that is neither a string nor null, is reported through onError and writes nothing. In every case where nothing is written, the previous URL stays held (a held trace cannot be retracted). The hook applies to the url trace only, not to network events, breadcrumbs, transaction names or the replay stream, and it is kept by relaunch().
The same sanitizer is applied to the other places the SDK writes the page address: the pageload and navigation transaction names (path only, no query), the Next.js navigation breadcrumb, resource-timing span descriptions, and the href of the replay stream's opening Meta event.
The SDK protects the origin's storage quota by default whenever it persists to IndexedDB (set diskProtection: false to opt out; any other value keeps it on). It watches navigator.storage.estimate() headroom (quota - usage; an estimate, not device free space) every 5 minutes, and every 30 seconds once headroom is under 500 MiB. Under 50 MiB it stops writing replay, logs and network data to IndexedDB, and under 10 MiB it writes nothing, except that, when the state comes from a storage READING, the small capture state record of the system traces is still written so that a recovered report can say why it is empty (a QuotaExceededError, AbortError or InvalidStateError on a write pins the state and blocks that exception too; traces, events, input and breadcrumbs keep being written until then); the in-memory window is kept either way, so a report built while the page is alive holds everything the page still has in memory (records already written to a database the browser can no longer read are not in it; records still queued for it or refused by it are, and a database that stops answering is given up on after 5 seconds without progress; while it stays unreadable a report waits at most 2 seconds for it and is built from what is queued or refused, and once a read of the database completes again, even a slow one, every record it holds is back in the next report: a read that overruns its 2 seconds is not abandoned but finishes in the background, so a slow but healthy database is never treated as dead for good; its data is discarded and it does not delay the deletion of an evicted part), and what a crash or reload loses is the data that was not written. A state is left only 10 MiB above its threshold, and a write refused with QuotaExceededError moves it to the out-of-space state until a write succeeds again. Chromium reports a flat, generous headroom from estimate(), so on Chrome a refused write (Chrome reports it as QuotaExceededError, AbortError or InvalidStateError) is what triggers the protection. When replay has been written again for 5 seconds the recorder restarts with a fresh full snapshot. captureDiskSpace: true (off by default; only the boolean true counts) adds the telemetry: with replay on, the capture system trace also states low_disk_space and no_disk_space. It needs the protection and a durable store, and is reported once through onError when there is none.
License
MIT. See the LICENSE file.
The route and view traces
client.setRoute('/orders/:id') records the route the user is on as the route system trace, and client.setView('OrderDetail') (or { name, type }, or null for "no view") records the screen as the view trace. Both are held step curves, de-duplicated, and written by the browser, a WebView and an Electron renderer (Node holds the value and writes nothing). Pass the router's PATTERN, not the concrete URL: a fragment and any query are cut (a ? that is router syntax, such as /:id? or /:id(colou?r), stays), userinfo and e-mail addresses (plain, %40-encoded or non-ASCII) are removed, an optional literal character such as /ab?cd is read as a query, secret shapes are redacted and the value is clipped (route 1024 characters, view name and type 256). A WebView writes route only: its native host owns view. Several Electron renderers interleave their route/view values in one stream, as the url trace does. A navigation reported to an adapter before the SDK is launched is dropped (not held), and a committed navigation whose component name is unreadable leaves the previous view held; setView(null) clears it. The framework adapters (@bugsee/react, vue, solid, angular, svelte/SvelteKit, nuxt) call setRoute on committed navigations; they call setView only when they can read a component name that is not minified, so setView is the only guaranteed source of the view trace. A plain page without an adapter writes no route (the url trace is its page-level record).
