Skip to main content
The gem is called bitfab. It requires Ruby 3.4 or newer. It uses a global singleton client pattern (Bitfab.configure / Bitfab.client).

Framework Integrations

No framework-native adapters are shipped for Ruby yet. Instrument Ruby code manually via Bitfab::Traceable, Bitfab::Traceable.wrap, or a bound Bitfab::BitfabFunction. See the Ruby SDK guide. Framework coverage status lives in the Frameworks overview.

Module Layout

Module-Level API

Bitfab.configure(api_key: nil, service_url: nil, enabled: true, strict: false)

Creates the global client. Subsequent calls replace it. The key is resolved lazily, at the first traced call. If you passed a Proc, it is called at that point. A missing or empty configured value falls back to ENV["BITFAB_API_KEY"]. In normal mode, a missing key warns. It also disables tracing. With strict: true, a missing key raises instead. enabled: false disables tracing outright. It does not resolve or warn about the key at all.

Bitfab.client

Returns the configured Bitfab::Client. Raises RuntimeError("Bitfab not configured...") if configure has not been called.

Bitfab.client_or_nil

Returns the configured Bitfab::Client, or nil if configure has not been called. The traced-call path for bitfab_span resolves the client this way, not through Bitfab.client. As a result, a method invoked before Bitfab.configure runs untraced instead of raising.

Bitfab.reset!

Resets the global client to nil. Intended for tests.

Bitfab.current_span

Returns the active CurrentSpan, or Bitfab::NO_OP_SPAN outside a span context.

Bitfab.current_trace

Returns the active CurrentTrace, or Bitfab::NO_OP_TRACE outside a span context.

Bitfab.current_replay_branch

Returns the Bitfab::ReplayBranch resolved for the replay item currently running. Returns nil outside a replay item. It also returns nil when the item’s source trace carried no DB snapshot reference. Call it inside the replayed method, then fall back to your normal connection string:
The branch is a frozen, per-item value object. It exposes database_url, neon_branch_id, env_key, expires_at, snapshot_timestamp, provider_console_url, read_only, region, and trace_id. It is built from the thread-local replay context, so parallel items each see their own branch. Reading database_url marks the trace as having used the branch, reported as accessed. The other readers inspect the branch without exposing the connection string, and deliberately leave that mark untouched. inspect redacts the URL. Every field the service puts on the lease gets a reader under its snake_case name. A field added server-side is readable before you upgrade the SDK. database_url is the one exception. It carries the credential. It is also the only member whose read marks the branch as accessed. as_json and to_json serialize only those exposed fields. Without this, ActiveSupport would serialize the object’s instance variables by default. That would put the connection string into any Rails log line or API response that touched a branch. Because of this override, a serialized branch is safe to hand around. database_url still stays available to code that explicitly asks for it.

Bitfab.report_replay_progress(progress)

A ready-made replay lifecycle callback. Pass it into replay as both on_item_start: and on_item_finish:. It writes lifecycle events to stderr, prefixed with Bitfab::BITFAB_PROGRESS_PREFIX. The Bitfab plugin reads those events to identify in-flight traces and finished results. stdout stays free for direct-run ReplayResult JSON. It never raises.

Bitfab.serialize_replay_result(result)

Returns indented JSON. It preserves structured fields from :trace_error and :replay_error, including Bitfab::DbBranchReplayError#code, original_trace_id, and nested cause. Use it for direct-run stdout instead of JSON.pretty_generate(result). JSON.pretty_generate(result) raises when a result contains an exception object.

Bitfab.flush_traces(timeout: 30)

Waits for queued spans and trace completions from every live client to reach the server within one total deadline. Returns true when everything landed and false on delivery failure or timeout. It does not close the clients.

class Bitfab::Client

SPAN_TYPES

Readable client state

api_key does not apply the ENV["BITFAB_API_KEY"] fallback. It also never warns. enabled reports the effective state. Reading it resolves the key, the same way the first traced call would. service_url and datasets are attribute readers.

get_trace_span

Fetches one persisted span without loading its trace. trace_id is the canonical Bitfab trace ID. Exactly one of the span’s Bitfab id or name is required. occurrence accepts "first", "last", or a zero-based integer. It defaults to "last". Returns nil when no trace or span matches.

datasets

Dataset operations for the authenticated organization. These are the same operations the Bitfab MCP tools expose to a coding agent. Every method returns the parsed JSON response as a Hash with string keys matching the HTTP API ("traceCount", "addedTraceIds", and so on).
  • save is an upsert keyed on (trace_function_key, name). "created" is true for a new dataset. It is false when an existing one was updated instead. A nil description leaves an existing description untouched.
  • list takes an optional trace function key. Without it, every dataset in the organization is returned.
  • A dataset carries "id", "traceFunctionKey", "name", "description", "traceCount", "graders" ([{"id", "name"}]), "createdAt", and "updatedAt".
  • list_traces returns {"datasetId", "traceIds"}. This is the same membership a replay with dataset_id: selects.
  • add_traces and add_graders accept 1 to 100 ids. They report partial acceptance instead of rejecting the whole call. Ids outside the organization, or under another trace function, come back in "skippedTraceIds" / "skippedGraderIds". Ids already present come back in "alreadyPresentTraceIds" / "alreadyAssignedGraderIds".
  • remove_traces never deletes a trace, only its membership in the dataset. Ids that were not members come back in "notPresentTraceIds". remove_graders reports "notAssignedGraderIds" the same way.
  • rerun_graders re-scores every trace in the dataset. grader_ids: defaults to every assigned grader. An unassigned id rejects the call. It waits up to timeout: seconds, polling every poll_interval: seconds. It returns the last "run" seen. Pass wait: false to return as soon as the run is queued. A request matching an in-flight run joins it instead, reporting "joinedExisting" as true.
  • A run has "status" (pending | running | completed | errored), "graderIds", "progress" ({completedTraces, totalTraces, graderCount} while running), "result" ({tracesGraded, gradersRun} when completed), and "error".
  • A dataset id from another organization raises Net::HTTPError with a 404.

initialize(api_key: nil, service_url: nil, enabled: true, strict: false)

Same semantics as Bitfab.configure.

flush(timeout: 30)

Waits for the spans and traces this client queued to be delivered, without closing it. Returns true when everything landed inside the deadline.

close(timeout: 30)

Flushes, then permanently shuts down this client’s OpenTelemetry batch worker. Returns true when everything queued was delivered inside the deadline. Later submissions on the closed client are dropped with a one-time warning. A shared client does not need this. at_exit already shuts remaining transports down.

Replay registry and command

Bitfab::ReplayRegistry#register stores the exact production receiver method. It also stores static per-function replay behavior and an optional options_factory:. A bitfab_span method supplies its trace function key automatically. A plain handler method must pass trace_function_key: explicitly. Pass client: to route this entry’s replay through a specific client. It defaults to Bitfab.client. Unlike Traceable.wrap’s client:, which resolves on every call, this one resolves once, when register is called. The factory receives JSON values from --params and --param. It can use them to construct executable options such as mock_override:. A direct --param value overrides one loaded from a --params file. Registered and factory-produced options are limited to these names:
  • limit, trace_ids, name, max_concurrency
  • code_change_description, code_change_files
  • experiment_group_id, dataset_id, dataset_ids, grader_ids
  • mock, mock_override, adapt_inputs, db_branch
An unknown option name is rejected, either when the registry loads or when the factory runs. Lifecycle callbacks are excluded from this list because the installed command owns progress reporting. The gem installs the bitfab-replay executable. Run it as bundle exec bitfab-replay --registry <path> <pipeline> [options]. The registry module must define REGISTRY.

register_mock_override

Registers an instance-scoped override for every subsequent replay. A keyed resolver runs only for child spans with that trace function key. A keyed { match:, value: } hash also applies its own matcher. A global resolver can route on ctx[:node][:trace_function_key]. Return Bitfab::NO_MOCK_OVERRIDE to continue to lower-priority overrides and the base mock strategy. nil remains a valid mocked output. Per-call overrides take precedence over registered overrides.

clear_mock_overrides

Removes every override registered on this client.

replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil, max_concurrency: 10, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, mock: "marked", mock_override: nil, adapt_inputs: nil, db_branch: nil, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)

Returns: a Hash with the keys :items, :test_run_id, and :test_run_url. Each item in :items carries these fields:
  • :input, :result, :original_output, :error, :trace_error, :replay_error
  • :duration_ms (Integer, nil), this replay’s own duration
  • :original_duration_ms, :original_tokens, :original_model
  • :db_branch_timings (Hash, nil)
  • :tokens ({ input:, output:, cached:, total: } or nil)
  • :model (String, nil)
  • :trace_id, :original_trace_id, :original_span_id (each String, nil)
  • the deprecated aliases :source_trace_id and :source_span_id
  • :db_snapshot_ref (Hash, nil), the source trace’s snapshot pin, if any
  • :trace_outline and :original_trace_outline (Hash, nil)
:trace_outline and :original_trace_outline are the replayed and the original trace’s span trees, with no inputs or outputs. The server passes them through as-is, with string camelCase keys, in the shape of the traceOutlines entries in the HTTP reference. Both are nil on progress items and against older servers. :trace_outline is also nil when the replay produced no trace. :trace_error retains the actual exception raised while executing the replayed trace. :replay_error retains the actual exception raised before the method could even run, such as a database warmup or input-loading failure. :error holds the compatible message for either one. A database branch resolver failure surfaces as a Bitfab::DbBranchReplayError, carrying code, the server message, and original_trace_id. This is true both in progress JSON and in Bitfab::ReplayError#items. HTTP, timeout, and network failures use the code lease_request_failed, with the original client exception as cause. Malformed refs use invalid_snapshot_ref. Unexpected resolver failures use internal_error. :duration_ms is how long this replay took. :tokens is the replayed run’s own token usage. The trace being replayed is described separately, by :original_duration_ms, :original_tokens, and :original_model. :model is a deprecated alias for :original_model. Compare :tokens against :original_tokens to see the cost delta. :trace_id is the new replay trace’s server id. It becomes available to on_item_finish once that item’s trace has been flushed. It stays nil if delivery could not be confirmed, or against an older server. :original_trace_id and :original_span_id identify the original trace and root span being replayed. If trace delivery or finalization fails after items settle, replay raises Bitfab::ReplayError. Its items, test_run_id, and test_run_url retain the partial result. Its cause retains the original whole-run exception. Mock strategies:
  • "marked": only child spans declared with mock_on_replay: true return historical output. If a selected occurrence is unavailable, the item errors without executing the real child. Default.
  • "none": every child span runs real code.
  • "all": every matched recorded child span returns its historical output. Only the root runs real code. A missing or exhausted occurrence fails the item closed.
Child calls are matched by trace_function_key and span name. Repeated calls with the same key and name are distinguished by call order. Calls that share a trace function key but have different span names are tracked independently.

Replay auto-capture environment variables

get_function(trace_function_key)

Returns a Bitfab::BitfabFunction bound to trace_function_key. Mirrors client.get_function in the Python SDK and client.getFunction in TypeScript.

node(klass, method_name, name: nil, type: "custom", capture: true, test_run_id: nil, mock_on_replay: false, finalize: nil)

Client-bound external-class form of bitfab_node. The method is configured only beneath an active bitfab_trace owned by this client and otherwise runs unchanged.

execute_span(...) (internal)

Called by Traceable. Not intended for direct use.

Transport environment variables

Commit ref environment variables

CommitRef

The commit the traced code was running at, sent as commit_ref on every root trace completion. sha is the commit. branch is the checked-out branch, or nil when detached or unknown. dirty is true when the working tree had uncommitted or untracked changes, false when it was clean, and nil when the SDK could not tell, which is always the case when the ref came from environment variables rather than git. remote is the origin URL reduced to host/owner/repo with any credentials removed, so a CI checkout token never reaches the trace. root_sha is the repository’s first commit, so two checkouts of the same repository match even without a remote. Resolution order is BITFAB_COMMIT_SHA, then the deploy platform’s build variables, then git in the process’s working directory (see Commit ref environment variables). Environment resolution is synchronous and free. The git path runs once per process on a background thread, with a two second timeout per command, so it never sits on the code path that ran the traced function. A trace that completes before it lands ships without a commit_ref, and a process with neither variables nor a repository never sends one. The result, including a negative one, is memoized for the life of the process. Set BITFAB_DISABLE_COMMIT_REF to opt the process out entirely. Bitfab::CommitRef.current returns the hash, or nil while unresolved.

class Bitfab::BitfabFunction

Fluent wrapper bound to a single trace_function_key, returned by Bitfab::Client#get_function.

attr_reader :trace_function_key

wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)

Wraps method_name on klass with span tracing under the bound trace_function_key. Delegates to Bitfab::Traceable.wrap.

trace(klass, method_name, name: nil, type: "custom", max_depth: 30, max_spans: 500, exclude: [], include_wrappers: false)

Experimental. Wraps method_name as the root and captures its first-party Ruby call subtree via TracePoint. Delegates to Bitfab::Traceable.trace; the bound key is used for the root and every captured call.

module Bitfab::Traceable

Mixin for declarative instance-method tracing.

include Bitfab::Traceable

Extends the including class with ClassMethods below.

Class Methods (after include)

bitfab_function(key)

Sets the default trace_function_key for all bitfab_span declarations in this class.

bitfab_span(method_name, trace_function_key: nil, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)

Wraps method_name with span tracing. Spans exist only where you create them. Nesting is automatic, but only between spans that already exist. One wrapper around the outermost function records a single-node trace. See Instrumentation. Supports three call styles:
  1. Before def: registered via method_added hook, wrapped when method is defined
  2. Inline: bitfab_span def foo ... end, type: "function" (Ruby’s def returns :foo)
  3. After def: wraps immediately if the method already exists
Raises RuntimeError if no trace_function_key is provided and no class-level bitfab_function was set.

bitfab_trace(method_name, trace_function_key: nil, name: nil, type: "custom", max_depth: 30, max_spans: 500, exclude: [], include_wrappers: false)

Experimental. Records the root method and every first-party Ruby method it calls, at any depth, without declaring those methods separately. The root uses name and type; descendants use their method names and type "function". Capture is active only while the root runs, including while a returned Enumerator is consumed. It follows recursive, module, inherited, define_method, singleton-class, cross-file, and same-thread child Fiber calls. An alias is named and matched by exclude using the invoked alias rather than its original definition name. Ruby standard-library, dependency-gem, Bitfab SDK, synthetic dynamic-evaluation, and child-thread methods are excluded. An installed gem containing the traced root is treated as first-party, without admitting neighboring gems or SDK internals. Blocks and lambdas are not method-call events and are not recorded. Methods whose only parameters are *args, **kwargs, and &block are treated as forwarding decorator wrappers and skipped unless include_wrappers: true. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits through Ruby Fiber storage, with separate call stacks. Pre-existing Fibers and Fiber.new(storage: nil) do not inherit this context. After an inner capture ends, child Fibers retain any still-active outer capture, with their existing parenting and depth limits. Inherited context expires once all enclosing captures end. While a root remains active, hitting max_spans stops new capture but allows already-open spans in every Fiber to finish. Independently traced Fibers remain isolated during interleaving, including when one root finishes before another. When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and Subtree capture ended before the call returned as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use. A returned Enumerator defers capture completion until iteration finishes or raises. Calls already paused in child Fibers retain their inputs, parenting, node context, depth and span-budget state across that handoff. Between the initial return and consumption, the session is inactive and its deferred frames are detached from the thread dispatcher. Calls still unfinished when iteration ends receive the incomplete-capture error described above. An Enumerator that is never consumed remains pending; the SDK does not force iteration. Automatically captured methods that return Enumerators record a <unserializable: Enumerator (lazy_enumerator)> output placeholder without consuming them; the traced root records the values yielded during consumption. A nested bitfab_trace root with a different trace function key starts an independent trace while its complete subtree also appears in every outer bitfab_trace capture. The traces have distinct trace and span IDs, including during replay, and the same structure each root records alone. Two active roots therefore double span volume in their shared region. A recursive invocation or nested annotation with the same key stays inside the active trace instead of starting another root. max_depth and max_spans cap descendant capture; hitting either warns once per trace function key. exclude contains unqualified method names as strings or symbols. Automatically captured spans are observations, not replay mock targets. A descendant declared with bitfab_span records exactly once through its explicit wrapper, remains replay-mockable, and parents automatically captured calls beneath it. The SDK prepares the project boundary and exclusions when the method is declared, refreshes the boundary if hot reload moves the root definition, caches canonical source paths, and shares one TracePoint dispatcher across active roots on a calling thread. After max_spans is exhausted, it stops collecting metadata and values immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call. Keep this experimental API for discovery; prefer explicit bitfab_span declarations on production hot paths. Supports the same before-def, inline, and after-def declaration styles as bitfab_span:
The wrapper preserves the root method’s public, protected, or private visibility.

bitfab_node(method_name, name: nil, type: "custom", capture: true, test_run_id: nil, mock_on_replay: false, finalize: nil)

Experimental. Configures a method only when it is discovered beneath an active bitfab_trace. It never creates a standalone span or trace and inherits the active root’s trace function key, limits, parent, and lifecycle. Captured nodes count toward the enclosing root’s max_depth and max_spans. A root-level exclude wins over the node configuration for that root. In different-key overlapping roots, each root remains independent: the innermost owner uses the configured node span while outer roots keep their automatically captured copy. capture: false applies to every active root because it is an unconditional property of the method. capture: false, mock_on_replay: true raises ArgumentError because an omitted node has no recorded output. Supports the same before-def, inline, and after-def declaration styles as bitfab_span and preserves public, protected, or private visibility.

Module-Level Wrapping

Bitfab::Traceable.wrap(klass, method_name, trace_function_key:, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, client: nil)

Wraps a method on a class you do not own. Uses Module#prepend.

Bitfab::Traceable.trace_function_key_for(receiver, method_name)

Returns the trace function key carried by a method wrapped with bitfab_span / Traceable.wrap, or nil when the receiver method has no Bitfab wrapper. Replay and replay registries use it to reject key mismatches.

Bitfab::Traceable.trace(klass, method_name, trace_function_key:, name: nil, type: "custom", max_depth: 30, max_spans: 500, exclude: [], include_wrappers: false)

Experimental external-class form of bitfab_trace. Uses Module#prepend; its options have the same semantics as the class macro.

Bitfab::Traceable.node(klass, method_name, name: nil, type: "custom", capture: true, test_run_id: nil, mock_on_replay: false, finalize: nil)

Experimental external-class form of bitfab_node. Uses Module#prepend; pass client: internally or prefer client.node(...) when configuration must belong to a specific client.

class Bitfab::CurrentSpan

class Bitfab::CurrentTrace

All methods swallow exceptions internally. They never raise. drop flags the trace to be dropped. Once flagged, spans that complete afterward are not uploaded at all. The flag itself still rides out on the completion payload. At completion, the server scrubs any payloads that already raced out, meaning the trace itself, its external trace, and any sibling spans. It deletes the archived S3 objects. It marks the trace dropped instead of completed, keeping only a skeleton audit row. Calling drop outside a span, on NO_OP_TRACE, is a no-op.

class Bitfab::NoOpCurrentSpan / NoOpCurrentTrace

Singleton instances exposed as Bitfab::NO_OP_SPAN and Bitfab::NO_OP_TRACE. All methods are no-ops. id and trace_id return "".

Thread & Concurrency Model

  • Span stack is stored in fiber-local Thread.current[:__bitfab_span_stack]. It is not propagated to child threads. Traced methods that return an Enumerator bridge their parent span into the same-thread Enumerator.new / enum_for source fiber. This keeps nested spans and replay mocks as descendants while the stream is consumed.
  • Replay context lives in true thread-variables instead (Thread.current.thread_variable_get / thread_variable_set). This context includes the mock tree, the DB branch lease, and the test run id. Unlike the fiber-local span stack, it persists across every fiber on the same thread. It still never propagates to child threads or forked processes.
  • Trace state is stored in a mutex-protected module-level hash (Bitfab::TraceState), keyed by trace_id.
  • Spans and trace completions are queued on the client’s private OpenTelemetry BatchSpanProcessor. They are delivered by its batch worker. Every other API call is blocking.
  • Bitfab.flush_traces(timeout:) waits for every live client’s queued spans. It returns false when delivery fails or the deadline expires. client.close(timeout:) shuts a single client’s worker down permanently.
  • Subtree capture reuses one TracePoint dispatcher per calling thread and keeps a separate method stack per fiber and active different-key root. Nested roots share the dispatcher, but every root still filters and serializes its own copy of overlapping calls, so capture work grows with the number of overlapping roots. Same-key nested annotations and recursion stay in the active session. Calls made on child threads are not captured.

Error Behavior Summary