Instrumentation & executions
Instrument functions, scope each unit of work, and tune recorder limits if you need to.
Enable automatic instrumentation
Use the same build and start flags as Node when you ship compiled JavaScript. HTTP requests are scoped by the Elysia plugin (see Executions).
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/elysia/instrument/bun src/app.ts",
"start:node": "node --import @rasputin-ai/elysia/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 or job. 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). |
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
Put .use(plugin) first in your Elysia chain:
import { Elysia } from 'elysia';
import { RasputinInit } from '@rasputin-ai/elysia';
const { client, plugin } = RasputinInit({
// ...required options
});
const app = new Elysia()
.use(plugin)
.use(otherPlugin)
.get('/health', () => 'ok');The plugin calls execution.createScope({ kind: 'http', … }) once per
request, so handlers and onError already run inside that execution. Do not
wrap routes yourself. Nested scopes duplicate recordings.
Background jobs
Wrap each queue message, worker loop, or cron run. Use kind: 'job' or kind: 'scheduled' and a stable name.
const scope = client.execution.createScope({
kind: 'job',
name: 'sync-invoices',
});
try {
return await scope.run(() => syncInvoices());
} catch (error) {
const observation = scope.observeError(error);
client.captureException(error, { observation });
} finally {
scope.finish();
}For cron-style work, use kind: 'scheduled':
const scope = client.execution.createScope({
kind: 'scheduled',
name: 'nightly-rollup',
});
try {
await scope.run(() => runNightlyRollup());
} catch (error) {
const observation = scope.observeError(error);
client.captureException(error, { observation });
} finally {
scope.finish();
}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);
client.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 = client.execution.trace(
'src/billing/charge.ts:chargeCustomer',
async (customerId: string) => {
// ...
},
);Adaptive mode
Full recording copies and bounds every argument and return on every call. That adds CPU and memory work on busy servers. Adaptive mode skips value capture on the happy path and still records call history and timings, so you keep useful failures with less overhead.
When it is enabled:
- Each execution still records which functions ran, how they nest, and how long they took.
- Arguments and returns wait until a qualifying error (a failure Rasputin reports). HTTP
4xxresponses do not turn on full capture. - That error turns on full capture for the same execution type (same job name, route, and so on)
for
fullCaptureDurationMs, default 24 hours. See Configuration for the window and optional duration sampling. - Matching runs that start during the window include argument and return values. The run that failed does not.
Need values on the first failure? Leave adaptive mode off.
const { client, plugin } = RasputinInit({
// ...required options
executionRecorder: {
adaptive: { enabled: true },
},
});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 | 1500 | Maximum events kept for one execution. |
maxCapturedCallsPerFunction | 1 | 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 property names copied at init and applied at ingest with the default sensitive names. |
excludeSources | [] | Source globs or function-name patterns to exclude. |
adaptive.enabled | false | Record calls and timings without values until a qualifying error, then capture values for matching executions for a fixed window. Default captures arguments and returns from the start. |
adaptive.fullCaptureDurationMs | 86400000 | How long matching executions keep capturing values after the first qualifying error. Default 24 hours. Clamped to 1 second–7 days. Ignored unless adaptive recording is enabled. |
adaptive.dynamicDurationSampling | false | Inactive adaptive only. Default false: enabling adaptive still times every eligible invocation. True reduces duration measurements progressively for each function, its direct caller, and execution type, then resets those counters every hour. Invalid values are treated as false. Counts and call history stay retained. Selected sessions and activations always have full timing coverage. |
const { client, plugin } = RasputinInit({
// ...required options
executionRecorder: {
adaptive: { enabled: true, dynamicDurationSampling: false, fullCaptureDurationMs: 86_400_000 },
excludeSources: ['src/logger/**', 'packages/shared-logger/**'],
redactKeys: ['customerEmail'],
maxEventsPerExecution: 200,
},
});executionRecorder.redactKeys at ingest. Set scrubPII: true on RasputinInit (default false) to also redact
personal data.
See Redaction for ignore comments and what we hide automatically.