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
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:
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: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"
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.trace_id (returns 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_metadata(hash): Arbitrary key-value metadata on the trace. Merges with existing metadata.add_context(hash): 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 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
at_exit hook.
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).
Per-item :duration_ms and :model come from the historical trace that fed the 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 nil when it wasn’t captured.
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 traces to replay (default: 5). Ignored whentrace_idsis passed (with a warning): an explicit ID list already determines how many traces replay.trace_ids(optional): Array of 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.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)code_change_files(optional): Array of edited files, each as{ path:, before:, after: }(use""for newly created or deleted files)mock(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 child returns its historical output)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.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_progress(optional): A callable->(progress)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 raising callback never crashes the run. Bitfab plugin replay scripts can pass the SDK’s ready-made reporter straight in (on_progress: Bitfab.method(:report_replay_progress)); 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(optional): ABitfab::ReplayEnvironment. 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.database_urlinside the replayed method (releasing the branch after each item). Readenvironment.active?to 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 withBitfab::ReplayEnvironment.newand read it only inside the replayed method.
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 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.
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.
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:
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 child span returns its recorded output; only the root function executes real code).
Repeated calls to the same trace_function_key are distinguished by call order, so step:0, step:1, step:2 correspond to the first, second, and third invocations. Unmarked spans still advance the counter, so a marked sibling that runs after an unmarked one lines up with the right historical entry.
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 ({ trace_id:, source_span_id: }) 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[: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 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 script 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, :duration_ms, :tokens, :model, and :trace_id, plus :test_run_id and :test_run_url. 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 method raises on a given trace, Bitfab.replay rescues it, sets item[:error], leaves item[:result] as nil, and continues. Treat items with item[:error] set as unreplayable, not as failing outputs; compute pass/fail only over items where it’s nil. 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 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.
Environment. Replay executes in the app’s own process: the instrumented method is loaded 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. require "dotenv/load" at the top, or run via bundle exec dotenv ruby scripts/replay.rb). 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 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.