Skip to main content
The Bitfab TypeScript SDK captures your AI function calls to automatically generate evaluations. Re-run your prompts with different models, parameters, and inputs to iterate faster.

Installation

Quick Start

Choose an instrumentation style

Two decisions, in order. First opt-out or opt-in: trace() records a root and every first-party call beneath it, while withSpan() records exactly what you wrap. Opt-out is the recommended starting point and is still experimental; see Instrumentation for the comparison and Experimental subtree tracing below for setup. Then, within opt-in, wrapper or decorator:
withSpan works with every supported TypeScript version and with class methods, standalone functions, class fields, accessors, and functions from other libraries.
The stable span() decorator form requires TypeScript 5.0 or newer and a build pipeline that supports the standard ECMAScript decorator transform. Leave experimentalDecorators and emitDecoratorMetadata disabled or omitted. Use withSpan() in legacy-decorator projects. The experimental subtree transform accepts both standard @bitfab.trace / @bitfab.node output and the legacy output used by frameworks such as NestJS.
Need an API key? Get one from the Bitfab dashboard or see the API Keys guide for detailed setup instructions.
Copy this prompt into your coding agent (tested with Cursor and Claude Code using Sonnet 4.5):

Basic Configuration

Capture off is not tracing removed. With captureEnabled: false the wrappers stay in place and keep their trace function keys, so replay and seedTrace still work against a capture-off client. Replay records inside each item, and seedTrace records the one call it runs. Everything else runs untraced and sends nothing. enabled is a deprecated alias that warns once.
Missing API key doesn’t crash. If the API key is missing, empty, or whitespace-only, the SDK automatically disables tracing and logs a one-time warning at first use. All wrapped functions still execute normally — no spans are sent, no errors are thrown. You don’t need any conditional logic around the API key.

API key resolution

The key is resolved lazily, the first time a span runs, not when the client is constructed. This matters in scripts: with ES modules, an import that constructs the client is evaluated before the importing file’s body runs dotenv.config(), so a key read at construction would be empty even though it is set moments later. Resolving at first use reads the key after env loading has happened.
When no key is passed (or it resolves empty), the SDK falls back to reading BITFAB_API_KEY from the environment, again at first use. For standalone scripts where a run that emits no traces should be treated as a failure rather than silently skipped, set strict:
If you load env with dotenv in a script, prefer loading it before the module graph is evaluated, for example node --env-file=.env script.ts, so every module-level read sees the key.

Tracing

Experimental subtree tracing

Experimental. trace() and withTrace() subtree expansion may change in a future release. withSpan() is the stable explicit instrumentation API. Opt-out is the recommended starting point despite that, because the trace shape you get by default is the one you want; see Instrumentation.
For server-side TypeScript, Bitfab can record one root plus the first-party function calls beneath it, at any depth, without wrapping every function. Install the experimental build package and add one adapter at the point where your server code is first compiled:
If you previously installed bitfab-transform, replace that dependency with @bitfab/transform. Update imports and build configuration from bitfab-transform/ to @bitfab/transform/. Next.js needs only a config wrapper. It instruments Node server modules in both Turbopack and webpack builds, while leaving client and Edge bundles untouched:
next.config.ts
Vite and its server-side meta-frameworks use the ordinary Vite plugin shape:
vite.config.ts
For a direct TypeScript process, the registration hook both instruments and compiles repository modules. With tsx, register tsx first:
In a monorepo, run from the repository root to include shared packages in capture. The loader resolves its runtime from the SDK available to the transform installation, even when a shared package has no SDK dependency. To select a different application’s SDK, set BITFAB_SDK_RESOLVE_FROM to its package.json:
This selects the SDK installation; the working directory still determines capture scope and relative function IDs. It works for both ESM and CommonJS. Nest has a dedicated config wrapper. Select its webpack builder in nest-cli.json:
nest-cli.json
Then append the transform to Nest’s generated webpack configuration:
webpack.config.cjs
A Nest application using the tsc or SWC builder still records the rich root but does not gain automatic descendants from this adapter. Plain tsconfig.json cannot load a TypeScript transformer. Complete runnable transform configs, direct-Node and Next.js fixtures, and a real Nest server fixture with OTLP delivery assertions live in bitfab-typescript-example/auto-trace/. The root APIs are ordinary runtime wrappers; the transform does not rewrite their arguments or change their behavior. Without a transform, they still record one normal rich root span and run the code unchanged. They cannot discover calls beneath the root unless a compatible transform instruments those functions. For a class method, use the normal method decorator:
For function-oriented code, use withTrace:
Both root forms create the same rich root span and call-scoped subtree context. Most calls require no Bitfab wrappers. Use node() for a transformed method or withNode() for a transformed standalone function only when one discovered call needs explicit naming, typing, capture, finalization, or replay-mocking policy. A configured call outside the enclosing subtree trace runs normally and never creates a span or trace. The build adapter rewrites eligible first-party functions because imports, callbacks, dependency injection, and virtual dispatch make the runtime call tree impossible to predict statically. Each rewritten function checks the call-scoped context first. Outside the traced invocation, it runs its original body directly without emitting a span or constructing span metadata, captured inputs, or an invocation closure. A capture-off client or a client without a resolvable API key also runs the root directly: it does not request capture policy or activate automatic trace context. The options follow the same subtree model as Python:
  • name and type control the rich root span. Unconfigured descendants are function spans.
  • mockOnReplayDefault: true makes replay mocking the default for nodes under the trace’s default mock: "marked" replay strategy. A configured node with mockOnReplay: false overrides it. The option is off by default.
  • maxDepth (default 30) and maxSpans (default 500) bound each invocation. Hitting either limit warns once and marks the trace metadata as truncated because the emitted tree is incomplete.
  • exclude accepts simple or qualified function names. Skipping is transparent: calls below an excluded function still parent to the nearest recorded caller.
  • Anonymous callbacks are omitted. Rest-argument wrapper functions are omitted unless includeWrappers: true is set.
Each invoked function in a transformed server module becomes a real nested span. Automatic child spans capture their full inputs, outputs, and thrown errors by default, with no node() annotation or Studio setup. Inputs are the original call arguments supplied at the function boundary, including the complete objects and arrays used by destructured parameters, not reconstructed bindings or placeholders. Each span also carries timing, parentage, its qualified name, and a stable source-derived function ID. node() and withNode() apply trace-owned configuration to one discovered call. capture: true (the default) makes that call rich and applies name, type (default custom), testRunId, mockOnReplay, and finalize. capture: false omits the call and transparently attaches its captured descendants to the nearest captured parent. Combining capture: false with mockOnReplay: true throws because an omitted call has no recorded output. The runtime protocol is transform-agnostic: every adapter emits the same calls to @bitfab/sdk/auto, so application code and versioned, lexical function IDs do not change when the build tool changes. Re-running a transform is safe: generated modules carry an idempotence marker and are not instrumented twice. Bitfab’s installed SDK implementation is a dependency and is never transformed. Functions passed directly or by reference to withSpan() and withTrace() are also left alone because those wrappers already own their span. This prevents an explicit span from gaining an automatic duplicate. Functions passed to withNode() remain eligible because the enclosing subtree trace still owns their span. Named repository functions called inside an explicit span remain part of the automatic tree. Nested withTrace() roots. A withTrace() or trace() root entered beneath another one starts a separate trace while the outer root keeps recording. The nested root’s function and every call beneath it appear in both traces with separate span IDs, and each trace has the same shape it would record alone, so you can test either boundary of an agent on its own. Two overlapping roots therefore double span volume in the region they share, and each root applies its own limits, exclusions, and capture policy to its copy. A node() or withNode() configuration applies in every root’s copy, including testRunId and the finalized output, and finalize runs once per call; framework spans (the OpenAI Agents processor, the Vercel AI SDK middleware, the LangGraph integration) attach inside the innermost trace only. The outer trace’s span for the nested root carries nested_trace_id, nested_trace_function_key, and nested_root_span_id, and the nested root span carries enclosing_trace_id, enclosing_span_id, and enclosing_trace_function_key, so the two traces point at each other. In the trace viewer the enclosing trace’s span for the nested root shows a lip naming the nested trace function. It opens that trace at its root span. The nested trace’s root shows a lip back to the enclosing trace function that opens the span that started it.
Inside replay() or seedTrace() a nested root starts no trace of its own. The item’s trace records it as an ordinary descendant, so an experiment never gains traces under a second trace function key.

Mixing opt-in and opt-out tracing

withSpan() is the opt-in surface: it records exactly the functions you wrap. withTrace(), trace(), node(), and withNode() are the opt-out surface: they record a root plus the first-party calls beneath it. Pick one per workflow. Entering one beneath the other throws MixedTracingError naming both surfaces:
A blended stack produces a trace whose shape misrepresents how the code is instrumented, and the replay boundaries stop being predictable, so the SDK stops rather than recording it. To resolve it, pick one surface per workflow. Inside a subtree trace, reach for node() or withNode() when a discovered call needs its own name, type, capture, finalization, or replay-mocking policy: they configure the call the trace already owns rather than opening a second surface. Going the other way, wrap the caller with withTrace() too, or drop back to withSpan() throughout. Wanting a withSpan() root with a subtree lower down is really just a subtree with a smaller root. Move withTrace() to the function you wanted the subtree under. node() and withNode() follow the same rule. Beneath a withSpan() with no enclosing trace there is nothing for them to configure, so they throw rather than run as a silent no-op. With no tracing active at all they still run the function untouched. The root span that replay() and seedTrace() wrap around an undecorated callable is the one exception. It belongs to neither surface, so a withTrace() root called from that callable nests beneath it instead of throwing, matching Python.
The Python SDK raises MixedTracingError in the same places. Framework adapters are unaffected. The LangGraph, OpenAI Agents, and Vercel AI SDK wrappers open their spans on the surrounding surface rather than declaring one, and the callback-based handlers emit spans directly, so an adapter instrumenting part of your stack never trips the check.
Studio configuration is optional. In a trace’s header, choose Configure capture only when you want later traces to keep rich content for a narrower set of discovered functions. Running SDK processes refresh a confirmed policy in the background within about one minute; a trace snapshots its selection when it begins, so an in-flight trace never changes capture behavior halfway through. The policy request never blocks application code. If no confirmed policy exists, or the first policy request fails before any policy has loaded, full capture remains the default. Once a policy has loaded, refresh failures preserve it and retry later. Source-level exclude and capture: false opt-outs remain authoritative. Without a compatible transform, both root APIs still emit their normal rich root and run unchanged; automatic descendants are absent. Compatibility requires the adapter to see your original server-side TypeScript or JavaScript before decorators are lowered. Generator function definitions, JavaScript #private methods, getters/setters, dependency modules, declarations, "use client" modules, and Edge bundles are currently skipped. Async-generator roots preserve automatic context while their results are consumed, so eligible regular functions called from the generator body still appear.
Trace the whole workflow, not just its entrypoint. A single decorator or withSpan wrapper on the outer function records one input and one output for everything inside it, which leaves replay mocking, per-step diagnosis, and prompt iteration with nothing to work on. Spans exist only where you create them: nesting is automatic, but only between spans that exist.
Give a step its own span when any of these is true:
  • It calls a model. Always. This is the span you iterate on, compare across experiments, and attach graders to.
  • It reads external mutable state (DB query, HTTP GET, object storage, vector search, cache). These are the spans you will want to mock on replay.
  • It writes external state (DB write, queue publish, email, charge, file write). Mark these to mock on replay so a replayed trace does not repeat the side effect.
  • It transforms the model output (parsing, validation, ranking, formatting), so a quality regression points at the model or at your post-processing.
  • It retries or loops, one span per attempt or iteration, so a trace shows how many attempts it really took.
Skip trivial in-memory helpers, per-item work inside a large loop (wrap the loop or the batch), and internals a framework integration already captures. Worked examples, replay-mocking decisions, and common pitfalls: Instrumentation.

Declaring the trace function key

Declare the trace function key once and wrap multiple functions. This is the recommended form on every supported TypeScript version:
Calling tracedProcessOrder(id) records one trace with four spans:
Wrapping only processOrder would record the same work as a single node, with the model call, the database read, and the validation collapsed into the root’s input and output.

Multi-File Projects

For projects with instrumented functions spread across multiple files, create a dedicated file that initializes Bitfab and exports the function. Import it wherever you need to instrument.
Spans from different files are automatically linked as parent-child when one wrapped function calls another.

Wrapping Existing Functions Inline

When wrapping a function you didn’t define (e.g. an SDK or library call), pass it directly to withSpan and call the result immediately. This ensures the arguments are captured as span input.
Never wrap functions in an anonymous function like async () => fn(args). The SDK captures the wrapper function’s arguments as span input — an anonymous wrapper has no arguments, so the span records nothing.

Using withSpan() Directly

For a single span without linking to a function group:

Automatic Nesting

Spans nest automatically based on call stack:
For reusable helpers that should appear only inside an existing trace, use captureWhen: "nested":

Method Decorators (TypeScript 5+)

Decorators are an optional shorthand for class methods. withSpan() remains the recommended default because it works with every supported TypeScript version and every kind of callable. Requirements Use decorators only when all of these requirements are met:
  • The project compiles with TypeScript 5.0 or newer
  • Its compiler or transpiler supports the standard ECMAScript decorator transform
  • experimentalDecorators is disabled or omitted
  • emitDecoratorMetadata is disabled or omitted
The stable span() decorator does not support TypeScript’s older legacy decorator transform. Projects that use legacy decorators or emitted decorator metadata should continue using withSpan(). The experimental subtree trace() and node() decorators are separate APIs and support legacy method decorator output when @bitfab/transform runs first. Bind methods to one trace function Use getFunction() when several methods belong to the same traced workflow. Calls nest automatically when one decorated method calls another:
Calling new DocumentService().process(text) records one trace with process as the root and #normalize as a nested call. Decorate a method directly Use @bitfab.span(traceFunctionKey, options) when a class has only one method to trace or you do not need a key-bound getFunction() handle:
Supported methods and limitations Decorators work on instance, static, and private methods. Arguments, return values, errors, nesting, span options, and this are handled exactly as they are with withSpan(). Continue using withSpan() for:
  • Standalone functions
  • Class fields and accessors
  • Functions from other libraries
  • TypeScript 4.x projects
  • Projects using the legacy decorator transform
Projects using TypeScript 4.x can continue using every non-decorator API, including withSpan(), getFunction(), framework handlers, and replay. The published SDK declarations do not reference TypeScript 5-only global types. Only the @...span() syntax requires TypeScript 5.0 or newer.

Span Options

Parameters:
  • traceFunctionKey (required): String identifier for grouping spans
  • name (optional): Display name. Defaults to the function’s qualified name (Order.process for a method, process for a plain function), then the trace function key
  • type (optional): Span type. Defaults to "custom". A label only, used to organize and filter spans in the dashboard; it does not change how the span is traced, replayed, or evaluated
  • captureWhen (optional): "always" (default) or "nested". Nested-only spans are captured under an active parent and run untraced when called standalone. Unknown values warn once and default to "always"
  • testRunId (optional): Link the span and, when it is the root, its trace to a test run
  • mockOnReplay (optional): Return this call’s recorded output under the default mock: "marked" replay strategy
  • finalize (optional): (result) => serializableView. Record a serializable view of a non-serializable result (a live stream). See Tracing streaming functions
Span Types:
Examples:

Tracing Streaming Functions

A streaming function returns a live stream object that the caller consumes directly (an SSE response, a UI message stream). That object isn’t serializable as a trace output, and awaiting it to completion before returning would break streaming and first-byte latency. The finalize option solves this: withSpan hands the live stream back to the caller unchanged, but records await finalize(result) as the span output, a drained, serializable, replayable value such as { text, usage, toolCalls }. For the Vercel AI SDK, use the prebuilt finalizers.aiSdk helper. Reading the result’s text / totalUsage promises does not consume the live stream (the AI SDK tees internally), so your own streaming is unaffected. (Use both together: the middleware records the model call as a child span, finalizers.aiSdk records the streamed output on the root. The middleware alone, with no root, records one llm span per call and no workflow around it.)
The root and the model call share the chat-turn key, so the model span nests beneath the root:
Without wrapLanguageModel, runChatTurn records a single node: the model call happens inside the span but is not one, so there is no recorded prompt, no per-call token usage, and nothing to mock on replay. Provide your own finalize to record a specific shape:
For a raw ReadableStream, use finalizers.readableStream, which tee()s the stream and collects its chunks; the caller must use the live branch it hands back:
finalize runs in the background and never affects the caller’s value; a finalize that throws records an error on the span instead of crashing the host. It is ignored for async-generator results, which are captured automatically. Inputs to the wrapped function must still be serializable for the trace to replay. An async generator has two async chains: the service produces values and the controller consumes them. To include spans from both chains in one trace, make the controller the outer root and trace the generator as its child:

Span Context

Use getCurrentSpan() to get a handle to the active span, then call .addContext() to attach contextual key-value pairs from inside a traced function — useful for runtime values like request IDs, computed scores, or dynamic context:
Each addContext call pushes the entire object as one entry. Multiple calls accumulate entries:

Span IDs

Access the canonical Bitfab span and trace IDs from getCurrentSpan().id and getCurrentSpan().traceId. These are useful for persisted lookups, replay, or logging:
Outside a span context, both IDs are empty strings.

Span Prompt

Use getCurrentSpan() to set the prompt string on the current span. This is stored in span_data.prompt and is useful for capturing the exact prompt text sent to an LLM:
The prompt is metadata only. It records the prompt text for display and reference in the dashboard; it does not send the prompt to any model or change what the span executes. The last setPrompt call wins — it overwrites any previously set prompt on the span. Calling setPrompt outside a span context is a no-op (it never crashes).

Framework Integrations

Bitfab provides automatic tracing for popular AI frameworks. See the dedicated guides for full API references:

LangGraph / LangChain

Callback tracing plus Experimental (alpha) ToolNode output mocking for replay

OpenAI Agents SDK

Trace processor for agent runs

BAML

Auto-capture prompts and LLM metadata

Claude Agent SDK

Capture LLM turns, tool calls, and subagents

Vercel AI SDK

Language model middleware for every generateText / streamText call

Trace Context

Use getCurrentTrace() to set context that applies to the entire trace (all spans within a single execution). This is useful for grouping traces by session or attaching trace-level metadata:
  • setSessionId(id) — Groups traces by user session. Stored as a database column for efficient filtering.
  • setName(name) — The trace’s title in Bitfab, and a field you can search and filter on. Use it for the case, ticket, or record the run is about. Unset, the trace is titled by its trace function key.
  • setMetadata(obj) — Arbitrary key-value metadata on the trace. Merges with existing metadata.
  • addContext(obj) — Key-value context entries. Accumulates across multiple calls.

Dropping a Trace

Call .drop() on the current-trace handle to discard the in-flight trace. Once flagged, spans that complete afterward are not uploaded at all, and the flag rides out on the completion payload, so when the trace completes the server scrubs any payloads that already raced out (the trace, its external trace, and sibling spans), deletes the archived S3 objects, and marks it dropped instead of completed, keeping only a skeleton audit row. Use it to discard runs you never want stored (health checks, test traffic) or a run you know carries sensitive data.
  • Safe to call outside a trace (a no-op), and never throws into your application.

Detached Trace

Use client.getTrace(traceId) to get a handle to a trace that has already closed. This lets you add context, merge metadata, or set the session ID from any process, thread, or agent that knows the trace ID, with no shared in-memory state.
The traceId is Bitfab’s canonical trace ID, the same UUID exposed by getCurrentSpan().traceId for native SDK traces and used in Bitfab trace URLs. All methods block: each resolves once the server has applied the change and rejects if the server refused it, the same way getTraceSpan behaves. If you were ignoring the returned promise, start awaiting it - a rejected update (for example one naming a trace ID that does not exist) now surfaces to you instead of being logged and dropped.
  • addContext(context) — Appends a context entry. Existing entries are preserved.
  • setMetadata(metadata) — Shallow-merges new keys into existing metadata.
  • setSessionId(sessionId) — Replaces any existing session ID.

Read One Persisted Span

Use getTraceSpan to fetch one span without loading the full trace. Both the trace ID and exact span ID are canonical Bitfab IDs; ingestion source IDs are not accepted. Repeated name matches default to the last span.
occurrence also accepts a zero-based integer. A missing trace or span returns null.

Error Handling

Errors are captured in the span and re-raised:
Each error is classified by source. Errors thrown by your code are recorded with error_source: "code". SDK-internal errors (e.g. serialization failures) are recorded with source: "sdk". Both appear in the span’s errors array in the Bitfab dashboard.

Span Delivery

Spans are batched and delivered in the background, so tracing never sits on your request path. To make sure everything reached Bitfab before a script or test exits:
The return value is false when an export fails or the deadline expires. Traces also flush automatically on process exit via a beforeExit hook.

OpenTelemetry Transport

The SDK lazily creates one private OpenTelemetry provider and bounded BatchSpanProcessor per client. It does not replace your application’s global OTel provider, and an unused client starts no OTel worker. withSpan and every framework handler submit the same replay-safe Bitfab payloads through one transport interface. Batches are sent to Bitfab as OTLP/JSON. Each carrier is encoded once and the request body is assembled from those encodings, so a batch is never re-encoded to measure its size. Live and replay traces share the same OTel pipeline. Before completing a replay test run, the SDK flushes OTel and uses delivery acknowledgments to confirm every carrier that reached Bitfab. If any delivery is uncertain, it polls Bitfab until every submitted replay trace completion and expected span count is persisted. For the full ownership model, carrier format, live and replay flows, batching limits, lifecycle, and failure semantics, see OpenTelemetry Transport Architecture. The SDK partitions count-based OTel exports into requests of at most about 3 MB. Each request contains at most 128 carriers and is packed using their exact encoded size, and up to 32 are sent concurrently (BITFAB_OTEL_EXPORT_CONCURRENCY, 1-64). Set BITFAB_OTEL_MAX_REQUEST_BYTES to a positive integer no greater than 3000000 to use a smaller target for a stricter proxy; unsafe values warn and fall back to 3000000. An oversized carrier gets its own request and uses the compression and trimming fallback described below. If it still cannot fit or ingress rejects it with HTTP 413, the SDK reports an export failure. If Bitfab rejects malformed carriers from an otherwise valid direct batch, the standard OTLP partialSuccess response is logged with the rejected-span count and reason. A single span may use up to 7,800,000 carrier bytes when its dedicated request gzips below the 3,000,000-byte wire target and remains below the 8,000,000-byte decompressed ingress limit. The carrier is the payload re-escaped into the OTLP attribute. If it does not compress enough, compression is unavailable, or it exceeds the raw ceiling, the SDK replaces its largest fields with <unserializable: too_large_N_bytes> placeholders until it fits the 2,800,000-byte fallback budget. The trim is recorded on the span’s errors so the trace is flagged as incomplete. For transient clients in long-running processes, call client.close(30_000) when finished. Closing is idempotent: it flushes and shuts down only that client’s OTel worker, which is also reused by framework handlers created from that client. A shared application client can remain open and will still shut down automatically at process exit. Handlers you construct directly, without a Bitfab client, own their worker and expose their own close().

Content capture from the sim plan

The client reads your organization’s sim plan in the background and applies it to every span it sends. A span whose content is turned off in the plan still records its name, type, timing, errors, contexts, and links to other traces, but not its inputs and outputs, and it carries content_off_by_simulation_plan: true so Bitfab knows why the content is missing. The plan is matched by the trace’s root trace function key and the span’s name. The read starts as soon as you wrap a function or call getFunction, is given a five second timeout, and it never blocks a traced call. A span sent before the first read has succeeded is held back and sent once the plan arrives, with the plan applied, so no span ever leaves the process with content the plan turned off, not even the first one; held spans go out on flushTraces(), on close(), and at exit, as soon as the plan has loaded, and while records are held those paths wait up to the five second read timeout for the plan before giving up on them. At most 1,000 records are held per client, and past that the oldest are dropped with a one-time warning. A trace’s completion waits behind whichever of that trace’s spans are held, and goes out right away when none are, so a trace made only of framework spans is never held back. A failed read is retried every ten seconds while a record is held, and otherwise on the next span: before the first success that keeps spans held, after it the last plan stays in effect. The plan is refreshed about once a minute while spans flow, so a change takes effect within one refresh. Spans recorded by a framework integration (the OpenAI Agents processor, the LangGraph handler and integration, the Claude Agent SDK handler, the Vercel AI SDK middleware) always keep their content, and the plan cannot turn those off; every span carries a span_origin record (the SDK name and version, and instrumentation.name saying what recorded it). A server that has no sim plan feature answers the read with 404, which counts as an empty plan loaded: nothing is held and nothing is stripped. Pass simulationPlan: false to the client to turn the plan off entirely (no read, nothing held back, nothing stripped), the same as setting BITFAB_DISABLE_SIM_PLAN to any value that is not empty or whitespace. A browser deployment must proxy GET /api/sdk/sim-plan alongside the OTLP route, or pass simulationPlan: false.

Advanced Configuration

  • timeout: Request timeout in milliseconds for API calls. Defaults to 120000 (2 minutes).
  • envVars: Pass LLM provider API keys for native function execution via call().
  • enabled: When false, all tracing is disabled. Wrapped functions still execute normally but no spans are sent.
  • simulationPlan: When false, the sim plan is never read and content capture is never narrowed. See Content capture from the sim plan.
  • bamlClient: The generated BAML client instance (e.g., b from @baml). See BAML framework guide for full usage.

Replay

A trace is replayable when its root span has serializable inputs, or when the workflow is instrumented through a framework handler (whose recorded root input is itself serializable). One of these must hold for replay to work. Replay historical traces through an updated function version to compare outputs:
Pass replay() either an already-withSpan-wrapped function (it carries its trace function key, so replay() runs it as-is) or a plain callable (which replay() wraps under the key for you). Do not wrap an already-instrumented function in a fresh closure: a plain arrow like (input) => myWrappedFn(input) carries no trace function key, so replay() adds its own root span around it while myWrappedFn records its own span underneath, nesting a duplicate. If your root is already wrapped, pass it directly: bitfab.replay("my-function", myWrappedFn, ...).
Replay waits for each item’s trace (spans + completion) to be persisted server-side before completing the test run, so item.traceId is a real server trace ID for completed items. Plain callables are wrapped in withSpan internally, so every replayed invocation records a trace. If NO completed item’s trace persisted (uploads wholesale failed), replay() throws a BitfabError instead of silently returning null trace IDs. If only SOME items’ traces are missing (a transient per-item upload failure), those items get null trace IDs with a loud console.error and the rest of the run is returned intact. item.traceId is also null for errored (unreplayable) items, and for all items when the server predates the trace-ID mapping (a console warning explains which). Anything describing the trace being replayed carries the original prefix: originalDurationMs, originalModel, and originalTokens. Unprefixed fields are the replay’s own: durationMs is how long this run’s call took, and tokens is the replayed run’s usage (the same numbers Studio’s experiments view shows). Comparing tokens.total against originalTokens.total tells you how your change moved cost. Each field is null when it wasn’t captured. The same rule names the two trace outlines: originalTraceOutline is the original trace’s span tree and traceOutline is the replayed trace’s. An outline carries each span’s name, type, nesting, order, duration, tokens, model, errors, and whether it was mocked, and no inputs or outputs, so it is small enough to keep on every item. Both are filled in at completion (they are null in onItemFinish and against older servers) so a grader can compare the path the replay took against the original’s, not only its output. See TraceOutline in the TypeScript reference. model remains as a deprecated alias for originalModel. Note that durationMs changed meaning: it used to report the original trace’s duration and now reports the replay’s. Options:
  • limit — Maximum number of recent traces to replay (default: 5; maximum: 5,000). Ignored when traceIds, datasetId or datasetIds is passed, since an explicit ID list or a dataset already determines how many traces replay. To replay part of a dataset selection, name the members to run in traceIds.
  • traceIds — Specific trace IDs to replay (max 100). The ID count determines how many traces replay, and limit is ignored when both are passed. Passed alongside a dataset selector it pins which members of that selection replay, and the server rejects any ID none of those datasets contains.
  • name — Optional display name for the resulting experiment/test run.
  • maxConcurrency — Number of traces to replay in parallel (default: 10)
  • attempts — How many times to replay each trace (1 to 100). Every attempt is its own replay trace under the same experiment, so the experiment reports per-attempt pass rates and flags traces whose attempts disagree. Default: 1
  • codeChangeDescription — Optional rationale for the code change being tested in this replay (stored on the experiment); when supplied alone, it is preserved while files are captured automatically
  • codeChangeFiles — Optional list of edited files, each as { path, before, after } (use "" for newly created or deleted files); omit to capture automatically or pass null to suppress capture
  • mock — Mock strategy for child spans during replay: "marked" (default, only return historical output for child spans declared with mockOnReplay: true), "none" (run real code for every child), or "all" (return historical output for every matched recorded child; a missing occurrence fails the item closed). See Mocking child spans during replay below.
  • mockOverride — One { match, value } pair, or an array of them, that injects a custom value into matched spans (first matcher wins). Takes precedence over registerMockOverride and the base mock strategy. See Injecting custom values with overrides below.
  • experimentGroupId — Optional UUID string that groups multiple replay runs into a single experiment batch. Pass the same ID across successive replay() calls to link them together in the dashboard.
  • datasetId — Optional dataset UUID. Replays that dataset’s traces and durably attributes the resulting experiment to it, and limit is ignored because the dataset determines the item count. Pass traceIds alongside it to replay only those members.
  • datasetIds — Optional dataset UUIDs, for benchmarking one function against several corpora in a single run. Replays the union of their traces, graded by the union of their graders, and attributes the experiment to every one of them. Pass one dataset through datasetId and several through this.
  • graderIds — Optional array of grader UUIDs (max 100) attached directly to this replay run, independent of the dataset’s own graders. The resulting experiment is graded by the union of these and the dataset’s runnable graders. Use it to grade a single run with a check you don’t want to add to the dataset permanently. Each id must be an active grader in the same organization and trace function, or the replay is rejected with a 400. A replay with no dataset can still carry graders this way.
  • adaptInputs — Optional hook to reshape recorded inputs onto the function’s current signature when its shape changed after the traces were captured. See Adapting inputs after a signature change below.
  • onItemStart — Optional callback fired when a worker begins processing an item, before replay setup and customer code run. Pair it with onItemFinish to distinguish queued items from in-flight items whose callback has not returned. A throwing callback never crashes the run. The installed bitfab-replay command uses reportReplayProgress for both callbacks so liveness heartbeats identify the historical traces currently in flight.
  • onItemFinish — Optional callback fired exactly once per item as it finishes, always with that item plus running totals (original/source trace id, the server replay traceId read back per trace off the OTLP ingest response and surfaced as each item finishes (its trace is flushed on finish, so the id is in hand at the callback, not only after the whole run), input, result, original output, error, duration, tokens/model metadata). It never emits a whole-run completion event. Use it to render live progress or start evaluating completed items while replay runs. A throwing callback never crashes the run. The deprecated onProgress callback receives the same per-item events plus its legacy item-less terminal complete event, and is ignored when both are supplied. The installed bitfab-replay command uses the SDK’s ready-made reportReplayProgress callback for both lifecycle hooks; it writes events to stderr, which the plugin uses to identify in-flight traces, report finished items, and write per-item result files (stdout remains available for direct-run ReplayResult JSON).
  • dbBranch — Optional boolean | DbBranchOptions. dbBranch: true requests a DB branch per replay item with the mirror’s own sizing; pass an object to tune it, and false or omission leaves branching off. Each replay worker resolves its branch from the source trace’s captured snapshot reference, so maxConcurrency also bounds live branches. getCurrentReplayBranch() hands the branch to you inside the replayed function, and the SDK releases it after the item. The accessor returns null when no branch was resolved (e.g. the trace predates snapshot capture, or DB branching isn’t configured), so branch?.databaseUrl ?? process.env.DATABASE_URL falls back to your live database. The fields tune the branch: minCu and maxCu are the compute’s autoscaling floor and ceiling (0.25 to 56). Equal values pin a fixed size, allowed up to 56; an autoscaling range may not span more than 8 CU or exceed 16 CU; setting them equal pins the size, so one item can’t post a better number purely because it ran against an already-scaled endpoint. warmupSql is appended to the branch’s readiness check, so the cache is warm before your function sees the branch and the warm-up is never charged to the replayed call. Omit them and the branch keeps the mirror’s own defaults.

Replaying handler-instrumented functions

Workflows instrumented through a framework handler (getLangGraphCallbackHandler, getLangChainCallbackHandler, getClaudeAgentHandler, getOpenAiAgentHandler) have no withSpan-wrapped root in the application code: the handler (or run wrapper) records the framework invocation itself as the root span, with the framework’s own input (a LangGraph initial state, an agent prompt, the run input) as the recorded root input. LangGraph/LangChain roots are registered as pending traces when the root callback starts and completed when it ends, so long-running runs can appear before final output is available. These traces are fully replayable. Pass the handler’s trace function key plus any plain callable that re-invokes the framework entrypoint:
The OpenAI Agents SDK uses getOpenAiAgentHandler(key).wrapRun(agent, input) (a drop-in for run) for the replayable root; the bare getOpenAiTracingProcessor captures internals only and records an empty-input root. The Claude Agent SDK handler needs a hint: the prompt is not present in the message stream, so pass it explicitly, wrapQuery(stream, { input: prompt }) (or wrapResponse(stream, { input })), for the handler to record a replayable root.
How it fits together:
  • replay() fetches the handler-recorded production traces by the key string, and wraps a plain callable in withSpan under that key internally so each replayed invocation records a trace tied to the test run. The key is the only link; it does not matter that production traces were written by the handler and the callable was written today.
  • Passing an already-withSpan-wrapped function under the same key also works (older SDKs require this form); a wrapped function whose key contradicts the replay key throws.
  • The recorded root input is whatever the handler captured at the framework boundary (a LangGraph state object arrives as a single argument).
  • Attaching the handler inside the callable makes the replayed graph’s node/LLM/tool spans nest under the replay span, so replay traces have the same tree as production ones.
  • The callable rebuilds runtime wiring the trace never captured: framework configurable, dependency objects, API keys. Put every unsafe call made by that wiring behind a replay-mockable marked span. Use a no-op value only for a replay-only callback slot with no recorded call to mock.

Mocking child spans during replay

For the workflow-level guide, see Replay Mocking. When iterating on a root function, child spans sometimes fail in your local environment for reasons unrelated to the code under test: a paid API key is missing, an external service is flaky, or a production-only DB row isn’t seeded locally. The mock option lets the child return its recorded output so the root function can still run. Three strategies on replay():
  • "marked" (default): only descendants declared with mockOnReplay: true are short-circuited; everything else runs real. This is the iteration-friendly mode.
  • "none": every child span runs real code. Use only when you intentionally want every dependency real and have verified that is safe.
  • "all": every matched recorded descendant withSpan returns its historical output. The root function still runs real; a missing or exhausted child occurrence fails the item closed. Useful for a quick sanity-check against recorded data; not the recommended iteration strategy because changes to matched descendants won’t execute.
Per-span opt-in via SpanOptions.mockOnReplay:
mockOnReplay is a per-span tag at definition time — it has no effect outside replay, and it’s read by the default mock: "marked" strategy. The root function always runs real code; only descendants can be mocked. When a strategy selects a child for mocking but no historical occurrence is available (for example, the recorded trace did not reach that branch or the replay called it more times), the item errors and the real child does not execute.

Injecting custom values with overrides

Marking a span replays its recorded output. A mock override substitutes a value you supply for a matched span, so downstream real code runs against it — for “what if this step returned X” experiments without editing the traced code. An override is a { match, value } pair: match selects spans by structural metadata (spanName, type, traceFunctionKey, originalSpanId); value is the substitution (full replacement) — a flat value, or a function of the context. The trace function key passed to replay() selects the workflow’s historical root traces; it does not by itself identify the descendant to override. Because the example above binds every call to process-article, match the descendant by its span name. If your calls use separate trace function keys, matching node.traceFunctionKey is also valid.
value can also be a function receiving the span’s live replay inputs and a getOriginalOutput() that lazily fetches the recorded output (memoized per trace) when you want to tweak it rather than replace it:
A flat or synthetic value (one that never calls getOriginalOutput) fetches no recorded outputs at all. Register overrides on the client to apply them to every replay:
Use the keyed form when one registration belongs to a known trace function. The second argument can be either a resolver or another { match, value } pair; in the latter case both the key and match must match:
For one client-wide resolver, pass a function directly. It runs for every child span, so it can route by trace function key. Return NO_MOCK_OVERRIDE to decline a span without using undefined (which remains a valid mocked output):
Precedence per span: per-call mockOverride, then registered overrides, then the base mock strategy. NO_MOCK_OVERRIDE continues at the next override, then falls back to that base strategy. A synchronous wrapped function cannot wait for any Promise-returning resolver, including one that eventually resolves to NO_MOCK_OVERRIDE. For mixed sync/async trees, use a non-async routing function that returns NO_MOCK_OVERRIDE synchronously for sync keys and returns a Promise only for async keys. Likewise, because getOriginalOutput() is async, a synchronous span cannot use it; make the span async, or use mock: "all". A flat value or synchronous function works on synchronous spans.

Adapting inputs after a signature change

Replay deserializes each trace’s inputs exactly as they were captured against the function’s signature at trace time, then spreads them into the current function. If the signature drifted since capture (a param renamed, reordered, collapsed into an options object, or a new required arg added), that spread no longer lines up and the call throws. The adaptInputs hook reshapes the recorded inputs onto the current signature so replay can still run:
The hook runs once per item, inside the same error boundary as the function: if it throws, that item’s error is set and the run continues, so a single unmappable trace never crashes the batch. The array it returns is what gets spread into the function and what item.input reports. ctx carries { originalTraceId, originalSpanId } (with deprecated sourceTraceId/sourceSpanId aliases) so a table-driven adapter can look up a per-trace transform (ctx.originalTraceId is the original Bitfab trace ID). This is the escape hatch for reshapes that need judgement rather than mechanical rearrangement: compute the adapted inputs per trace up front, then have the hook look them up by sourceTraceId, keeping replay deterministic instead of calling a model mid-replay. When the new signature has a genuinely new required input with no analog in the recorded trace, don’t fabricate one — there’s nothing faithful to map it to. Leave those traces unmapped (let them error) rather than inventing test inputs. For anything beyond a one-liner, keep the adapter in its own file next to the replay registry and import it — the AdaptInputsFn type is exported for this:
That keeps the transform versioned and reviewable alongside the function it adapts, and you add the import only when a drift actually needs it.

Attaching a Code Change

Each replay creates an experiment (test run). When you’re iterating on a function and replaying after every edit, attach the change so the dashboard can show exactly what was edited alongside the results. The agent reads each file before editing, edits, then reads it again — the two strings go straight into codeChangeFiles. There’s no diff format to construct.
Both options are optional and independent — you can pass just codeChangeDescription for a quick rationale-only annotation, or just codeChangeFiles to record the literal edits. If you omit codeChangeFiles, replay() falls back to capturing your working-tree diff against the trunk merge-base (best-effort, only inside a git repo), so an experiment still shows a diff. This fallback uses Git rename detection: a renamed-and-edited file is compared once under its destination path, while an unchanged rename adds no content diff. A supplied codeChangeDescription is preserved while the files are captured. Passing codeChangeFiles explicitly always wins and is the way to record a precise per-edit before/after. To opt out for one replay run, pass codeChangeFiles: null (and optionally codeChangeDescription: null if you also want no description). Set BITFAB_DISABLE_CODE_CHANGE_CAPTURE to turn the fallback off for every replay in the process. Notes:
  • Use a single Bitfab client across instrumentation and replay. If your instrumented module constructs new Bitfab() at import and your replay registry constructs another, they do not share registered trace functions — import the client from the instrumented module (or a shared singleton) rather than constructing a new one in the registry.

Replay Output Contract

Replay results are typically consumed by automation (CI logs, code reviewers, and coding agents). When BITFAB_REPLAY_RESULT_PATH is set, bitfab.replay() automatically writes the full ReplayResult JSON to that file. For direct/manual runs, emit the full ReplayResult as a single stdout JSON block so a consumer can JSON.parse it and reason about every field, including the per-item originalDurationMs, originalTokens, originalModel, tokens, originalTraceOutline, and traceOutline. Never print only lengths, counts, hashes, or truncated previews, and never replace the JSON block with ad-hoc per-field log lines. Recommended script tail (TypeScript):
The dumped object includes every item’s input, result, originalOutput, error, structured traceError and replayError, durationMs, originalDurationMs, originalTokens, originalModel, tokens, model, dbBranchTimings, traceOutline, originalTraceOutline, and traceId, plus testRunId and testRunUrl. Import serializeReplayResult from @bitfab/sdk; raw JSON.stringify drops the useful fields on JavaScript Error objects. When the Bitfab plugin runs this script, it sets BITFAB_REPLAY_RESULT_PATH; the SDK writes the same structured JSON there, and the plugin reads that file into the replay run’s .bitfab/replays/<run-id>/events.jsonl while writing large per-item payloads under .bitfab/replays/<run-id>/items/. Per-item errors are part of the contract. If the wrapped function throws while executing the replayed trace, bitfab.replay retains the actual exception in item.traceError, copies its message to item.error, leaves item.result undefined, and continues. If replay setup fails before the function starts (for example database warmup or input loading), the actual exception is instead in item.replayError. A database branch resolution failure is a DbBranchReplayError; inspect its code, message, and originalTraceId to distinguish failures such as branch_create_failed, snapshot_from_replaced_origin, and invalid_snapshot_ref without parsing item.error. A lease-endpoint HTTP, timeout, or network failure uses lease_request_failed and retains the original client exception as cause; unexpected resolver failures use internal_error. Treat either error kind as unreplayable, not as a failing output. If the whole run later throws, ReplayError.items still contains every collected item and ReplayError.cause retains the whole-run exception. Don’t swallow per-item errors in the script. A custom try/catch that returns a placeholder turns infra failures into fake successes. Let the SDK record them. The only allowed top-level catch is a fatal handler around main() that exits non-zero, so callers can tell a whole-replay crash from a clean run with some unreplayable items. Input serialization caveat. Replay deserializes historical span inputs and passes them back to your function. This works for strings, numbers, and plain objects. If your span wraps a function that takes hydrated domain objects (ORM models, class instances, DB records), they won’t round-trip through serialization — move the span to where inputs are IDs or plain data and let the function fetch objects internally, or reshape arguments in the wrapper.

Replay Registry

Create a small registry module. Your project owns only the imports and the mapping from command names to the exact traced functions production calls. The SDK installs the standard bitfab-replay executable, so upgrading @bitfab/sdk updates argument parsing, progress events, code-change loading, result serialization, and summary output without regenerating project code.
Keep non-trivial executable configuration in a sibling module and import it into the registry. For example, replayMocks.ts can export a mock factory whose { match, value } result uses live replay inputs and caller-supplied parameters. optionsFactory receives JSON values loaded from --params <file> and repeated --param name=value; direct parameters override file values. Run the executable with bitfab-replay --registry scripts/replayRegistry.ts <pipeline>. It loads TypeScript registry modules directly. Put static function-specific behavior such as adaptInputs, mockOverride, or dbBranch in options; build parameterized behavior in optionsFactory. Command-line values override overlapping scalar defaults without removing unrelated executable options.

Seeding traces

Replay needs a trace to replay. Until production has produced one, there is nothing to select, so a corpus you already hold (a dataset export, a spreadsheet, hand-written cases) cannot be run. seedTrace writes that corpus into Bitfab as replayable traces and returns each trace ID. It has two forms, chosen by the second argument. Pass a function to run it once and record the execution. Pass a case to write a trace without running anything.

Seed by running the function once

This runs runTicket("T-1") once and records the execution as an original trace: root span, first-party subtree, the real input, and whatever the run produced as the output. Capture stays off for everything else, so a seeding script does not have to run with tracing on for the rest of the process. The trace lands under agent-turn with ingestion_type: seeded, replay selects it like any captured trace, and every replay of it links back as originalTraceId. Because it has a full recorded subtree, replay mocking works on it exactly as it does on a captured trace. fn resolves the same way it does for replay. A withSpan-wrapped function records under its own key, which must match the key you pass, and a plain callable is wrapped under the key here. An exception is recorded on the root span, the trace still persists, and the exception is re-thrown. A call that records nothing (no API key resolved) rejects rather than handing back an ID replay could never find.

Seed from a case without running

The recorded root span carries input as its input and expected as its output, so a later replay reports the item against the value you expected rather than against a previous run. Passing fn checks the case against the function’s required argument count, so a case that could never run fails while you seed instead of during replay. Omit it when the seeding script cannot import the callable. A case-seeded trace has no child spans, so replay mocking has nothing recorded to substitute. Supply mockOverride at replay time for calls that must not run for real. Neither seeding form pins a database, so dbBranch refuses a seeded trace.

Options both forms share

name, metadata, and sessionId apply to either form. name is the trace’s title and a searchable, filterable field. Put the case’s own label there (a ticket ID, a dataset row name) so the seeded trace can be found by it. metadata is stored on the trace and handed to a later replay’s adaptInputs hook as ctx.metadata, so a case’s provenance rides with the trace instead of through the recorded inputs. In the run form, if the function also emits its own trace through an integration that exports trace metadata, the caller’s metadata is merged onto that export and wins on any shared key. The case form additionally takes spanName and spanType, which label and type the root span it writes. The run form has no equivalent, since the executed function names its own root span.

Re-seeding a trace

A trace whose recorded run is wrong (it errored, or the world it ran against has moved on) can be re-seeded. reseedTrace reads the trace’s recorded inputs, name, session, and metadata, runs the function once the way seedTrace does, and asks Bitfab to adopt that run under the same trace id.
The trace keeps its id, labels, assertions, dataset membership, name, and metadata, so anything you stored against it still names the same case. The previous run is kept as its own trace, previousRunTraceId, with reseedOfTraceId pointing back at the case. Nothing is mocked and no experiment is created; a re-seed is a seed, not a replay. A run that throws is recorded but never adopted, so the trace is untouched, and Bitfab rejects a run that comes from another function or already belongs to a dataset. Graders on the datasets holding the trace re-run afterwards, and default replay selection skips previous runs. From the shell, bitfab-seed <pipeline> --from-trace <id>[,<id>...] does the same through the replay registry:
A re-seed runs the function exactly as production does, side effects included, so treat it like running the code, not like a replay.

Seeding a whole cases file

seedFromRegistry seeds through an already-registered pipeline, reusing its client, callable, and trace function key, so every case is written against the exact function the later replay selects. The installed command does the same from the shell:
Each line of cases.jsonl is a JSON object with an input array plus optional expected, metadata, and sessionId. A JSON array of those objects works too. With --run, the output is what the run produced, so a case carrying expected is rejected. The registration’s adaptInputs is a replay hook and is not applied at seed time, so a seeded trace is never adapted twice.

Replaying seeded traces

replay selects seeded traces the same way it selects captured ones, and each item reports its source’s ingestionType (a source with none reads as captured). The bitfab-replay summary counts a seeded item as matched or missed against its expected value rather than same or changed against a previous run, because a seeded trace’s recorded output is an assertion and not a prior run’s result. One run can replay both kinds, so both pairs of counts print when both are present. See the reference for full signatures.

Datasets

client.datasets creates, reads, and modifies datasets programmatically, with the same operations your coding agent reaches through the Bitfab MCP tools. A dataset is a named bucket of traces under one trace function. Experiments replay against it and its graders score its members.
save is an upsert on the dataset name within its trace function, so re-running a script does not accumulate duplicates. Membership and grader calls accept up to 100 ids and report ids they skipped rather than failing the whole call. removeTraces only drops membership. Traces are never deleted. rerunGraders waits for the run by default (90 seconds, configurable) and returns whatever state it last saw. Pass wait: false to return immediately and poll with getGraderRerun. See the reference for every method and result type.

Labels

client.labels writes pass/fail verdicts and reads them back, the same operations your coding agent reaches through the save_agent_labels, save_human_labels, and get_trace_labels MCP tools. A verdict says how a run that already happened turned out. Written per assertion, it is stored and read back per assertion, so a judge inside a replay process can score each expectation on its own and verify what landed without opening Studio.
save and saveAll write the agent’s verdicts, which start unapproved until a person approves them in Studio. saveHuman and saveHumanAll write verdicts that are validated on write, for cases a person has already decided, such as a production bug captured as a regression test. Both batches are all-or-nothing: a trace outside the organization, a repeated target, or an assertion that is not active on its trace rejects the call and writes nothing. get and getAll return each trace’s effective verdict plus one row per scored assertion. graders.getLabels is the per-grader breakdown the effective verdict folds together. Approving a verdict is not on this surface, or on MCP, by design. See the reference for every method and type.