Skip to content

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 maxBatchSize events (default 20);
  • an event of a type in immediateTypes is queued (default: exception — errors go out right away);
  • flushInterval ms pass since the queue started filling (default 2000);
  • 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.

OptionDefaultDescription
timeout5000ms before an in-flight request is aborted
retries1Extra attempts after a network failure (not after a 4xx/5xx response)
maxBatchSize20Events queued before an automatic flush
flushInterval2000ms 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 if send() buffers anything, so flush() 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 })