Module: Bitfab::Traceable

Defined in:
lib/bitfab/traceable.rb

Overview

Mixin for declarative span tracing on instance methods.

Examples:

Bitfab.configure(api_key: "...")

class OrderService
  include Bitfab::Traceable
  bitfab_function "order-processing"

  bitfab_span :process_order, type: "function"
  def process_order(order_id)
    { total: 100 }
  end

  bitfab_span :validate_order, name: "Validate", type: "guardrail"
  def validate_order(order_id)
    { valid: true }
  end
end

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



25
26
27
# File 'lib/bitfab/traceable.rb', line 25

def self.included(base)
  base.extend(ClassMethods)
end

.trace_function_key_for(receiver, method_name) ⇒ Object

Resolve the trace function key a traced method records spans under, or nil if the method carries no Bitfab span wrapper. Walks the receiver’s ancestor chain for the wrapper module that bitfab_span / Traceable.wrap prepended. Instances resolve via the class. A Class/Module receiver resolves either way: singleton-class ancestors first (class/module methods wrapped on the singleton class), then the class’s own ancestors (methods whose wrapper was prepended onto the class itself), so a class-method replay is guarded regardless of which surface carries the wrapper. Singleton-first keeps the method actually dispatched by ‘receiver.send` (a class method shadows a same-named instance wrapper).

replay() uses this to reject a trace_function_key that contradicts the method’s declared key: a mismatch fetches one function’s historical traces but re-records the replay under the method’s own key, producing an incoherent test run. Mirrors the TypeScript and Python SDKs, which throw on the same key mismatch.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/bitfab/traceable.rb', line 93

def self.trace_function_key_for(receiver, method_name)
  name = method_name.to_s
  ancestors = if receiver.is_a?(Module)
    receiver.singleton_class.ancestors + receiver.ancestors
  else
    receiver.class.ancestors
  end
  ancestors.each do |mod|
    next unless mod.instance_variable_defined?(:@bitfab_span_method)
    next unless mod.instance_variable_get(:@bitfab_span_method) == name

    return mod.instance_variable_get(:@bitfab_span_key)
  end
  nil
end

.wrap(klass, method_name, trace_function_key:, name: nil, type: "custom", mock_on_replay: false, client: nil) ⇒ Object

Wrap an existing method on an external class with span tracing. Use this to trace third-party library calls without modifying their source.

Examples:

Bitfab::Traceable.wrap(OpenAI::Client, :chat,
  trace_function_key: "openai", name: "Chat", type: "llm")

Parameters:

  • klass (Class, Module)

    the class to wrap

  • method_name (Symbol)

    the method to wrap

  • trace_function_key (String)

    the trace function key

  • name (String, nil) (defaults to: nil)

    explicit span name (defaults to method name)

  • type (String) (defaults to: "custom")

    span type: llm, agent, function, guardrail, handoff, custom

  • mock_on_replay (Boolean) (defaults to: false)

    mark this span for the “marked” mock strategy. When true, ‘client.replay(… mock: “marked”)` returns this span’s historical output instead of executing the wrapped method.

  • client (Bitfab::Client, nil) (defaults to: nil)

    route spans through this specific client instead of the global ‘Bitfab.client`. When nil (default), the wrapper resolves `Bitfab.client` at each call (so `Bitfab.configure` / `reset!` between calls keeps working). Used by `Bitfab::Client#get_function` to preserve the bound client through the fluent wrapper, matching Python’s ‘BitfabFunction.span()` and TypeScript’s ‘BitfabFunction.withSpan()`.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/bitfab/traceable.rb', line 50

def self.wrap(klass, method_name, trace_function_key:, name: nil, type: "custom",
  mock_on_replay: false, client: nil)
  span_name = name || method_name.to_s
  method_name_str = method_name.to_s
  bound_client = client

  wrapper = Module.new do
    define_method(method_name) do |*args, **kwargs, &block|
      target_client = bound_client || Bitfab.client
      target_client.send(:execute_span,
        trace_function_key:,
        span_name:,
        span_type: type,
        function_name: method_name_str,
        args:,
        kwargs:,
        mock_on_replay:) do
        super(*args, **kwargs, &block)
      end
    end
  end

  wrapper.instance_variable_set(:@bitfab_span_method, method_name_str)
  wrapper.instance_variable_set(:@bitfab_span_key, trace_function_key)
  klass.prepend(wrapper)
end