Instrumentation & executions
Instrument functions, scope each unit of work, and tune recorder limits if you need to.
Enable automatic instrumentation
Wrap functions at build time or when the process starts so Rasputin can observe calls.
Compiled JavaScript
Use this in production. Build as you already do, then run rasputin-instrument on the output. Start the
process normally, with no extra flags.
{
"scripts": {
"build": "tsc && rasputin-instrument dist",
"start:node": "node dist/app.js",
"start:bun": "bun dist/app.js"
}
}Emit .js.map files next to the output so stack
frames map back to .ts.
Running TypeScript directly
rasputin-instrument rewrites compiled .js files on disk. It does not run when you start
TypeScript with Bun. Use --preload / --import so files are wrapped as they load.
{
"scripts": {
"start:bun": "bun --preload @rasputin-ai/node/instrument/bun src/app.ts",
"start:node": "node --import @rasputin-ai/node/instrument/node dist/app.js"
}
}Use those flags while you run TypeScript locally. In production, instrument at build time instead. Wrapping files at startup is slower and uses more memory.
Executions
An execution groups instrumented calls under one request, job, or handler. Recorder state lives only inside that scope and is attached when the work fails.
| kind | Use for |
|---|---|
http | One HTTP request (set method, route, and status when you scope it yourself). |
serverless | One short-lived handler. Call flush before exit. |
job | One background or queue worker iteration. |
scheduled | One cron or scheduled run. Use a stable name. |
custom | Other work that does not fit the kinds above. |
HTTP requests
Node and Bun have no built-in HTTP wrapper. Scope each request in your middleware:
const scope = rasputin.execution.createScope({
kind: 'http',
request: { method: req.method, route: req.url },
});
try {
await scope.run(async () => {
await handleRequest(req, res);
});
} catch (error) {
const observation = scope.observeError(error);
rasputin.captureException(error, { observation });
} finally {
scope.finish({ request: { status: res.statusCode } });
}Background jobs
Wrap each queue message, worker loop, or cron run. Use kind: 'job' or kind: 'scheduled' and a stable name.
const scope = rasputin.execution.createScope({
kind: 'job',
name: 'sync-invoices',
});
try {
return await scope.run(() => syncInvoices());
} catch (error) {
const observation = scope.observeError(error);
rasputin.captureException(error, { observation });
} finally {
scope.finish();
}For cron-style work, use kind: 'scheduled':
const scope = rasputin.execution.createScope({
kind: 'scheduled',
name: 'nightly-rollup',
});
try {
await scope.run(() => runNightlyRollup());
} catch (error) {
const observation = scope.observeError(error);
rasputin.captureException(error, { observation });
} finally {
scope.finish();
}Serverless handlers
Use kind: 'serverless' per invocation. Call flush before the process exits.
export async function handler(event: unknown) {
const scope = rasputin.execution.createScope({
kind: 'serverless',
name: 'process-upload',
});
try {
await scope.run(async () => {
await processUpload(event);
});
} catch (error) {
const observation = scope.observeError(error);
rasputin.captureException(error, { observation });
} finally {
scope.finish();
await rasputin.flush(300);
}
}Errors, finish, and flush
scope.run() only enters the async context. You still
own await. When you catch an error, call observeError and pass observation to captureException.
Call finish() when the work ends. For HTTP, pass request.status when you have it. For streams, call finish() when the stream closes.
try {
await chargeCustomer();
} catch (error) {
const observation = scope.observeError(error);
rasputin.captureException(error, { observation });
}Manual instrumentation
If auto-instrumentation is not an option (for example, a single-file bundle), wrap functions with execution.trace() inside an execution scope.
// Identity: source path and function name (add @line:col when names collide)
const chargeCustomer = rasputin.execution.trace(
'src/billing/charge.ts:chargeCustomer',
async (customerId: string) => {
// ...
},
);Configuration
Recording is on by default. Change these to exclude noisy code, redact fields, or use less memory. Each error snapshot is capped at 256 KiB.
| Field | Default | What it does |
|---|---|---|
enabled | true | Turns runtime-state capture on or off. |
maxEventsPerExecution | 500 | Maximum events kept for one execution. |
maxCapturedCallsPerFunction | 3 | Detailed successful calls kept before similar calls are summarized. |
maxActiveMemoryBytes | 64 MiB | Maximum recorder memory shared across active executions. |
maxDepth | 3 | Maximum captured value depth. |
maxObjectKeys | 30 | Maximum properties kept from one object. |
maxArrayElements | 20 | Maximum items kept from one array, map, or set. |
maxStringLength | 500 | Maximum characters kept from one string. |
maxSerializedValueBytes | 16 KiB | Maximum retained size of one captured value. |
redactKeys | [] | Extra case-insensitive property names to redact. |
excludeSources | [] | Source globs or function-name patterns to exclude. |
const rasputin = RasputinInit({
// ...required options
executionRecorder: {
excludeSources: ['src/logger/**', 'packages/shared-logger/**'],
redactKeys: ['customerEmail'],
maxEventsPerExecution: 200,
},
});scrubPII on RasputinInit (default true) redacts common personal
data. Add field names to executionRecorder.redactKeys.