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_name(value) ⇒ Object
-
.deserialize_inputs(item) ⇒ Array(Array, Hash)
Deserialize replay inputs from a span’s data into [args, kwargs].
-
.marshal_value(value) ⇒ String?
Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction.
- .oversized?(value) ⇒ Boolean
- .safe_to_s(value) ⇒ Object
-
.safely_call(value, method, *args) ⇒ Object
Call ‘value.<method>` and return the result, or yield to the fallback block if the call raises.
-
.serialize_inputs(args, kwargs = {}) ⇒ Object
Serialize function inputs (args + kwargs) for span data (human-readable).
-
.serialize_inputs_with_report(args, kwargs = {}) ⇒ Object
Like serialize_inputs, but also returns the accumulated dropped list so the send boundary can mark a lossy capture non-replayable.
-
.serialize_value(value) ⇒ Object
Serialize a value for JSON storage (human-readable).
- .serialize_value_inner(value, depth, dropped = []) ⇒ Object
-
.serialize_value_with_report(value) ⇒ Object
Like serialize_value, but also reports what could not be faithfully captured.
-
.unmarshal_value(encoded) ⇒ Object
Unmarshal a Base64-encoded string back into a Ruby object.
- .unserializable_stub(value, reason) ⇒ Object
Class Method Details
.class_name(value) ⇒ Object
117 118 119 120 121 |
# File 'lib/bitfab/serialize.rb', line 117 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.
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/bitfab/serialize.rb', line 192 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.
167 168 169 170 171 172 173 174 175 |
# File 'lib/bitfab/serialize.rb', line 167 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
127 128 129 130 131 132 133 |
# File 'lib/bitfab/serialize.rb', line 127 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
110 111 112 113 114 115 |
# File 'lib/bitfab/serialize.rb', line 110 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.
104 105 106 107 108 |
# File 'lib/bitfab/serialize.rb', line 104 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).
136 137 138 |
# File 'lib/bitfab/serialize.rb', line 136 def serialize_inputs(args, kwargs = {}) serialize_inputs_with_report(args, kwargs).first end |
.serialize_inputs_with_report(args, kwargs = {}) ⇒ Object
Like serialize_inputs, but also returns the accumulated dropped list so the send boundary can mark a lossy capture non-replayable.
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/bitfab/serialize.rb', line 142 def serialize_inputs_with_report(args, kwargs = {}) dropped = [] serialized = args.map do |arg| result, arg_dropped = serialize_value_with_report(arg) dropped.concat(arg_dropped) result end unless kwargs.empty? kw = kwargs.each_with_object({}) do |(k, v), acc| result, v_dropped = serialize_value_with_report(v) dropped.concat(v_dropped) acc[safe_to_s(k)] = result end serialized << kw end [serialized, dropped] 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 |
# File 'lib/bitfab/serialize.rb', line 31 def serialize_value(value) serialize_value_with_report(value).first end |
.serialize_value_inner(value, depth, dropped = []) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/bitfab/serialize.rb', line 53 def serialize_value_inner(value, depth, dropped = []) if depth > MAX_SERIALIZE_DEPTH dropped << "max_depth" return "<unserializable: max_depth>" end 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, dropped) end when Array value.map { |v| serialize_value_inner(v, depth + 1, dropped) } when Set value.map { |v| serialize_value_inner(v, depth + 1, dropped) } 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") do dropped << class_name(value) return unserializable_stub(value, "to_h_raised") end serialize_value_inner(h, depth + 1, dropped) elsif value.respond_to?(:to_a) a = safely_call(value, "to_a") do dropped << class_name(value) return unserializable_stub(value, "to_a_raised") end serialize_value_inner(a, depth + 1, dropped) else # An arbitrary object with no structured conversion: stringifying it is # a lossy capture (a repr, not its data), so report it as dropped. dropped << class_name(value) safe_to_s(value) end end rescue StandardError, SystemStackError dropped << class_name(value) unserializable_stub(value, "inner_error") end |
.serialize_value_with_report(value) ⇒ Object
Like serialize_value, but also reports what could not be faithfully captured. Returns [result, dropped] where dropped lists the class name (or “max_depth”/“too_large”) behind every placeholder the walk had to emit. A non-empty dropped means the capture is lossy, so the send boundary marks the span serialization_degraded (non-replayable) instead of shipping it as if it round-trips. Mirrors the Python/TS SDKs’ report serializers.
41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/bitfab/serialize.rb', line 41 def serialize_value_with_report(value) dropped = [] result = serialize_value_inner(value, 0, dropped) if oversized?(result) dropped << "too_large" return [unserializable_stub(value, "too_large"), dropped] end [result, dropped] rescue StandardError, SystemStackError [unserializable_stub(value, "unexpected_error"), [class_name(value)]] end |
.unmarshal_value(encoded) ⇒ Object
Unmarshal a Base64-encoded string back into a Ruby object.
181 182 183 |
# File 'lib/bitfab/serialize.rb', line 181 def unmarshal_value(encoded) Marshal.load(Base64.strict_decode64(encoded)) # rubocop:disable Security/MarshalLoad end |
.unserializable_stub(value, reason) ⇒ Object
123 124 125 |
# File 'lib/bitfab/serialize.rb', line 123 def unserializable_stub(value, reason) "<unserializable: #{class_name(value)} (#{reason})>" end |