Available in the TypeScript, Python, Ruby, and Go SDKs. Go mocking applies to closure-style
Client.Span calls, which own execution; manual Start/End spans cannot prevent caller-owned code from running.When to use it
Use replay mocking when a child span is not the thing you are trying to improve:- An unsafe side effect must not execute again, such as sending an email, charging a card, publishing to a queue, or writing to a database.
- An LLM call is expensive, slow, or rate-limited.
- An external API is flaky or needs credentials you do not have locally.
- A database read depends on production-only rows.
- A retrieval or preprocessing step should stay fixed while you iterate on later logic.
How it works
Every replay has a mock strategy. If you omit the option, Bitfab uses"marked".
The root function always runs real code. Mocking only applies to descendant spans.
Python async-generator spans are not mockable yet. When
mock="all", mock="marked" selects one, or an override matches, the replay item fails without iterating the real generator. Move unsafe work inside the generator to a mockable sync or coroutine descendant.threading.Thread need Bitfab(trace_across_threads=True); pre-created consumers and other processes are outside that propagation. Ruby replay context is thread-local, so move work dispatched to another thread/process behind a boundary that runs in the replay thread. Traced Ruby methods that return an Enumerator bridge their parent span and replay context into same-thread Enumerator.new / enum_for source fibers. In TypeScript, a synchronous span cannot consume the lazy output used by mock: "marked"; use an async/Promise-returning boundary, or eager mock: "all" only when freezing every matched recorded child is the intended experiment.
Opt a trace into descendant mocking
Subtree tracing can opt into descendant-default mocking without changing the global replay strategy. Set the trace option totrue, then annotate only the
code that should keep running with a node-level false.
applyNewPolicy runs because its @node opts out. Without
mockOnReplayDefault: true,
subtree mocking remains opt-in exactly as before. Python exposes the analogous
@trace(..., mock_on_replay_default=True) option, but only configured @node()
descendants can inherit it because interpreter monitoring cannot skip an
unwrapped call. Replay mock: "all" retains its original all-descendants meaning.
Mark child spans
Client.Span returns any, and recorded JSON objects otherwise decode as map[string]any. Add WithMockOutputType[T]() when a mocked child returns a struct, slice, or other concrete type; the SDK decodes the substituted output into T before returning it.
Replay with marked mocks
fetch-article-from-db returns the output recorded in each historical trace. summarize-article runs your current code, so the experiment tests the code you are changing.
What Bitfab matches
Bitfab matches mocked child calls against the recorded span tree for each source trace. Repeated calls to the same span are matched by call order, so the first call gets the first historical output, the second call gets the second, and so on. Current SDKs emit strictly increasing microsecond timestamps so rapid sibling calls retain that order; the service falls back to ingestion order and span ID for legacy traces whose timestamps tie exactly. If a strategy selects a child for mocking but its recorded occurrence is missing or exhausted, Bitfab fails that replay item without executing the real child. Unselected calls still run real code.During replay,
mockOverride / mock_override / MockOverrides checks each non-root call that passes through a Bitfab span wrapper (withSpan, @span, bitfab_span, or Go’s closure-style Client.Span). It does not iterate over trace-plan nodes or framework spans that Bitfab only observes.Inject custom values (overrides)
Mock overrides are available in the TypeScript, Python, Ruby, and Go SDKs.
match + value pair. match selects which calls it applies to (by span name, type, trace function key, or any structural field); value is the substitution — a flat value used as-is, or a function that produces one. Full replacement: value becomes the call’s output.
The trace function key passed to replay() selects the workflow’s historical root traces; it does not by itself identify the descendant to override. In this example every call is bound to process-article, so the override matches the descendant by its span name. If your calls use separate trace function keys, a matcher can select by that field instead.
value is a function it receives the span’s live replay inputs, plus get_original_output to read the recorded output when you want to tweak it rather than replace it wholesale. In the Python and Ruby SDKs the context also carries the live keyword args (ctx.kwargs / ctx[:kwargs]) alongside the positional inputs:
value (one that never calls get_original_output) fetches no recorded output at all. Pass a single override or an array (first matcher wins). To apply overrides to every replay on a client, register them:
mockOverride / mock_override option. Precedence for each span: a per-call override wins, then a registered override, then the base mock strategy. The sentinel declines only the current resolver, so lower-priority overrides still get a chance before the base strategy.
In the TypeScript SDK, a synchronous wrapped function cannot wait for any Promise-returning resolver, including one that eventually resolves to
NO_MOCK_OVERRIDE. For a global resolver used across mixed sync/async spans, use a non-async routing function: return NO_MOCK_OVERRIDE synchronously for sync keys, and return a Promise only for async keys. getOriginalOutput() also fetches asynchronously, so a synchronous span cannot use it; make the span async, or use mock: "all". A flat value or synchronous function works on synchronous spans. Python, Ruby, and Go read the recorded output synchronously, so this restriction does not apply there; Go’s fetch blocks only that replay worker.Parameterize registry mocks
Keep reusable mock construction in the replay registry instead of adding application-specific flags to the SDK command. Supply JSON values with--params scenario.json and override individual values with repeatable --param name=value. Values that parse as JSON retain their type; other values remain strings.
Common patterns
Keep expensive model calls fixed
Mark a paid classification or extraction span withmockOnReplay / mock_on_replay, then iterate on the decision logic that consumes its output.
Stabilize infrastructure while changing prompts
Mock data loading, retrieval, or external tools, then keep the prompt or formatter real. This isolates prompt changes from local setup problems.Pair with database branching
Replay mocking is about replacing child function outputs. Database branching is about replaying against the database state that existed when the trace was captured. Use both when a replay needs production-like state and selected child calls should still be short-circuited.Mocking a seeded trace
A seeded trace is one written from a case you already held rather than captured from production. What mocking can do with it depends on how it was seeded.
For a case-seeded trace, use overrides to supply values for calls that must not run for real. An override injects a value you write, so it does not need a recorded output to work from.
Neither kind of seeded trace carries a database pin, so
dbBranch / db_branch refuses it rather than silently pinning the wrong moment.