bitfab 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bitfab/client.rb +2 -3
- data/lib/bitfab/replay.rb +13 -10
- data/lib/bitfab/serialize.rb +87 -10
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90f13972943bdfcdd6c5b72859d9ae03b23549577771f0e7d4f8726e2cff9037
|
|
4
|
+
data.tar.gz: f7c393d995069261d6ddf554727b02b25e6ec37f1f516a0ad01b114f684b5cb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ddfbc335058f37fc3f024b6596d3029dff1601be1243a8f779bbcd7dc899d78f75769c2db8e70894a316649ece042b56f9089fbe39b573bfe40cf313ec2c984
|
|
7
|
+
data.tar.gz: 0eca0c359e794699587347552b79d608d4ebf6016abef029795a2ffabc475f7d52bc16bcddf32525c7f9a714e7df6386af3c2f9ab6d40d0f8a13c638baf4bf3c
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -84,13 +84,12 @@ module Bitfab
|
|
|
84
84
|
return yield unless @enabled
|
|
85
85
|
|
|
86
86
|
parent = SpanContext.current
|
|
87
|
-
|
|
87
|
+
replay_ctx = ReplayContext.current
|
|
88
|
+
trace_id = parent ? parent[:trace_id] : (replay_ctx&.dig(:trace_id) || SecureRandom.uuid)
|
|
88
89
|
span_id = SecureRandom.uuid
|
|
89
90
|
parent_span_id = parent&.dig(:span_id)
|
|
90
91
|
is_root_span = parent_span_id.nil?
|
|
91
92
|
started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
92
|
-
|
|
93
|
-
replay_ctx = ReplayContext.current
|
|
94
93
|
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
95
94
|
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
96
95
|
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -22,19 +22,14 @@ module Bitfab
|
|
|
22
22
|
|
|
23
23
|
# Execute a block with replay context set on the current thread.
|
|
24
24
|
# The context is automatically cleared when the block completes.
|
|
25
|
-
|
|
26
|
-
# @param test_run_id [String]
|
|
27
|
-
# @param input_source_span_id [String, nil]
|
|
28
|
-
# @param input_source_trace_id [String, nil]
|
|
29
|
-
# @param mock_tree [Hash{String => Hash}, nil] keyed by "#{key}:#{index}"
|
|
30
|
-
# @param mock_strategy [String, nil] one of MOCK_STRATEGIES
|
|
31
|
-
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil,
|
|
25
|
+
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
32
26
|
mock_tree: nil, mock_strategy: nil)
|
|
33
27
|
previous = Thread.current[REPLAY_CONTEXT_KEY]
|
|
34
28
|
ctx = {
|
|
35
29
|
test_run_id:,
|
|
36
30
|
input_source_span_id:,
|
|
37
|
-
input_source_trace_id
|
|
31
|
+
input_source_trace_id:,
|
|
32
|
+
trace_id:
|
|
38
33
|
}
|
|
39
34
|
if mock_tree
|
|
40
35
|
ctx[:mock_tree] = mock_tree
|
|
@@ -100,9 +95,14 @@ module Bitfab
|
|
|
100
95
|
Bitfab.flush_traces
|
|
101
96
|
|
|
102
97
|
begin
|
|
103
|
-
http_client.complete_replay(test_run_id)
|
|
98
|
+
complete_response = http_client.complete_replay(test_run_id)
|
|
99
|
+
trace_id_map = complete_response&.dig("traceIds") || {}
|
|
100
|
+
result_items.each do |item|
|
|
101
|
+
item[:trace_id] = trace_id_map[item[:trace_id]]
|
|
102
|
+
end
|
|
104
103
|
rescue => e
|
|
105
104
|
warn "Bitfab: Failed to complete replay: #{e.message}"
|
|
105
|
+
result_items.each { |item| item[:trace_id] = nil }
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
{
|
|
@@ -248,11 +248,13 @@ module Bitfab
|
|
|
248
248
|
|
|
249
249
|
fn_result = nil
|
|
250
250
|
fn_error = nil
|
|
251
|
+
sdk_trace_id = SecureRandom.uuid
|
|
251
252
|
|
|
252
253
|
ReplayContext.with_context(
|
|
253
254
|
test_run_id:,
|
|
254
255
|
input_source_span_id:,
|
|
255
256
|
input_source_trace_id:,
|
|
257
|
+
trace_id: sdk_trace_id,
|
|
256
258
|
mock_tree:,
|
|
257
259
|
mock_strategy:
|
|
258
260
|
) do
|
|
@@ -272,7 +274,8 @@ module Bitfab
|
|
|
272
274
|
error: fn_error,
|
|
273
275
|
duration_ms: metrics[:duration_ms],
|
|
274
276
|
tokens: metrics[:tokens],
|
|
275
|
-
model: metrics[:model]
|
|
277
|
+
model: metrics[:model],
|
|
278
|
+
trace_id: sdk_trace_id
|
|
276
279
|
}
|
|
277
280
|
end
|
|
278
281
|
end
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -6,43 +6,115 @@ require "time"
|
|
|
6
6
|
|
|
7
7
|
module Bitfab
|
|
8
8
|
module Serialize
|
|
9
|
+
# Cap on serialized payload size. Walking arbitrary objects (e.g. an
|
|
10
|
+
# OpenAI client passed as a span input) can produce hundreds of KB to MB
|
|
11
|
+
# of useless internal state. Anything beyond this cap is replaced with a
|
|
12
|
+
# stub so the span still ships and the trace isn't dropped server-side.
|
|
13
|
+
MAX_SERIALIZED_BYTES = 512_000
|
|
14
|
+
|
|
15
|
+
# Recursion guard for cyclic graphs and pathologically nested structures.
|
|
16
|
+
MAX_SERIALIZE_DEPTH = 16
|
|
17
|
+
|
|
9
18
|
module_function
|
|
10
19
|
|
|
11
20
|
# Serialize a value for JSON storage (human-readable).
|
|
12
21
|
# Handles primitives, hashes, arrays, and objects with common conversion methods.
|
|
13
22
|
# Note: We intentionally avoid as_json here because it requires ActiveSupport,
|
|
14
23
|
# and we want to keep the SDK dependency-free (stdlib only).
|
|
24
|
+
#
|
|
25
|
+
# Guarantees:
|
|
26
|
+
# - Never raises. Pathological inputs (objects with raising to_s/to_h,
|
|
27
|
+
# cycles, BasicObject subclasses) return a stub string.
|
|
28
|
+
# - Never returns a value whose JSON encoding exceeds MAX_SERIALIZED_BYTES.
|
|
29
|
+
# Without this the wire-side JSON.dump in the http client can produce a
|
|
30
|
+
# request that times out or gets rejected, leaving a trace with zero spans.
|
|
15
31
|
def serialize_value(value)
|
|
32
|
+
result = serialize_value_inner(value, 0)
|
|
33
|
+
return result unless oversized?(result)
|
|
34
|
+
|
|
35
|
+
unserializable_stub(value, "too_large")
|
|
36
|
+
rescue StandardError, SystemStackError
|
|
37
|
+
unserializable_stub(value, "unexpected_error")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize_value_inner(value, depth)
|
|
41
|
+
return "<unserializable: max_depth>" if depth > MAX_SERIALIZE_DEPTH
|
|
42
|
+
|
|
16
43
|
case value
|
|
17
44
|
when nil, true, false, Integer, Float, String
|
|
18
45
|
value
|
|
19
46
|
when Hash
|
|
20
|
-
value.
|
|
47
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
48
|
+
acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1)
|
|
49
|
+
end
|
|
21
50
|
when Array
|
|
22
|
-
value.map { |v|
|
|
51
|
+
value.map { |v| serialize_value_inner(v, depth + 1) }
|
|
23
52
|
when Set
|
|
24
|
-
value.map { |v|
|
|
53
|
+
value.map { |v| serialize_value_inner(v, depth + 1) }
|
|
25
54
|
when Time, DateTime
|
|
26
|
-
value
|
|
55
|
+
safely_call(value, "iso8601", 3) { safe_to_s(value) }
|
|
27
56
|
when Date
|
|
28
|
-
value
|
|
57
|
+
safe_to_s(value)
|
|
29
58
|
when Symbol
|
|
30
59
|
value.to_s
|
|
31
60
|
else
|
|
32
61
|
if value.respond_to?(:to_h)
|
|
33
|
-
|
|
62
|
+
h = safely_call(value, "to_h") { return unserializable_stub(value, "to_h_raised") }
|
|
63
|
+
serialize_value_inner(h, depth + 1)
|
|
34
64
|
elsif value.respond_to?(:to_a)
|
|
35
|
-
|
|
65
|
+
a = safely_call(value, "to_a") { return unserializable_stub(value, "to_a_raised") }
|
|
66
|
+
serialize_value_inner(a, depth + 1)
|
|
36
67
|
else
|
|
37
|
-
value
|
|
68
|
+
safe_to_s(value)
|
|
38
69
|
end
|
|
39
70
|
end
|
|
71
|
+
rescue StandardError, SystemStackError
|
|
72
|
+
unserializable_stub(value, "inner_error")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Call `value.<method>` and return the result, or yield to the fallback
|
|
76
|
+
# block if the call raises. Used to harden every call site that invokes a
|
|
77
|
+
# user-defined method on an arbitrary object.
|
|
78
|
+
def safely_call(value, method, *args)
|
|
79
|
+
value.public_send(method, *args)
|
|
80
|
+
rescue
|
|
81
|
+
yield
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def safe_to_s(value)
|
|
85
|
+
str = value.to_s
|
|
86
|
+
str.is_a?(String) ? str : "<#{class_name(value)}: to_s returned non-String>"
|
|
87
|
+
rescue
|
|
88
|
+
"<#{class_name(value)}: to_s raised>"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def class_name(value)
|
|
92
|
+
value.class.name || "Object"
|
|
93
|
+
rescue
|
|
94
|
+
"Object"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unserializable_stub(value, reason)
|
|
98
|
+
"<unserializable: #{class_name(value)} (#{reason})>"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def oversized?(value)
|
|
102
|
+
JSON.dump(value).bytesize > MAX_SERIALIZED_BYTES
|
|
103
|
+
rescue
|
|
104
|
+
# If JSON.dump can't handle it, the wire path can't either, so treat as
|
|
105
|
+
# oversized to force the stub fallback.
|
|
106
|
+
true
|
|
40
107
|
end
|
|
41
108
|
|
|
42
109
|
# Serialize function inputs (args + kwargs) for span data (human-readable).
|
|
43
110
|
def serialize_inputs(args, kwargs = {})
|
|
44
111
|
serialized = args.map { |arg| serialize_value(arg) }
|
|
45
|
-
|
|
112
|
+
unless kwargs.empty?
|
|
113
|
+
kw = kwargs.each_with_object({}) do |(k, v), acc|
|
|
114
|
+
acc[safe_to_s(k)] = serialize_value(v)
|
|
115
|
+
end
|
|
116
|
+
serialized << kw
|
|
117
|
+
end
|
|
46
118
|
serialized
|
|
47
119
|
end
|
|
48
120
|
|
|
@@ -51,8 +123,13 @@ module Bitfab
|
|
|
51
123
|
#
|
|
52
124
|
# @param value [Object] any Ruby value
|
|
53
125
|
# @return [String, nil] Base64-encoded Marshal dump, or nil if marshalling fails
|
|
126
|
+
# (objects with non-marshallable members like Proc/IO, or whose marshalled
|
|
127
|
+
# size exceeds MAX_SERIALIZED_BYTES).
|
|
54
128
|
def marshal_value(value)
|
|
55
|
-
|
|
129
|
+
dumped = Marshal.dump(value)
|
|
130
|
+
return nil if dumped.bytesize > MAX_SERIALIZED_BYTES
|
|
131
|
+
|
|
132
|
+
Base64.strict_encode64(dumped)
|
|
56
133
|
rescue TypeError, ArgumentError
|
|
57
134
|
# Some objects (Proc, IO, etc.) can't be marshalled
|
|
58
135
|
nil
|
data/lib/bitfab/version.rb
CHANGED