Installation
Quick Start
Coding Agent Prompt (Cursor, Claude Code)
Coding Agent Prompt (Cursor, Claude Code)
Copy this prompt into your coding agent (tested with Cursor and Claude Code using Sonnet 4.5):
Basic Configuration
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, animport 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.
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:
node --env-file=.env script.ts, so every module-level read sees the key.
Tracing
Custom (Recommended)
Using getFunction() to Link Spans
Declare the trace function key once and wrap multiple functions:
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.Wrapping Existing Functions Inline
When wrapping a function you didn’t define (e.g. an SDK or library call), pass it directly towithSpan and call the result immediately. This ensures the arguments are captured as span input.
Using withSpan() Directly
For a single span without linking to a function group:
Automatic Nesting
Spans nest automatically based on call stack:Span Options
Parameters:traceFunctionKey(required): String identifier for grouping spansname(optional): Display name. Defaults to function name, then trace function keytype(optional): Span type. Defaults to"custom"finalize(optional):(result) => serializableView. Record a serializable view of a non-serializable result (a live stream). See Tracing streaming functions
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. Thefinalize 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. (For automatic per-call llm spans with no withSpan at all, see the Vercel AI SDK framework integration; finalizers.aiSdk is for an explicit root span around the call.)
finalize to record a specific shape:
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.
Span Context
UsegetCurrentSpan() 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:
addContext call pushes the entire object as one entry. Multiple calls accumulate entries:
Span Trace ID
Access the current trace ID from within a span usinggetCurrentSpan().traceId. This is useful for capturing trace IDs to use with replay or for logging:
getCurrentSpan().traceId returns an empty string.
Span Prompt
UsegetCurrentSpan() 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:
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 handler that records a replayable framework root plus graph nodes, LLM calls, tools, and retrievers
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
UsegetCurrentTrace() 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.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
Useclient.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.
traceId must be the caller-supplied ID used when the trace was originally created. All methods are fire-and-forget (return Promises you may await or ignore). Pending requests are tracked so flushTraces() waits for them.
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.
Error Handling
Errors are captured in the span and re-raised: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.
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 viacall().enabled: Whenfalse, all tracing is disabled. Wrapped functions still execute normally but no spans are sent.bamlClient: The generated BAML client instance (e.g.,bfrom@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: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).
Per-item durationMs and model come from the historical trace that fed this replay item. tokens is the replayed run’s token usage (the same numbers Studio’s experiments view shows), so comparing each item’s tokens.total against the original trace’s recorded usage tells you how your change moved cost. Each field is null when it wasn’t captured.
Options:
limit— Maximum number of traces to replay (default: 5). Ignored whentraceIdsis passed (with a warning): an explicit ID list already determines how many traces replay.traceIds— Specific trace IDs to replay (max 100). The ID count determines how many traces replay;limitis ignored when both are passed.name— Optional display name for the resulting experiment/test run.maxConcurrency— Number of traces to replay in parallel (default: 10)codeChangeDescription— Optional rationale for the code change being tested in this replay (stored on the experiment)codeChangeFiles— Optional list of edited files, each as{ path, before, after }(use""for newly created or deleted files)mock— Mock strategy for child spans during replay:"marked"(default, only return historical output for child spans declared withmockOnReplay: true),"none"(run real code for every child), or"all"(return historical output for every child). See Mocking child spans during replay below.experimentGroupId— Optional UUID string that groups multiple replay runs into a single experiment batch. Pass the same ID across successivereplay()calls to link them together in the dashboard.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.onProgress— Optional callback fired once per item as it settles, with running totals plus the settled item payload (source trace id, local replay trace id, input, result, original output, error, duration, tokens/model metadata). Use it to render live progress or start evaluating completed items while replay runs. A throwing callback never crashes the run. Bitfab plugin replay scripts can pass the SDK’s ready-madereportReplayProgresscallback straight in (onProgress: reportReplayProgress); it writes the event to stderr, which the Bitfab plugin polls to report live progress and write per-item result files while replay runs (stdout remains available for direct-run ReplayResult JSON).environment— OptionalReplayEnvironment. When passed, the Bitfab server resolves a per-trace database branch from each source trace’s captured snapshot reference, and the SDK exposes that branch’s URL viaenvironment.databaseUrlinside the replayed function (releasing the branch after each item). Readenvironment.activeto fall back to your live database when no branch was resolved (e.g. the trace predates snapshot capture, or DB branching isn’t configured). Construct one withnew ReplayEnvironment()and read it only inside the replayed function.
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.replay()fetches the handler-recorded production traces by the key string, and wraps a plain callable inwithSpanunder 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 the runtime environment the trace never captured: framework
configurable, dependency objects, API keys. Use safe no-op substitutes for side-effectful wiring (billing or credit callbacks, notification senders); replay should never charge or notify anyone.
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. Themock option lets the child return its recorded output so the root function can still run.
Three strategies on replay():
"marked"(default): only descendants declared withmockOnReplay: trueare short-circuited; everything else runs real. This is the iteration-friendly mode."none": every child span runs real code. Use when your local environment can faithfully reproduce the trace."all": every descendantwithSpanreturns its historical output. The root function still runs real, but every child is short-circuited. Useful for a quick sanity-check against recorded data; not the recommended iteration strategy because changes to descendants won’t actually execute.
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 no historical span matches a child call (e.g. the recorded trace didn’t reach that branch), execution falls through to the real function — never silent omission.
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. TheadaptInputs hook reshapes the recorded inputs onto the current signature so replay can still run:
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 { traceId, sourceSpanId } so a table-driven adapter can look up a per-trace transform (ctx.traceId 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 traceId — 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 script and import it — the AdaptInputsFn type is exported for this:
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 intocodeChangeFiles. There’s no diff format to construct.
codeChangeDescription for a quick rationale-only annotation, or just codeChangeFiles to record the literal edits.
Notes:
- Use a single
Bitfabclient across instrumentation and replay. If your instrumented module constructsnew Bitfab()at import and your replay script 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 replay script.
Replay Output Contract
Replay results are typically consumed by automation (CI logs, code reviewers, and coding agents). WhenBITFAB_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 new per-item durationMs, tokens, and model. 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):
input, result, originalOutput, error, durationMs, tokens, model, and traceId, plus testRunId and testRunUrl. When the Bitfab plugin runs this script, it sets BITFAB_REPLAY_RESULT_PATH; the SDK writes the final result 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 on a given trace, bitfab.replay catches it, sets item.error, leaves item.result undefined, and continues. Treat items with item.error set as unreplayable, not as failing outputs — compute pass/fail only over items where it’s unset. This matters most for DB reads/writes: a stale FK, missing record, or rejected write is infra failure, not a regression.
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.
Environment. Replay executes in the app’s own process — the instrumented function is imported as a library, and its DB clients, env vars, config loaders, and model IDs resolve from whatever environment the replay script is run under. The script must bootstrap the same environment the app uses (e.g. import "dotenv/config" at the top, or run via pnpm with-env tsx scripts/replay.ts). Do not mock these — they’re the same dependencies the app resolves in production. For replay to see the same DB rows the trace was captured against, point the script at the trace’s source environment (the environment field on the trace — production / staging / development).
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.