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 or job. Recorder state lives only inside that scope and is attached when the work fails.

kindUse for
httpOne HTTP request (set method, route, and status when you scope it yourself).
jobOne background or queue worker iteration.
scheduledOne cron or scheduled run. Use a stable name.
customOther 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();
}

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) => {
    // ...
  },
);

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:

  1. Each execution still records which functions ran, how they nest, and how long they took.
  2. Arguments and returns wait until a qualifying error (a failure Rasputin reports). HTTP 4xx responses do not turn on full capture.
  3. 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.
  4. 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 rasputin = 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.

FieldDefaultWhat it does
enabledtrueTurns runtime-state capture on or off.
maxEventsPerExecution1500Maximum events kept for one execution.
maxCapturedCallsPerFunction1Detailed successful calls kept before similar calls are summarized.
maxActiveMemoryBytes64 MiBMaximum recorder memory shared across active executions.
maxDepth3Maximum captured value depth.
maxObjectKeys30Maximum properties kept from one object.
maxArrayElements20Maximum items kept from one array, map, or set.
maxStringLength500Maximum characters kept from one string.
maxSerializedValueBytes16 KiBMaximum 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.enabledfalseRecord 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.fullCaptureDurationMs86400000How 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.dynamicDurationSamplingfalseInactive 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 rasputin = RasputinInit({
  // ...required options
  executionRecorder: {
    adaptive: { enabled: true, dynamicDurationSampling: false, fullCaptureDurationMs: 86_400_000 },
    excludeSources: ['src/logger/**', 'packages/shared-logger/**'],
    redactKeys: ['customerEmail'],
    maxEventsPerExecution: 200,
  },
});

Sensitive data: captured state can include arguments and returns. Rasputin redacts default secret names and 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.