bitfab 0.21.1 → 0.22.0
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 +18 -3
- data/lib/bitfab/constants.rb +4 -0
- data/lib/bitfab/replay.rb +20 -7
- data/lib/bitfab/serialize.rb +55 -16
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +23 -0
- 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: d3286f80b5a55bff760f454480b1a3662fdf984174ee65d6f195dcbca5d456ed
|
|
4
|
+
data.tar.gz: d1d8bc8e244075f56cc6c06d42dae614da95b4531ea8bafc21de2b84b1d2526a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ffbf4c36e417d4bf391eb9509a0136bc29dda1e54e191c84b51a59c400821fe7c21dbb796d272eaaca30f72502160d3aa8cb93efe1159304778422ac70ca9c1
|
|
7
|
+
data.tar.gz: 2dac975acb96571c6e7303ac7632cf3b099f3884ce3bf7f499dbfb35c71c357674868210906ebc6864dabb3a6645b7f82e41b4b8559553ab2a93304e2d106871
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -425,9 +425,12 @@ module Bitfab
|
|
|
425
425
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
426
426
|
span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
|
|
427
427
|
started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil)
|
|
428
|
-
# Human-readable JSON (input/output fields)
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
# Human-readable JSON (input/output fields). The *_with_report variants
|
|
429
|
+
# also report what could not be faithfully captured so a lossy span is
|
|
430
|
+
# marked non-replayable below instead of shipping as if it round-trips.
|
|
431
|
+
human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
432
|
+
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
433
|
+
dropped = input_dropped + output_dropped
|
|
431
434
|
|
|
432
435
|
# Marshal + Base64 for full Ruby-to-Ruby object reconstruction
|
|
433
436
|
raw_input = (args.length == 1 && kwargs.empty?) ? args[0] : {args:, kwargs:}
|
|
@@ -469,6 +472,18 @@ module Bitfab
|
|
|
469
472
|
}
|
|
470
473
|
payload["testRunId"] = test_run_id if test_run_id
|
|
471
474
|
|
|
475
|
+
# A value that could only be captured as a placeholder makes the span
|
|
476
|
+
# non-replayable; record it on the payload's errors (matching the Python
|
|
477
|
+
# and TypeScript SDKs) instead of shipping the lossy capture silently.
|
|
478
|
+
unless dropped.empty?
|
|
479
|
+
names = dropped.uniq.sort.join(", ")
|
|
480
|
+
payload["errors"] = [{
|
|
481
|
+
"source" => "sdk",
|
|
482
|
+
"step" => "serialization_degraded",
|
|
483
|
+
"error" => "non-replayable: could not faithfully capture #{names}"
|
|
484
|
+
}]
|
|
485
|
+
end
|
|
486
|
+
|
|
472
487
|
@http_client.send_external_span(payload) # Returns the background thread
|
|
473
488
|
end
|
|
474
489
|
|
data/lib/bitfab/constants.rb
CHANGED
|
@@ -3,4 +3,8 @@
|
|
|
3
3
|
module Bitfab
|
|
4
4
|
DEFAULT_SERVICE_URL = "https://bitfab.ai"
|
|
5
5
|
REPLAY_CONTEXT_KEY = :__bitfab_replay_context
|
|
6
|
+
|
|
7
|
+
# Wire prefix the Bitfab plugin scans for on stderr to report live progress
|
|
8
|
+
# while a replay runs. One trailing space, then the progress object as JSON.
|
|
9
|
+
BITFAB_PROGRESS_PREFIX = "@@bitfab:progress "
|
|
6
10
|
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -102,8 +102,12 @@ module Bitfab
|
|
|
102
102
|
# that item's :error rather than crashing the run.
|
|
103
103
|
# @param on_progress [#call, nil] optional callback invoked once per item as
|
|
104
104
|
# it finishes, with a running-totals hash { completed:, total:, succeeded:,
|
|
105
|
-
# errored: }
|
|
106
|
-
#
|
|
105
|
+
# errored:, item: } where item is { trace_id:, error:, duration_ms: } for
|
|
106
|
+
# the single item that just settled. trace_id is the SOURCE (historical)
|
|
107
|
+
# trace that was replayed, error is that item's replay error or nil, and
|
|
108
|
+
# duration_ms is how long that one trace took to replay. Use it to
|
|
109
|
+
# render replay progress (e.g. a per-trace log). A raising callback never
|
|
110
|
+
# crashes the run.
|
|
107
111
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
108
112
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
109
113
|
code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
|
|
@@ -254,14 +258,23 @@ module Bitfab
|
|
|
254
258
|
completed = 0
|
|
255
259
|
succeeded = 0
|
|
256
260
|
errored = 0
|
|
257
|
-
|
|
261
|
+
# Each event carries the single item that just settled so a progress UI
|
|
262
|
+
# can render per-trace pass/fail as the run streams. trace_id is the
|
|
263
|
+
# SOURCE (historical) trace that was replayed, taken from the server
|
|
264
|
+
# item: the result's own :trace_id at this stage is the new replay
|
|
265
|
+
# trace id (assigned in run() after complete_replay), not the source.
|
|
266
|
+
report = lambda do |result, source_trace_id|
|
|
258
267
|
return unless on_progress
|
|
259
268
|
|
|
260
269
|
progress_mutex.synchronize do
|
|
261
270
|
completed += 1
|
|
262
|
-
result[:error]
|
|
271
|
+
error = result[:error]
|
|
272
|
+
error.nil? ? (succeeded += 1) : (errored += 1)
|
|
263
273
|
begin
|
|
264
|
-
on_progress.call({
|
|
274
|
+
on_progress.call({
|
|
275
|
+
completed:, total:, succeeded:, errored:,
|
|
276
|
+
item: {trace_id: source_trace_id, error:, duration_ms: result[:duration_ms]}
|
|
277
|
+
})
|
|
265
278
|
rescue => e
|
|
266
279
|
warn "Bitfab: replay on_progress callback raised: #{e.message}"
|
|
267
280
|
end
|
|
@@ -272,7 +285,7 @@ module Bitfab
|
|
|
272
285
|
server_items.map do |item|
|
|
273
286
|
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
274
287
|
adapt_inputs, include_db_branch_lease)
|
|
275
|
-
report.call(result)
|
|
288
|
+
report.call(result, item["traceId"])
|
|
276
289
|
result
|
|
277
290
|
end
|
|
278
291
|
else
|
|
@@ -290,7 +303,7 @@ module Bitfab
|
|
|
290
303
|
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
291
304
|
adapt_inputs, include_db_branch_lease)
|
|
292
305
|
results_mutex.synchronize { results[idx] = result }
|
|
293
|
-
report.call(result)
|
|
306
|
+
report.call(result, item["traceId"])
|
|
294
307
|
end
|
|
295
308
|
end
|
|
296
309
|
end
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -29,28 +29,44 @@ module Bitfab
|
|
|
29
29
|
# Without this the wire-side JSON.dump in the http client can produce a
|
|
30
30
|
# request that times out or gets rejected, leaving a trace with zero spans.
|
|
31
31
|
def serialize_value(value)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
serialize_value_with_report(value).first
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
# Like serialize_value, but also reports what could not be faithfully
|
|
36
|
+
# captured. Returns [result, dropped] where dropped lists the class name (or
|
|
37
|
+
# "max_depth"/"too_large") behind every placeholder the walk had to emit. A
|
|
38
|
+
# non-empty dropped means the capture is lossy, so the send boundary marks
|
|
39
|
+
# the span serialization_degraded (non-replayable) instead of shipping it as
|
|
40
|
+
# if it round-trips. Mirrors the Python/TS SDKs' report serializers.
|
|
41
|
+
def serialize_value_with_report(value)
|
|
42
|
+
dropped = []
|
|
43
|
+
result = serialize_value_inner(value, 0, dropped)
|
|
44
|
+
if oversized?(result)
|
|
45
|
+
dropped << "too_large"
|
|
46
|
+
return [unserializable_stub(value, "too_large"), dropped]
|
|
47
|
+
end
|
|
48
|
+
[result, dropped]
|
|
36
49
|
rescue StandardError, SystemStackError
|
|
37
|
-
unserializable_stub(value, "unexpected_error")
|
|
50
|
+
[unserializable_stub(value, "unexpected_error"), [class_name(value)]]
|
|
38
51
|
end
|
|
39
52
|
|
|
40
|
-
def serialize_value_inner(value, depth)
|
|
41
|
-
|
|
53
|
+
def serialize_value_inner(value, depth, dropped = [])
|
|
54
|
+
if depth > MAX_SERIALIZE_DEPTH
|
|
55
|
+
dropped << "max_depth"
|
|
56
|
+
return "<unserializable: max_depth>"
|
|
57
|
+
end
|
|
42
58
|
|
|
43
59
|
case value
|
|
44
60
|
when nil, true, false, Integer, Float, String
|
|
45
61
|
value
|
|
46
62
|
when Hash
|
|
47
63
|
value.each_with_object({}) do |(k, v), acc|
|
|
48
|
-
acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1)
|
|
64
|
+
acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1, dropped)
|
|
49
65
|
end
|
|
50
66
|
when Array
|
|
51
|
-
value.map { |v| serialize_value_inner(v, depth + 1) }
|
|
67
|
+
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
52
68
|
when Set
|
|
53
|
-
value.map { |v| serialize_value_inner(v, depth + 1) }
|
|
69
|
+
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
54
70
|
when Time, DateTime
|
|
55
71
|
safely_call(value, "iso8601", 3) { safe_to_s(value) }
|
|
56
72
|
when Date
|
|
@@ -59,16 +75,26 @@ module Bitfab
|
|
|
59
75
|
value.to_s
|
|
60
76
|
else
|
|
61
77
|
if value.respond_to?(:to_h)
|
|
62
|
-
h = safely_call(value, "to_h")
|
|
63
|
-
|
|
78
|
+
h = safely_call(value, "to_h") do
|
|
79
|
+
dropped << class_name(value)
|
|
80
|
+
return unserializable_stub(value, "to_h_raised")
|
|
81
|
+
end
|
|
82
|
+
serialize_value_inner(h, depth + 1, dropped)
|
|
64
83
|
elsif value.respond_to?(:to_a)
|
|
65
|
-
a = safely_call(value, "to_a")
|
|
66
|
-
|
|
84
|
+
a = safely_call(value, "to_a") do
|
|
85
|
+
dropped << class_name(value)
|
|
86
|
+
return unserializable_stub(value, "to_a_raised")
|
|
87
|
+
end
|
|
88
|
+
serialize_value_inner(a, depth + 1, dropped)
|
|
67
89
|
else
|
|
90
|
+
# An arbitrary object with no structured conversion: stringifying it is
|
|
91
|
+
# a lossy capture (a repr, not its data), so report it as dropped.
|
|
92
|
+
dropped << class_name(value)
|
|
68
93
|
safe_to_s(value)
|
|
69
94
|
end
|
|
70
95
|
end
|
|
71
96
|
rescue StandardError, SystemStackError
|
|
97
|
+
dropped << class_name(value)
|
|
72
98
|
unserializable_stub(value, "inner_error")
|
|
73
99
|
end
|
|
74
100
|
|
|
@@ -108,14 +134,27 @@ module Bitfab
|
|
|
108
134
|
|
|
109
135
|
# Serialize function inputs (args + kwargs) for span data (human-readable).
|
|
110
136
|
def serialize_inputs(args, kwargs = {})
|
|
111
|
-
|
|
137
|
+
serialize_inputs_with_report(args, kwargs).first
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Like serialize_inputs, but also returns the accumulated dropped list so the
|
|
141
|
+
# send boundary can mark a lossy capture non-replayable.
|
|
142
|
+
def serialize_inputs_with_report(args, kwargs = {})
|
|
143
|
+
dropped = []
|
|
144
|
+
serialized = args.map do |arg|
|
|
145
|
+
result, arg_dropped = serialize_value_with_report(arg)
|
|
146
|
+
dropped.concat(arg_dropped)
|
|
147
|
+
result
|
|
148
|
+
end
|
|
112
149
|
unless kwargs.empty?
|
|
113
150
|
kw = kwargs.each_with_object({}) do |(k, v), acc|
|
|
114
|
-
|
|
151
|
+
result, v_dropped = serialize_value_with_report(v)
|
|
152
|
+
dropped.concat(v_dropped)
|
|
153
|
+
acc[safe_to_s(k)] = result
|
|
115
154
|
end
|
|
116
155
|
serialized << kw
|
|
117
156
|
end
|
|
118
|
-
serialized
|
|
157
|
+
[serialized, dropped]
|
|
119
158
|
end
|
|
120
159
|
|
|
121
160
|
# Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction.
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
require_relative "bitfab/version"
|
|
4
6
|
require_relative "bitfab/constants"
|
|
5
7
|
require_relative "bitfab/warn_once"
|
|
@@ -103,5 +105,26 @@ module Bitfab
|
|
|
103
105
|
|
|
104
106
|
CurrentTrace.new(entry[:trace_id])
|
|
105
107
|
end
|
|
108
|
+
|
|
109
|
+
# A ready-made on_progress callback for replay: writes one @@bitfab:progress
|
|
110
|
+
# line per trace to stderr, which the Bitfab plugin polls to report live
|
|
111
|
+
# progress while the replay runs in the background, so replay scripts never
|
|
112
|
+
# hand-format the protocol. stdout stays the ReplayResult JSON. Never raises
|
|
113
|
+
# (progress must not crash a run). The progress hash's item ({ trace_id:,
|
|
114
|
+
# error:, duration_ms: }) is forwarded verbatim through to_json, so the
|
|
115
|
+
# plugin sees the source trace id, error, and duration of the item that just
|
|
116
|
+
# settled.
|
|
117
|
+
#
|
|
118
|
+
# @example
|
|
119
|
+
# Bitfab.client.replay(..., on_progress: Bitfab.method(:report_replay_progress))
|
|
120
|
+
#
|
|
121
|
+
# @param progress [Hash] running totals with keys completed, total,
|
|
122
|
+
# succeeded, errored, and item (the object replay already passes to
|
|
123
|
+
# on_progress)
|
|
124
|
+
def report_replay_progress(progress)
|
|
125
|
+
warn "#{BITFAB_PROGRESS_PREFIX}#{progress.to_json}"
|
|
126
|
+
rescue
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
106
129
|
end
|
|
107
130
|
end
|