Every captureException(), log.*() and finished span ends the same way: the event is handed to the client's transport, which gets it to the network. By default that's a batching transport built on Node's global fetch(), posting to this project's /api/envelope.
The default: a batching fetch transport
Events aren't sent one request each. They're queued and posted together 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 queue reaches
maxBatchSizeevents (default20); - an event of a type in
immediateTypesis queued (default:exception— errors go out right away); flushIntervalms pass since the queue started filling (default2000);- a root span finishes — a whole trace ships as soon as it's done;
- you call
flush().
Flushing before exit
Nothing is sent when the process exits on its own terms — there's no equivalent of the browser's page-unload hook. Anything still queued when you call process.exit(), or when a serverless platform freezes the function after it returns, is lost. await flush() first:
ts
import { flush } from '@buglapse/node'
// a CLI script
await run()
await flush()
process.exit(0)ts
import { captureException, flush } from '@buglapse/node'
// a serverless handler
export async function handler(event) {
try {
return await handle(event)
} catch (error) {
captureException(error)
throw error
} finally {
await flush()
}
}On a graceful shutdown, flush after the server has stopped taking requests:
ts
process.on('SIGTERM', () => {
server.close(async () => {
await flush()
process.exit(0)
})
})A process that simply runs out of work exits after the pending batch is sent — the flush timer keeps it alive until then. A crash is handled by UncaughtExceptionIntegration, which flushes before exiting.
Retries and failures
A request that fails outright (network error, timeout) is retried once by default. A 4xx/5xxresponse isn't retried: a rejected payload or an exceeded quota won't be fixed by trying again.
When a batch is given up on, its events are dropped — there's no on-disk queue. Hook onError to know when that happens:
ts
import { init } from '@buglapse/node'
init({
dsn: process.env.BUGLAPSE_DSN,
transportOptions: {
timeout: 5000,
retries: 2,
onError: (error, event) => {
process.stderr.write(`Buglapse dropped a ${event.type} event: ${error.message}\n`)
},
},
})onError runs once per dropped event, after retries are exhausted. Don't report back through Buglapse from inside it — the backend that just failed would fail that report too, so the SDK drops such re-entrant events instead of looping.
A 403 with Origin not allowed for this project means the project has Allowed domains set under Settings → Security — requests from a server carry no Origin header and are rejected. Clear the list, or use a separate project for the server.
Rate limits
When the backend rate-limits the project (see Events → Rate limits), it answers with an X-Buglapse-Rate-Limits header, or a 429 with Retry-After. The transport then drops events of the affected types before they're queued, until the time is up. There's nothing to configure; turn on debug to see when it happens.
transportOptions reference
Tunes the default transport; ignored when you pass your own transport.
| Option | Default | Description |
|---|---|---|
timeout | 5000 | ms before an in-flight request is aborted |
retries | 1 | Extra attempts after a network failure (not after a 4xx/5xx response) |
maxBatchSize | 20 | Events queued before an automatic flush |
flushInterval | 2000 | ms after the first queued event before an automatic flush |
immediateTypes | ['exception'] | Event types that flush the queue right away |
onError | — | (error, event) => void, called per dropped event |
Swapping the transport
makeConsoleTransport prints each event to the console instead of sending it — handy for local development without a backend:
ts
import { init, makeConsoleTransport } from '@buglapse/node'
init({
transport: makeConsoleTransport,
})transport takes the factory itself — makeConsoleTransport, not makeConsoleTransport(). With a custom transport, dsn is no longer required.
Writing your own
A transport is an object with this shape:
ts
interface Transport {
send(event: BuglapseEvent): void | Promise<void>
flush?(): void | Promise<void>
readonly endpoint?: string
}send(event)— called once per event.flush()— optional; implement it ifsend()buffers anything, soflush()and the end-of-trace flush have something to push out.endpoint— optional. The HTTP integrations (breadcrumbs,httpTracingIntegration,httpErrorsIntegration) watch every outgoing request; a request to this URL is recognized as the SDK's own and skipped. If your transport sends over HTTP, set it — otherwise each delivery shows up as a breadcrumb, span or log of its own.
transport takes a factory, (options) => Transport. options carries the top-level dsn and debug plus your transportOptions. This one writes events to a local newline-delimited JSON file, e.g. for a log shipper to pick up:
ts
import { appendFileSync } from 'node:fs'
import { init, type Transport } from '@buglapse/node'
function makeFileTransport(): Transport {
return {
send(event) {
appendFileSync('/var/log/buglapse-events.ndjson', JSON.stringify(event) + '\n')
},
}
}
init({ transport: makeFileTransport })