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.

kindUse for
httpOne HTTP request (set method, route, and status when you scope it yourself).
serverlessOne short-lived handler. Call flush before exit.
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();
}

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.

FieldDefaultWhat it does
enabledtrueTurns runtime-state capture on or off.
maxEventsPerExecution500Maximum events kept for one execution.
maxCapturedCallsPerFunction3Detailed 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 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,
  },
});

Sensitive data: captured state can include arguments and returns. scrubPII on RasputinInit (default true) redacts common personal data. Add field names to executionRecorder.redactKeys.