Every captureException(), log.*(), span(), and the rest all end the same way: the finished event is handed to the client's transport — the one thing responsible for actually getting it to the network. By default that's a batching fetch() transport pointed at this project's /api/envelope; both what it does and what it's replaced with are configured via the transport/ transportOptions options covered in Configuration.
The default: a batching fetch transport
init() builds makeFetchTransport for you from the dsn (on the CDN embed it's already baked into the bundle). It doesn't fire one HTTP request per event — it queues them and flushes a batch as one POST https://app.buglapse.com/api/envelope with { events: [...] } and the DSN's key in an X-Buglapse-Auth: Buglapse buglapse_key={key} header, whichever comes first:
- the buffer reaches
maxBatchSize(default20), - an event of a type listed in
immediateTypesis queued (default: justexception— an error is worth shipping right away rather than waiting on the timer), flushIntervalms of inactivity passes since the last queued event (default2000),- something calls
Buglapse.flush()explicitly, or - the tab is backgrounded or closed (see Delivery on page unload below).
A finished root span/asyncSpan also triggers a flush on its own, so a whole trace ships together as soon as it's done rather than trickling in on whatever cadence the batch timer picks.
Retries and failures
A request that fails outright (network error, timeout) is retried once by default before the batch is given up on — a 4xx/5xx response from the backend is not retried, since a rejected payload or an exceeded quota won't be fixed by trying again. Both are tunable:
ts
Buglapse.init({
dsn: '{dsn}',
transportOptions: {
timeout: 5000, // ms before an in-flight request is aborted
retries: 1, // additional attempts after a transient network failure
},
})When a batch is given up on, its events are dropped — there's no on-disk queue or later retry. Hook onError to find out when that happens instead of losing it silently:
ts
Buglapse.init({
dsn: '{dsn}',
transportOptions: {
onError: (error, event) => {
console.warn('Buglapse dropped an event:', event.type, error.message)
},
},
})onError fires once per dropped event, after retries are exhausted. Don't report back through Buglapse itself from inside it (Buglapse.captureException(error)) — the backend that just failed would likely fail that report too, so the SDK detects and drops any such re-entrant call rather than risk looping forever.
Rate limits
When the backend rate-limits the project (see Events → Rate limits) it answers with an X-Buglapse-Rate-Limits header naming the affected event types and how long to wait, or a 429 with Retry-After. The transport then drops events of those types before they're even queued until the time is up — there's nothing to configure. Turn on debug to see when it happens.
Delivery on page unload
A fetch() already in flight when the tab closes or navigates away can be cancelled by the browser before it completes, and one about to be scheduled by the batch timer may never get the chance to start at all. Whenever the page is hidden or unloading (document.visibilityState turning "hidden"), the transport skips straight to navigator.sendBeacon() for whatever is still buffered instead of racing fetch() against teardown — the browser's guaranteed best-effort delivery mechanism for exactly this case. sendBeacon can't set headers, so the key travels as a ?buglapse_key= query parameter instead. It's fire-and-forget by design (no retry, no response), and only used at that moment; every other flush still goes through fetch().
Flushing manually
Buglapse.flush() forces the current batch out immediately and returns a promise that resolves once it's sent:
ts
Buglapse.addBreadcrumb({ type: 'ui.click', level: 'info', message: 'Clicked "Log out"' })
await Buglapse.flush()
window.location.href = '/logged-out'Reach for it before something that might tear the page down before the batch timer would otherwise fire — a manual redirect, closing a popup, tearing down an SPA route. Automatic delivery on unload (above) already covers the tab being closed by the user, so this is mainly for navigations your own code triggers.
transportOptions reference
Tunes the default fetch transport; every key is ignored if you supply your own transport (see below).
timeout— ms before an in-flight request is aborted. Default5000.retries— additional attempts after a transient network failure, not a 4xx/5xx response. Default1.maxBatchSize— events buffered before an automatic flush. Default20.flushInterval— ms of inactivity since the last queued event before an automatic flush. Default2000.immediateTypes— event types that flush the whole batch right away instead of waiting on the timer/size limit. Default['exception'].onError— called once per event when a batch fails to send, after retries are exhausted. See Retries and failures above.
Swapping the transport
Pass a different transport factory to send events somewhere other than this project's /api/envelope — most commonly makeConsoleTransport, which just logs each event instead of sending it, for working locally without a running backend:
ts
import { init, makeConsoleTransport } from '@buglapse/browser'
init({
dsn: '{dsn}',
transport: makeConsoleTransport,
})transport takes the factory itself, not a call to it — makeConsoleTransport, not makeConsoleTransport(). With a custom transport, dsn is no longer required and every transportOptions key is ignored, since there's no default fetch transport left to tune.
Writing your own
A transport is any object matching this shape:
ts
interface Transport {
send(event: BuglapseEvent): void | Promise<void>
flush?(): void | Promise<void>
readonly endpoint?: string
}send(event)— called once per captured event. What it does with it is entirely up to you — batch it, forward it, write it tolocalStorage, whatever a non-default backend needs.flush()— optional. Implement it ifsend()buffers anything, soBuglapse.flush()and the end-of-root-span flush have something to force out. A transport that sends synchronously (likemakeConsoleTransport) can leave it out entirely.endpoint— optional, and only relevant if you also enable BreadcrumbsIntegration or HttpErrorsIntegration: both patchfetch/XHRto turn outgoing requests into breadcrumbs/logs, and useendpointto recognize and skip the SDK's own delivery requests so they don't loop back in as noise about themselves.
Buglapse takes a factory, not a ready-made instance — (options) => Transport — so it controls when the transport is actually constructed:
ts
import { init, Transport, TransportOptions } from '@buglapse/browser'
function makeBeaconOnlyTransport(options: TransportOptions): Transport {
// DSN: https://{key}@{host}
const dsn = new URL(options.dsn!)
const endpoint = `${dsn.origin}/api/envelope`
const key = decodeURIComponent(dsn.username)
return {
endpoint,
send(event) {
const url = `${endpoint}?buglapse_key=${encodeURIComponent(key)}`
navigator.sendBeacon(url, JSON.stringify({ events: [event] }))
},
}
}
init({
dsn: '{dsn}',
transport: makeBeaconOnlyTransport,
})options is whatever the top-level dsn/debug plus your own transportOptions add up to — a custom transport is free to read any of it, or ignore it entirely and hardcode its own destination.