Overview
An integration is a small plugin that hooks into the SDK client once, when it's registered. Every built-in (httpServerIntegration, breadcrumbsIntegration, ...) uses the same interface you get — anything a built-in does, your own integration can do too.
ts
interface Integration {
name: string
setup(client: BuglapseClient): void
teardown?(): void
}name— a unique id.disable(name)looks it up, and it decides whether a second registration is ignored or replaces an existing one (see Registering).setup(client)— called once, when the integration is registered on a live client. Subscribe to events, patch modules, set context or add a preprocessor here.teardown()— optional. Called bydisable(name); undo whateversetup()did.
A minimal integration
Tag every event with the deployment it came from, read from environment variables:
ts
import type { Integration } from '@buglapse/node'
export function deploymentIntegration(): Integration {
return {
name: 'deployment',
setup(client) {
client.setTag('region', process.env.FLY_REGION ?? 'local')
client.setContext('deployment', {
id: process.env.DEPLOYMENT_ID,
commit: process.env.GIT_SHA,
})
},
}
}Export a factory, not a plain object. deploymentIntegration() returns a fresh object on each call, so per-instance state (listeners, timers, counters) never leaks between clients — and every integration, built-in or yours, is called the same way.
Registering an integration
In init()
ts
import { init } from '@buglapse/node'
import { deploymentIntegration } from './buglapse/deployment'
init({
dsn: process.env.BUGLAPSE_DSN,
integrations: [deploymentIntegration()],
})The list is merged with the SDK's defaults (currently only uncaughtExceptionIntegration()). An entry whose name matches a default replaces it — an integration named 'uncaught-exception' swaps out the built-in crash handling without a separate disable() call.
With use()
ts
import { use } from '@buglapse/node'
use(deploymentIntegration())use() works before init() (queued, applied right after the defaults) and after (setup() runs immediately). It also accepts a factory plus its config, the way built-ins are registered:
ts
use(slowQueryIntegration, { pool, thresholdMs: 500 })
// same as: use(slowQueryIntegration({ pool, thresholdMs: 500 }))Unlike init({ integrations }), use() never replaces: if an integration with the same name is already registered, the new one is ignored. To swap one out, disable(name) first.
Disabling
ts
import { disable } from '@buglapse/node'
disable('deployment') // calls teardown(), then unregisters itBefore init(), disable(name) only affects the built-in defaults — it keeps them from being registered at all.
What setup() can do
setup() receives the same BuglapseClient the flat API talks to:
| Method | Use it to |
|---|---|
client.setTag(key, value) | Add a searchable tag to every future event (process-wide) |
client.setContext(key, object) | Attach structured context to every future event (process-wide) |
client.withScope(fn) | Run fn with its own scope, breadcrumbs and tags |
client.addBreadcrumb({ type, level, message, data }) | Record something that leads up to the next exception |
client.captureException(error) | Report an error |
client.log.info(message, attributes) (and log/warn/error/debug) | Send a log event |
client.span() / client.asyncSpan() / client.startInactiveSpan() | Instrument an operation as a span |
client.addPreprocessor(fn) | Rewrite or drop events before they're sent |
client.flush() | Push out any batched events right away |
client.transport.endpoint | The envelope URL — to avoid instrumenting the SDK's own requests |
client.debug | Whether debug: true was passed to init() |
setTag() / setContext() on the client are global to the process. For anything specific to one request or job, use withScope() — see Tags & Context.
Listening to process events
Node reports a lot through events on process or on library objects. Subscribe in setup(), unsubscribe in teardown(). This one forwards Node's own runtime warnings (deprecations, MaxListenersExceededWarning, ...) as logs:
ts
import type { Integration } from '@buglapse/node'
export function processWarningsIntegration(): Integration {
return {
name: 'process-warnings',
setup(client) {
const onWarning = (warning: Error & { code?: string }) => {
client.log.warn(warning.message, { name: warning.name, code: warning.code })
}
process.on('warning', onWarning)
this.teardown = () => process.off('warning', onWarning)
},
}
}Setting this.teardown inside setup() is the simplest way to reach values that only exist once setup has run (listeners, original functions). It works because the client calls setup() on the integration object itself.
A timer started in setup() should be unref()'d, so it never keeps the process alive on its own — as EventLoopBlockIntegration does.
Wrapping a library
To trace or log calls into a library, wrap its method and restore it in teardown(). This one records every query of a pg pool as a db span and logs the slow ones (promise-style pool.query() calls only — the callback form isn't handled):
ts
import type { Integration } from '@buglapse/node'
import type { Pool } from 'pg'
export interface SlowQueryConfig {
pool: Pool
thresholdMs?: number
}
export function slowQueryIntegration(config: SlowQueryConfig): Integration {
const { pool, thresholdMs = 1000 } = config
return {
name: 'slow-query',
setup(client) {
const originalQuery = pool.query
pool.query = async function (this: Pool, ...args: any[]) {
const sql = typeof args[0] === 'string' ? args[0] : args[0]?.text
const span = client.startInactiveSpan(sql?.split(' ', 1)[0] ?? 'query', { op: 'db' })
span.setData('db.statement', sql)
const startedAt = Date.now()
try {
return await originalQuery.apply(this, args)
} catch (error) {
span.setStatus('error')
throw error
} finally {
const duration = Date.now() - startedAt
span.end()
if (duration >= thresholdMs) {
client.log.warn('Slow query', { sql, duration })
}
}
} as typeof pool.query
this.teardown = () => {
pool.query = originalQuery
}
},
}
}When patching:
- Never change the original behavior. Call through to the original and return (or re-throw) its result unchanged.
- Use
startInactiveSpan()for a call that's awaited while other work runs — it joins the active trace without becoming the active span itself. See Tracing → Leaf spans. - Skip the SDK's own traffic if you instrument HTTP: compare the request URL against
client.transport.endpoint, or each event delivery reports itself. - Several integrations can wrap the same function. Disable them in reverse order of registration, or a later
teardown()can restore a stale wrapper.
Filtering and rewriting events
A preprocessor sees every event before it's sent. Return the (possibly modified) event to keep it, or null to drop it. For exception events the second argument is the original Error, so you can branch on its class:
ts
import type { Integration } from '@buglapse/node'
export function ignoreClientErrorsIntegration(): Integration {
let enabled = true
return {
name: 'ignore-client-errors',
setup(client) {
client.addPreprocessor((event, error) => {
if (!enabled || event.type !== 'exception') return event
const status = (error as { statusCode?: number } | undefined)?.statusCode
return status !== undefined && status < 500 ? null : event
})
},
teardown() {
enabled = false
},
}
}Preprocessors can't be unregistered — gate yours behind a flag that teardown() flips, as above. They run in registration order, and once one returns null the rest are skipped. For a one-off filter, call addPreprocessor(fn) from @buglapse/node directly instead.
Sending other event types
For a built-in event type with no dedicated method, there's a lower-level escape hatch:
ts
client.capture('hit', { name: 'export-finished' })capture(type, payload) stamps the current tags, context and trace onto the payload and sends it through the same preprocessors and transport as everything else. payload must match that type's shape (see Events) — prefer the dedicated methods whenever one exists.
Debugging
With debug: true in init(), the client logs every integration registration (Integration "process-warnings" registered), every disable(), every captured event and every event a preprocessor drops. Check client.debug before adding extra logging of your own.