Bitfab Ruby SDK

Ruby client library for Bitfab - trace and monitor your Ruby application's function execution with nested span support.

Installation

Add to your Gemfile:

gem 'bitfab'

Or install directly:

gem install bitfab

Requirements

  • Ruby >= 3.4
  • No external runtime dependencies (uses stdlib only)

Quick Start

require 'bitfab'

# Configure once at application startup
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY')
)

# Add tracing to your classes
class OrderService
  include Bitfab::Traceable
  bitfab_function "order-processing"

  bitfab_span :process_order, type: "function"
  def process_order(order_id)
    # Your code here
    { status: "completed" }
  end
end

# Use your code normally - spans are sent automatically
service = OrderService.new
service.process_order("order-123")

# Flush traces before exit (automatic via at_exit hook)
Bitfab.flush_traces

Configuration

Basic Configuration

Bitfab.configure(
  api_key: "your-api-key"
)

Custom Service URL

Bitfab.configure(
  api_key: "your-api-key",
  service_url: "https://custom.example.com"
)

Disabling Span Sending

The enabled option controls whether spans are sent to Bitfab. When disabled, your code executes normally but no spans are created or sent.

# Disable tracing (useful for development/test environments)
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: false
)

Common patterns:

# Rails: Enable only in production
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: Rails.env.production?
)

# Environment variable control
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: ENV.fetch('BITFAB_ENABLED', 'false') == 'true'
)

# Multi-environment control
Bitfab.configure(
  api_key: ENV.fetch('BITFAB_API_KEY', 'dummy-key'),
  enabled: ['production', 'staging'].include?(ENV['RACK_ENV'])
)

When enabled: false:

  • ✅ Code executes normally with no performance impact
  • ✅ Return values and errors work as expected
  • ✅ Nested spans are properly skipped
  • ❌ No HTTP requests are made
  • ❌ No span data is collected or sent

Usage

Class-Level Trace Function Key

All spans in the class share the same trace function key:

class PaymentService
  include Bitfab::Traceable
  bitfab_function "payment-processing"

  bitfab_span :charge_card, type: "function"
  def charge_card(amount)
    # Traced automatically
  end

  bitfab_span :refund, type: "function"
  def refund(transaction_id)
    # Also uses "payment-processing" key
  end
end

Per-Span Trace Function Key

Each span can declare its own key:

class NotificationService
  include Bitfab::Traceable

  bitfab_span :send_email, trace_function_key: "email-notifications", type: "function"
  def send_email(to, subject)
    # Uses "email-notifications" key
  end

  bitfab_span :send_sms, trace_function_key: "sms-notifications", type: "function"
  def send_sms(to, message)
    # Uses "sms-notifications" key
  end
end

Span Types

Bitfab supports the following span types:

  • "llm" - LLM API calls
  • "agent" - Agent decision loops
  • "function" - Business logic functions
  • "guardrail" - Validation and safety checks
  • "handoff" - Human-in-the-loop interactions
  • "custom" - Custom span types (default)
bitfab_span :validate_input, type: "guardrail"
bitfab_span :call_openai, type: "llm"
bitfab_span :agent_loop, type: "agent"

Custom Span Names

By default, the method name is used as the span name. Override it:

bitfab_span :process_order, name: "ProcessOrderV2", type: "function"
def process_order(order_id)
  # Span will be named "ProcessOrderV2"
end

Nested Spans

Spans automatically track parent-child relationships:

class OrderPipeline
  include Bitfab::Traceable
  bitfab_function "order-pipeline"

  bitfab_span :process, type: "function"
  def process(order_id)
    validate(order_id)
    # More processing
  end

  bitfab_span :validate, type: "guardrail"
  def validate(order_id)
    check_fraud(order_id)
    # More validation
  end

  bitfab_span :check_fraud, type: "guardrail"
  def check_fraud(order_id)
    # Fraud checking logic
  end
end

# Creates 3 nested spans:
# process (parent)
#   └─ validate (child)
#       └─ check_fraud (grandchild)

Input/Output Capture

Positional and keyword arguments are automatically captured:

bitfab_span :process_order, type: "function"
def process_order(order_id, priority: :normal)
  # Input captured: { "order_id" => "123", "priority" => "normal" }
  { status: "completed" }
  # Output captured: { "status" => "completed" }
end

Metadata

Add custom metadata to spans:

# Definition-time metadata
bitfab_span :process_order,
  type: "function",
  metadata: { "region" => "us-east", "version" => "v2" }
def process_order(order_id)
  # ...
end

# Runtime metadata (inside a span)
bitfab_span :process_order, type: "function"
def process_order(order_id)
  Bitfab.current_span.(
    "user_id" => current_user.id,
    "request_id" => request.id
  )
  # Runtime metadata merges with definition-time metadata
end

Wrapping External Code

Wrap third-party library methods without modifying them:

class ExternalHttpClient
  def get(url)
    # Third-party code
  end
end

# Add tracing via wrap
Bitfab::Traceable.wrap(
  ExternalHttpClient,
  :get,
  trace_function_key: "http-client",
  type: "function"
)

# Now traced automatically
client = ExternalHttpClient.new
client.get("https://api.example.com")

Fluent API: client.get_function

Bind a trace_function_key once and wrap multiple methods or classes against it. Mirrors client.get_function in the Python SDK and client.getFunction in TypeScript.

fn = Bitfab.client.get_function("openai")

fn.wrap(OpenAI::Client, :chat, name: "Chat", type: "llm")
fn.wrap(OpenAI::Client, :embeddings, name: "Embed", type: "llm")

#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 BitfabFunction.

Replay with Mock Strategies

Replay reruns historical traces through your code so you can compare outputs after an iteration. By default every child span runs real code — fine for offline traces, but expensive when children make paid LLM/API calls. Three strategies control whether child spans return their historical output instead of executing:

# "none" (default): everything runs real code
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "none")

# "all": every child span returns its historical output
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "all")

# "marked": only spans tagged with `mock_on_replay: true` return historical output
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "marked")

Tag the spans you want mocked at definition time:

class Pipeline
  include Bitfab::Traceable
  bitfab_function "my-fn"

  # mock_on_replay: true → returns historical output under mock: "marked"
  bitfab_span :call_llm, type: "llm", mock_on_replay: true
  def call_llm(prompt)
    # paid OpenAI call — skip during replay
  end

  bitfab_span :transform, type: "function"
  def transform(text)
    # cheap, deterministic — keep running real
  end

  bitfab_span :process, type: "agent"
  def process(text)
    call_llm(text)
    transform(text)
  end
end

Use mock: "marked" when you want to iterate on process's logic without paying for the LLM call each run. Use mock: "all" for the cheapest possible replay (every child span returns its recorded output).

Error Handling

Errors are automatically captured and re-raised:

bitfab_span :risky_operation, type: "function"
def risky_operation
  raise StandardError, "Something went wrong"
end

begin
  service.risky_operation
rescue StandardError => e
  # Error is captured in span and re-raised
  puts "Caught: #{e.message}"
end

Lifecycle

Automatic Flush

Spans are sent in background threads and automatically flushed on exit via at_exit hook.

Manual Flush

Wait for all pending spans to be sent:

Bitfab.flush_traces
# Blocks until all background threads complete

Reset Client

Clear the global client (useful for testing):

Bitfab.reset!

Thread Safety

  • Each thread has its own span context stack (using Thread.current)
  • Nested spans only work within the same thread
  • Background span sending is thread-safe

Examples

See bitfab-ruby-example/ for complete working examples:

Development

See DEVELOPMENT.md for development setup, testing, and publishing instructions.

License

MIT