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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 364e0ec3b3aa00cc47a398872911e18b1a3e5253f4448214da67f9c12af51814
4
- data.tar.gz: 482340c71e8d2cb0f8ed92efa7a214278529a670e836e46b6209cc18126f230b
3
+ metadata.gz: d3286f80b5a55bff760f454480b1a3662fdf984174ee65d6f195dcbca5d456ed
4
+ data.tar.gz: d1d8bc8e244075f56cc6c06d42dae614da95b4531ea8bafc21de2b84b1d2526a
5
5
  SHA512:
6
- metadata.gz: d9c10548592e02d52a74b96b268bea01d33200c3ff338b61758ec6d54dc2adcf07ca5c442212d464cb53cb86a1aeeccb7466c9ca71ba49810902ff6f5445b0b5
7
- data.tar.gz: 3f12de6d0a9eeb5f7c8218d977c4d344a972f762f6c7380e5123d8976032c2215f63337698e4c7d6f18fd669786e4fded0084e0be2ef8fce1e7a696324c2f9b5
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
- human_inputs = Serialize.serialize_inputs(args, kwargs)
430
- human_output = Serialize.serialize_value(result)
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
 
@@ -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: }. Use it to render replay progress (e.g. a terminal progress
106
- # bar). A raising callback never crashes the run.
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
- report = lambda do |result|
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].nil? ? (succeeded += 1) : (errored += 1)
271
+ error = result[:error]
272
+ error.nil? ? (succeeded += 1) : (errored += 1)
263
273
  begin
264
- on_progress.call({completed:, total:, succeeded:, errored:})
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
@@ -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
- result = serialize_value_inner(value, 0)
33
- return result unless oversized?(result)
32
+ serialize_value_with_report(value).first
33
+ end
34
34
 
35
- unserializable_stub(value, "too_large")
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
- return "<unserializable: max_depth>" if depth > MAX_SERIALIZE_DEPTH
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") { return unserializable_stub(value, "to_h_raised") }
63
- serialize_value_inner(h, depth + 1)
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") { return unserializable_stub(value, "to_a_raised") }
66
- serialize_value_inner(a, depth + 1)
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
- serialized = args.map { |arg| serialize_value(arg) }
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
- acc[safe_to_s(k)] = serialize_value(v)
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.21.1"
4
+ VERSION = "0.22.0"
5
5
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.1
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team