@bucker/node
Server-side capture: crash handlers, on-disk source context, and per-request scopes
via AsyncLocalStorage.
Distribution note. The package is build-ready, not yet published. It carries a
publishConfig(pointing atdist/, split per subpath) alongside a source-pointing top-levelmain/types/exports— the split matters because several other workspace packages import the SDKs by source with no build step, while a published consumer cannot compile./srcand needs the compileddist/.pnpm build/prepackrunscripts/build-package.mjs, which refuses to finish if the built output does not match whatpublishConfig.exportspromises. Inside the monorepo it resolves throughworkspace:*. What remains is an actualnpm publish, a human decision covered insdks/PUBLISHING.md— not code readiness.
Init
Put this in its own module and import it first in your entry file, before any other import. Crash handlers installed after the rest of the app has evaluated do not cover the rest of the app evaluating.
// bucker.server.ts
import { init } from '@bucker/node'
export const bucker = init({
dsn: process.env.BUCKER_DSN ?? '',
environment: process.env.NODE_ENV ?? 'development',
})
// index.ts
import './bucker.server.js' // must stay first
import express from 'express'
create-bucker writes exactly this pair and injects the import for you.
Options
All of ClientOptions (see sdk-browser.md) plus:
| Option | Default | Meaning |
|---|---|---|
linesOfContext |
SDK default | Lines of on-disk source context per in-app frame. 0 disables the integration |
maxBufferBytes |
SDK default | Transport queue ceiling |
fetchImpl |
global fetch |
Testing seam |
serverName defaults to process.env.BUCKER_SERVER_NAME.
Default integrations
globalHandlersIntegration() and, unless linesOfContext: 0,
contextLinesIntegration().
Crash handlers. uncaughtException is the one hook where an SDK can genuinely
make things worse: install a listener and do nothing else, and you have converted
"crash loudly" into "limp along in an undefined state". So the default is capture →
flush with a bounded timeout (flushTimeoutMs, default 2000) → let the process die
exactly as it would have. exitOnUncaught defaults to true for the same reason.
unhandledRejection is captured too.
Source context. contextLinesIntegration reads ±N lines around each in-app frame
straight off disk at capture time, so a server stack arrives already readable without
any source-map step. SourceReader and addSourceContext are exported if you need
them directly.
Express
import { errorHandler, requestHandler } from '@bucker/node'
import { bucker } from './bucker.server.js'
app.use(requestHandler(bucker)) // before your routes
// … routes …
app.use(errorHandler(bucker)) // after them
requestHandler forks a scope per request, attaches request context (method, URL,
headers, query string) and a transaction name, then runs the rest of the chain inside
it. errorHandler captures with mechanism: { type: 'middleware', handled: false }
and passes the error onward untouched — it never swallows.
Client IPs are attached only when includeIp is set on the handler options.
Fastify
import { fastifyHooks } from '@bucker/node'
import { bucker } from './bucker.server.js'
const hooks = fastifyHooks(bucker)
app.addHook('onRequest', hooks.onRequest)
app.addHook('onError', hooks.onError)
Fastify runs the remaining lifecycle inside onRequest's done() continuation, so
the AsyncLocalStorage context propagates the same way it does through Connect's
next().
Next.js
Next.js runs on both sides, so it needs both SDKs. Use instrumentation.ts beside
your routes directory:
export async function register() {
if (process.env.NEXT_RUNTIME !== 'nodejs') return
const { init } = await import('@bucker/node')
init({ dsn: process.env.BUCKER_DSN ?? '', environment: process.env.NODE_ENV })
}
export async function onRequestError(error: unknown) {
const { captureException } = await import('@bucker/node')
captureException(error, { mechanism: { type: 'nextjs.onRequestError', handled: false } })
}
The dynamic import matters: an Edge runtime must not pull in the Node SDK at all.
Browser-side init goes in instrumentation-client.ts (Next ≥ 15.3) or, on older
versions, one import from the root layout. On Next < 15 you also need
experimental: { instrumentationHook: true }.
Capturing by hand
Same surface as the browser SDK: captureException, captureMessage,
addBreadcrumb, setUser, setTag, setExtra, setContext, flush, close,
getCurrentScope. getRequestScope() additionally returns the scope for the request
currently in flight.
Not built yet
Opt-in local-variable capture is deliberately absent. The seam exists —
StackFrame.vars in the shared schema plus the event-processor hook that
contextLinesIntegration already uses — but shipping variable capture before the
entropy- and format-aware masking (JWT / AWS key / Luhn / email) and the redaction
manifest is how an SDK exfiltrates a customer's secrets. It is staged after masking,
not before.
Also absent: OTel trace correlation beyond passing through traceId/spanId, cron
monitoring, and profiling.