Framework-native adapters (LangGraph, OpenAI Agents, BAML, Claude Agent SDK) are not yet available for Ruby. See Frameworks overview for current coverage. Instrument Ruby code manually via
Bitfab::Traceable or Bitfab.span.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 instrumented methods 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 whenconfigure is called. Resolution order is the configured value (a Proc is called while still unresolved), then a fallback read of ENV["BITFAB_API_KEY"].
strict:
Tracing
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.
Custom (Recommended)
Using Bitfab::Traceable to Link Spans
Include Bitfab::Traceable in a class, declare the trace function key once with bitfab_function, then use bitfab_span to wrap methods. Three declaration styles are supported:
process_order(id) records one trace with four spans:
process_order 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.
All three styles are equivalent. The before-def style is recommended for readability.
Multi-File Projects
For projects with instrumented methods spread across multiple files, create an initializer that configures Bitfab, then includeBitfab::Traceable in any class that needs tracing.
bitfab_function key are grouped together. Spans from different classes are automatically linked as parent-child when one instrumented method calls another.
Using bitfab_span with Explicit Key
For a single span with an explicit trace function key:
Automatic Nesting
Spans nest automatically based on call stack:capture_when: "nested":
Span Options
Parameters:method_name(required): Symbol of the method to wraptrace_function_key(optional): Override class-levelbitfab_functionname(optional): Display name. Defaults to method nametype(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 evaluatedcapture_when(optional):"always"/:always(default) or"nested"/:nested. Nested-only spans are captured under an active parent and run untraced when called standalone. Unknown values warn once and default to"always"
Span Context
UseBitfab.current_span to get a handle to the active span, then call .add_context() to attach contextual key-value pairs from inside a traced method, useful for runtime values like request IDs, computed scores, or dynamic context:
add_context call pushes the entire hash as one entry. Multiple calls accumulate entries:
Bitfab.current_span.id and Bitfab.current_span.trace_id (both return an empty string outside a span):
Span Prompt
UseBitfab.current_span 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:
set_prompt call wins — it overwrites any previously set prompt on the span. Calling set_prompt outside a span context is a no-op (it never crashes).
Trace Context
UseBitfab.current_trace 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:
set_session_id(id): Groups traces by user session. Stored as a database column for efficient filtering.set_name(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.set_metadata(hash): Arbitrary key-value metadata on the trace. Merges with existing metadata.add_context(hash): Key-value context entries. Accumulates across multiple calls.
Read One Persisted Span
Fetch one span without loading the full trace. Repeated name matches default to the last span.occurrence also accepts a zero-based integer. A missing trace or span returns nil.
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 raises into your application.
Error Handling
Errors are captured in the span and re-raised:error_source: "code". SDK-internal errors are recorded with source: "sdk". Both appear in the span’s errors array in the Bitfab dashboard.
Flushing Traces
false when an export fails or the deadline expires.
Traces flush automatically on process exit via at_exit hook.
OpenTelemetry Transport
The Ruby 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. Traced methods
submit the same replay-safe Bitfab payloads through a 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.
The SDK partitions count-based OTel exports into requests of at most about
3 MB. Each request contains at most eight carriers and up to 32 run
concurrently. Set BITFAB_OTEL_MAX_REQUEST_BYTES (a positive integer no greater
than 3000000) for a stricter proxy, and BITFAB_OTEL_EXPORT_CONCURRENCY (1
through 64) to tune request concurrency. Invalid values warn once and fall
back to the defaults.
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.
The queue holds 8,192 carriers (a span and its trace completion are two), and a
burst that queues faster than the exporter drains loses the excess rather than
growing your process’s memory. Bitfab.flush_traces reports on what reached
the exporter, so it returns true even when the queue had to drop carriers;
keep bursts inside that capacity when every span matters.
A long-running process that builds transient clients should release each
client’s batch worker when it is done with it; a shared client is closed at
process exit.
Wrapping Third-Party Methods
UseBitfab::Traceable.wrap to trace methods on external classes:
Replay
A trace is replayable when its root span has serializable inputs (the recorded inputs must round-trip throughto_json). Framework handlers are not yet available for Ruby, so serializable root inputs are the only path to a replayable trace; instrument the outer workflow method so its inputs serialize.
Replay historical traces through a method to create test runs. This re-runs past inputs through your updated code and compares the results.
:trace_id is a real server trace ID for completed items. If NO completed item’s trace persisted (uploads wholesale failed, or the replayed method isn’t traced), replay raises a RuntimeError instead of silently returning nil trace IDs. If only SOME items’ traces are missing (a transient per-item upload failure), those items get nil trace IDs with a loud warning and the rest of the run is returned intact. :trace_id is also nil for errored (unreplayable) items, and for all items when the server predates the trace-ID mapping (a warning explains which).
Anything describing the trace being replayed carries the original_ prefix: :original_duration_ms, :original_model, and :original_tokens. Unprefixed fields are the replay’s own: :duration_ms 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 :original_tokens[:total] tells you how your change moved cost. Each field is nil when it wasn’t captured.
The same rule names the two trace outlines: :original_trace_outline is the original trace’s span tree and :trace_outline 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 nil in on_item_finish and against older servers) so a grader can compare the path the replay took against the original’s, not only its output. The shape is documented in the Ruby reference.
:model remains as a deprecated alias for :original_model. Note that :duration_ms changed meaning: it used to report the original trace’s duration and now reports the replay’s.
Parameters:
receiver(required): An instance for instance methods, or a Class for class methodsmethod_name(required): Symbol of the method to replaytrace_function_key(required): The trace function keylimit(optional): Max recent traces to replay (default: 5; maximum: 5,000). Ignored whentrace_ids,dataset_idordataset_idsis 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 intrace_ids.trace_ids(optional): Array of specific trace IDs to replay (max 100). The ID count determines how many traces replay, andlimitis 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.max_concurrency(optional): Max threads for parallel replay (default: 10)code_change_description(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 automaticallycode_change_files(optional): Array of edited files, each as{ path:, before:, after: }(use""for newly created or deleted files); omit to capture automatically or passnilto suppress capturemock(optional): Mock strategy for child spans during replay. One of"marked"(default, only spans tagged withmock_on_replay: truereturn historical output),"none"(every child runs real code), or"all"(every matched recorded child returns its historical output; a missing occurrence fails the item closed)mock_override(optional): One{ match:, value: }hash, or an array of them, that substitutes a supplied output for matched spans. Per-call overrides take precedence over registered overrides and the basemockstrategy.experiment_group_id(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.dataset_id(optional): UUID of the dataset this replay runs against, durably attributing the experiment to that dataset. Passtrace_idsalongside it to replay only those members.dataset_ids(optional): UUIDs of the datasets this replay runs against, for benchmarking one method 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.grader_ids(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 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.adapt_inputs(optional): A callable->(args, kwargs, ctx)that reshapes recorded inputs onto the method’s current signature when its shape changed after the traces were captured. See Adapting inputs after a signature change below.on_item_start(optional): A callable fired when a worker begins processing an item, before replay setup and customer code run. Pair it withon_item_finishto distinguish queued items from in-flight items whose callback has not returned. A raising callback never crashes the run.on_item_finish(optional): A callable->(progress)fired exactly once per item as it finishes, always with that item plus running totals (original/source trace id, the server replay:trace_idread 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 raising callback never crashes the run. The deprecatedon_progresscallback receives the same per-item events plus its legacy item-less terminalcompleteevent, and is ignored when both are supplied. The installedbitfab-replaycommand passesBitfab.method(:report_replay_progress)as bothon_item_startandon_item_finish; it writes lifecycle events to stderr, which the plugin uses to identify in-flight traces, report finished items, and write per-item result files while stdout remains available for direct-runReplayResultJSON.db_branch(optional):trueor aHash.db_branch: truerequests a DB branch per replay item with the mirror’s own sizing; pass a hash to tune it, andfalseornilleaves branching off. Each replay worker resolves its branch from the source trace’s captured snapshot reference, somax_concurrencyalso bounds live branches.Bitfab.current_replay_branchhands the branch to you inside the replayed method, and the SDK releases it after the item. The reader returnsnilwhen no branch was resolved (e.g. the trace predates snapshot capture, or DB branching isn’t configured), sobranch ? branch.database_url : ENV["DATABASE_URL"]falls back to your live database. The keys tune the branch, symbol or string:min_cuandmax_cuare 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.warmup_sqlis appended to the branch’s readiness check, so the cache is warm before your method 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.
receiver+method_namemust resolve to the method that carries thetraceabledecoration. Passing a plain wrapper around it will not resolve the trace function key.trace_function_keymust match the method’s declared key. It is read from thebitfab_span/Bitfab::Traceable.wrapdeclaration on the method you point at. If the key you pass contradicts that declared key,replayraises anArgumentError: it would otherwise fetch one function’s historical traces but record the replay under the method’s own key, producing an incoherent test run. (Ruby has no plain-callable replay form, so there is no reason to pass a non-matching key.)trace_function_keyis passed explicitly, so instance methods and class methods are disambiguated by thereceiver.- Use a single
Bitfab::Clientacross instrumentation and replay. If your instrumented module constructs a client at load 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.
code_change_files. There’s no diff format to construct.
code_change_description for a quick rationale-only annotation, or just code_change_files to record the literal edits.
If you omit code_change_files, 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 code_change_description is preserved while the files are captured. Passing code_change_files explicitly always wins and is the way to record a precise per-edit before/after. To opt out for one replay run, pass code_change_files: nil (and optionally code_change_description: nil if you also want no description). Set BITFAB_DISABLE_CODE_CHANGE_CAPTURE to turn the fallback off for every replay in the process.
Mock child spans during replay
For the workflow-level guide, see Replay Mocking. By default replay uses"marked": child spans tagged with mock_on_replay: true return their historical outputs, while every other child runs real code. Three mock strategies control this behavior:
Traced methods that return an Enumerator preserve their parent span and replay context inside same-thread Enumerator.new / enum_for source fibers, so marked descendants remain mockable while the stream is consumed. Child threads and processes do not inherit replay context.
mock: "marked" behavior when you want to iterate on process_order’s logic without paying for the LLM call on each replay. Use mock: "all" when the goal is the cheapest possible replay (every matched recorded child span returns its recorded output; only the root function executes real code).
Calls are matched by trace function key and span name. Repeated calls with the same key and name are distinguished by call order, so their first, second, and third invocations receive the corresponding historical outputs. Calls that share a trace function key but have different span names are tracked independently.
If a strategy selects a call for mocking but its recorded occurrence is missing or exhausted, the item errors and the real method does not execute.
Injecting custom values with overrides
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: } hash: match is a callable selecting spans by structural metadata (node[:trace_function_key], node[:span_name], node[:type], node[:original_span_id]); value is a flat value injected as-is, or a callable that returns one.
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 bitfab_function "order-processing" binds every method in this example to the same key, match classify_intent by its span name.
value receives a context hash with the live positional :inputs, the live keyword :kwargs (empty when the call used none), and :get_original_output (synchronous in Ruby; its first call may fetch the recorded output lazily) to tweak the recorded output instead of replacing it:
clear_mock_overrides:
{ match:, value: }; for an override hash, both the registered trace function key and its match predicate must match:
Bitfab::NO_MOCK_OVERRIDE to decline a span; nil remains a valid mocked output:
mock_override:, then registered overrides, then the base mock: strategy. Bitfab::NO_MOCK_OVERRIDE continues at the next override, then falls back to that base strategy. Pass a single override hash, resolver, or array.
Fluent API: client.get_function
Bind a trace_function_key once and wrap multiple classes or methods against it. Mirrors client.get_function in the Python SDK and client.getFunction in TypeScript.
#wrap accepts the same options as Bitfab::Traceable.wrap (name, type, mock_on_replay), but the trace_function_key is fixed to the one bound on the returned Bitfab::BitfabFunction.
Adapting inputs after a signature change
Replay deserializes each trace’s inputs exactly as they were captured against the method’s signature at trace time, then calls the current method with them. If the signature drifted since capture (an argument renamed, reordered, folded into a hash, or a new required argument added), the call no longer lines up and raises. Theadapt_inputs hook reshapes the recorded inputs onto the current signature so replay can still run:
(args, kwargs) plus a per-trace ctx ({ original_trace_id:, original_span_id: }, with deprecated source_* aliases) and returns [new_args, new_kwargs]. The returned args is what item[:input] reports. It runs once per item, inside the same rescue as the method: if it raises, that item’s :error is set and the run continues, so one unmappable trace never crashes the batch.
ctx[:original_trace_id] (the original Bitfab trace ID) lets a table-driven adapter look up a per-trace transform. That’s 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 original_trace_id, keeping replay deterministic instead of calling a model mid-replay.
When the new signature has a genuinely new required argument with no analog in the recorded trace, don’t fabricate one. There’s nothing faithful to map it to, so leave those traces unmapped (let them raise) rather than inventing test inputs.
For anything beyond a one-liner, keep the adapter in its own file next to the replay registry and require it:
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 replay result JSON to that file. For direct/manual runs, emit the full replay result hash as a single stdout JSON block so a consumer can JSON.parse it and reason about every field, including the new per-item :duration_ms, :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:
:input, :result, :original_output, :error, structured :trace_error and :replay_error, :duration_ms, :tokens, :model, and :trace_id, plus :test_run_id and :test_run_url. Use Bitfab.serialize_replay_result; JSON.pretty_generate(result) raises when a result contains an exception object. 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 method raises while executing the replayed trace, Bitfab.replay retains the actual exception in item[:trace_error], copies its message to item[:error], leaves item[:result] as nil, and continues. If replay setup fails before the method starts (for example database warmup or input loading), the actual exception is instead in item[:replay_error]. A database branch resolution failure is a Bitfab::DbBranchReplayError; inspect its code, message, and original_trace_id 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 raises, Bitfab::ReplayError#items still contains every collected item and cause retains the whole-run exception.
Don’t swallow per-item errors in the script. A custom begin/rescue that returns a placeholder turns infra failures into fake successes. Let the SDK record them. The only allowed top-level rescue 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 method. This works for strings, numbers, and plain hashes. If your span wraps a method that takes hydrated domain objects (ActiveRecord 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 method fetch objects internally, or reshape arguments in the wrapper.
Replay Registry
Create a small registry module. Your project owns only the requires and registrations; installing the gem also installs the standardbitfab-replay executable, which owns command-line flags, lifecycle progress, code-change loading, result serialization, and summary output.
replay_mocks.rb can define a mock factory whose { match:, value: } result uses live replay inputs and caller-supplied parameters. options_factory: receives JSON values loaded from --params <file> and repeated --param name=value; direct parameters override file values.
Run the executable with bundle exec bitfab-replay --registry scripts/replay_registry.rb <pipeline>. The registry module must define REGISTRY.
Pass static function-specific behavior such as
adapt_inputs:, mock_override:, or db_branch: to register; build parameterized behavior with options_factory:. Command-line values override overlapping scalar defaults without removing unrelated executable options. Unknown registry option names fail when the module loads instead of reaching replay as misspelled keywords.
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. remove_traces only drops membership. Traces are never deleted. rerun_graders 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 get_grader_rerun. See the reference for every method and result shape.