Module: Bitfab::Serialize

Defined in:
lib/bitfab/serialize.rb

Constant Summary collapse

MAX_SERIALIZED_BYTES =

Cap on serialized payload size. Walking arbitrary objects (e.g. an OpenAI client passed as a span input) can produce hundreds of KB to MB of useless internal state. Anything beyond this cap is replaced with a stub so the span still ships and the trace isn’t dropped server-side.

512_000
MAX_SERIALIZE_DEPTH =

Recursion guard for cyclic graphs and pathologically nested structures.

16

Class Method Summary collapse

Class Method Details

.class_name(value) ⇒ Object



91
92
93
94
95
# File 'lib/bitfab/serialize.rb', line 91

def class_name(value)
  value.class.name || "Object"
rescue
  "Object"
end

.deserialize_inputs(item) ⇒ Array(Array, Hash)

Deserialize replay inputs from a span’s data into [args, kwargs].

Prefers Marshal-serialized ‘inputSerialized` for type preservation, falls back to the raw `input` field.

Parameters:

  • item (Hash)

    with “inputSerialized” and/or “input” keys

Returns:

  • (Array(Array, Hash))

    positional args and keyword args



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/bitfab/serialize.rb', line 153

def deserialize_inputs(item)
  input_serialized = item["inputSerialized"]
  raw_input = item["input"]

  if input_serialized.is_a?(String) && !input_serialized.empty?
    begin
      deserialized = unmarshal_value(input_serialized)
      if deserialized.is_a?(Hash) && (deserialized.key?(:args) || deserialized.key?(:kwargs))
        return [deserialized[:args] || [], deserialized[:kwargs] || {}]
      end
      return deserialized.nil? ? [[], {}] : [[deserialized], {}]
    rescue
      # Fall through to raw_input
    end
  end

  if raw_input.is_a?(Array)
    [raw_input, {}]
  elsif raw_input.is_a?(Hash)
    [[], raw_input.transform_keys(&:to_sym)]
  elsif raw_input.nil?
    [[], {}]
  else
    [[raw_input], {}]
  end
end

.marshal_value(value) ⇒ String?

Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction. Handles arbitrary Ruby objects including custom classes.

Parameters:

  • value (Object)

    any Ruby value

Returns:

  • (String, nil)

    Base64-encoded Marshal dump, or nil if marshalling fails (objects with non-marshallable members like Proc/IO, or whose marshalled size exceeds MAX_SERIALIZED_BYTES).



128
129
130
131
132
133
134
135
136
# File 'lib/bitfab/serialize.rb', line 128

def marshal_value(value)
  dumped = Marshal.dump(value)
  return nil if dumped.bytesize > MAX_SERIALIZED_BYTES

  Base64.strict_encode64(dumped)
rescue TypeError, ArgumentError
  # Some objects (Proc, IO, etc.) can't be marshalled
  nil
end

.oversized?(value) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
106
107
# File 'lib/bitfab/serialize.rb', line 101

def oversized?(value)
  JSON.dump(value).bytesize > MAX_SERIALIZED_BYTES
rescue
  # If JSON.dump can't handle it, the wire path can't either, so treat as
  # oversized to force the stub fallback.
  true
end

.safe_to_s(value) ⇒ Object



84
85
86
87
88
89
# File 'lib/bitfab/serialize.rb', line 84

def safe_to_s(value)
  str = value.to_s
  str.is_a?(String) ? str : "<#{class_name(value)}: to_s returned non-String>"
rescue
  "<#{class_name(value)}: to_s raised>"
end

.safely_call(value, method, *args) ⇒ Object

Call ‘value.<method>` and return the result, or yield to the fallback block if the call raises. Used to harden every call site that invokes a user-defined method on an arbitrary object.



78
79
80
81
82
# File 'lib/bitfab/serialize.rb', line 78

def safely_call(value, method, *args)
  value.public_send(method, *args)
rescue
  yield
end

.serialize_inputs(args, kwargs = {}) ⇒ Object

Serialize function inputs (args + kwargs) for span data (human-readable).



110
111
112
113
114
115
116
117
118
119
# File 'lib/bitfab/serialize.rb', line 110

def serialize_inputs(args, kwargs = {})
  serialized = args.map { |arg| serialize_value(arg) }
  unless kwargs.empty?
    kw = kwargs.each_with_object({}) do |(k, v), acc|
      acc[safe_to_s(k)] = serialize_value(v)
    end
    serialized << kw
  end
  serialized
end

.serialize_value(value) ⇒ Object

Serialize a value for JSON storage (human-readable). Handles primitives, hashes, arrays, and objects with common conversion methods. Note: We intentionally avoid as_json here because it requires ActiveSupport, and we want to keep the SDK dependency-free (stdlib only).

Guarantees:

  • Never raises. Pathological inputs (objects with raising to_s/to_h, cycles, BasicObject subclasses) return a stub string.

  • Never returns a value whose JSON encoding exceeds MAX_SERIALIZED_BYTES. Without this the wire-side JSON.dump in the http client can produce a request that times out or gets rejected, leaving a trace with zero spans.



31
32
33
34
35
36
37
38
# File 'lib/bitfab/serialize.rb', line 31

def serialize_value(value)
  result = serialize_value_inner(value, 0)
  return result unless oversized?(result)

  unserializable_stub(value, "too_large")
rescue StandardError, SystemStackError
  unserializable_stub(value, "unexpected_error")
end

.serialize_value_inner(value, depth) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/bitfab/serialize.rb', line 40

def serialize_value_inner(value, depth)
  return "<unserializable: max_depth>" if depth > MAX_SERIALIZE_DEPTH

  case value
  when nil, true, false, Integer, Float, String
    value
  when Hash
    value.each_with_object({}) do |(k, v), acc|
      acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1)
    end
  when Array
    value.map { |v| serialize_value_inner(v, depth + 1) }
  when Set
    value.map { |v| serialize_value_inner(v, depth + 1) }
  when Time, DateTime
    safely_call(value, "iso8601", 3) { safe_to_s(value) }
  when Date
    safe_to_s(value)
  when Symbol
    value.to_s
  else
    if value.respond_to?(:to_h)
      h = safely_call(value, "to_h") { return unserializable_stub(value, "to_h_raised") }
      serialize_value_inner(h, depth + 1)
    elsif value.respond_to?(:to_a)
      a = safely_call(value, "to_a") { return unserializable_stub(value, "to_a_raised") }
      serialize_value_inner(a, depth + 1)
    else
      safe_to_s(value)
    end
  end
rescue StandardError, SystemStackError
  unserializable_stub(value, "inner_error")
end

.unmarshal_value(encoded) ⇒ Object

Unmarshal a Base64-encoded string back into a Ruby object.

Parameters:

  • encoded (String)

    Base64-encoded Marshal dump

Returns:

  • (Object)

    the reconstructed Ruby object



142
143
144
# File 'lib/bitfab/serialize.rb', line 142

def unmarshal_value(encoded)
  Marshal.load(Base64.strict_decode64(encoded)) # rubocop:disable Security/MarshalLoad
end

.unserializable_stub(value, reason) ⇒ Object



97
98
99
# File 'lib/bitfab/serialize.rb', line 97

def unserializable_stub(value, reason)
  "<unserializable: #{class_name(value)} (#{reason})>"
end