bitfab 0.36.1 → 0.36.3

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: e0b827ad6e9bca08b3025f9c2419290910999a5985705369e6375aee8091232c
4
- data.tar.gz: e6e0a414013d3bee0efda22415430d9690fe537973a36e5f3dda540fcda3d5e4
3
+ metadata.gz: deea3052760a7b677216225f0cd030f4931995b1d10e6b7396c97a57a27ab9c1
4
+ data.tar.gz: 5f64a512db5c8c79f092e5bbf92e16adfc2e20a4a07c869eafab48fb42db5665
5
5
  SHA512:
6
- metadata.gz: ee72cf5ed5c81ad34dbd4d980091523c90c7cb1d11fc716499ab5f604f1829f5653851350942b2cea6660e20e67c065ae8807e0778153a0ccf08a1397cc08491
7
- data.tar.gz: 297efa6c3e5d9b1086536dd5e560168e9f9d4c638638b562a67d69ccb1c37253534da6447778032f93e186532f0a563eb7b7f41148eb6b9e0e2ac91c4ed2196e
6
+ metadata.gz: f9024ab4975b3963e4d2c91aa903b3e4bc7880791aa1f0a4132a9ee847e54e2132339246cbc1067beba8a2130e813738474fcc40a38a1d3b9a0e19ba92633834
7
+ data.tar.gz: b3c4b7c252a566192362e6d1a3e4d31bcad55f752a70527fce5de87ebc9ab75c4839a02fd5d67589823ee36003f3fcb49d5bdade0a315dc8a22971b9769ea9c3
@@ -181,9 +181,11 @@ module Bitfab
181
181
  request("/api/sdk/replay/start", payload, timeout:)
182
182
  end
183
183
 
184
- # Fetch an external span by ID. Blocking GET request.
185
- def get_external_span(span_id)
186
- get("/api/sdk/externalSpans/#{span_id}", timeout: 30)
184
+ # Fetch an external span by ID. Blocking GET request. The replay view keeps
185
+ # only the input/output serialization fields used by replay.
186
+ def get_external_span(span_id, replay_view: false)
187
+ query = replay_view ? "?view=replay" : ""
188
+ get("/api/sdk/externalSpans/#{span_id}#{query}", timeout: 30)
187
189
  end
188
190
 
189
191
  def get_trace_span(trace_id, id: nil, name: nil, occurrence: "last")
@@ -210,9 +212,12 @@ module Bitfab
210
212
  # payloads so only the spans actually mocked are fetched later (by
211
213
  # externalSpanId), avoiding dragging down every span's output when only a
212
214
  # few are mocked.
213
- def get_span_tree(external_span_id, include_outputs: true)
215
+ def get_span_tree(external_span_id, include_outputs: true, include_root_output: true)
214
216
  endpoint = "/api/sdk/replay/spanTree/#{external_span_id}"
215
- endpoint += "?includeOutputs=false" unless include_outputs
217
+ query = []
218
+ query << "includeOutputs=false" unless include_outputs
219
+ query << "includeRootOutput=false" unless include_root_output
220
+ endpoint += "?#{query.join("&")}" unless query.empty?
216
221
  get(endpoint, timeout: 30)
217
222
  end
218
223
 
data/lib/bitfab/replay.rb CHANGED
@@ -12,6 +12,30 @@ require_relative "traceable"
12
12
  require_relative "transport"
13
13
 
14
14
  module Bitfab
15
+ # A requested replay database branch could not be resolved. The resolver code
16
+ # and original message remain structured for callers to inspect.
17
+ class DbBranchReplayError < StandardError
18
+ attr_reader :code, :original_trace_id
19
+
20
+ def initialize(code, message, original_trace_id)
21
+ super(message)
22
+ @code = code
23
+ @original_trace_id = original_trace_id
24
+ end
25
+ end
26
+
27
+ # Whole-run replay failure with every item collected before the run failed.
28
+ class ReplayError < StandardError
29
+ attr_reader :items, :test_run_id, :test_run_url
30
+
31
+ def initialize(message, items:, test_run_id:, test_run_url:)
32
+ super(message)
33
+ @items = items
34
+ @test_run_id = test_run_id
35
+ @test_run_url = test_run_url
36
+ end
37
+ end
38
+
15
39
  # Replay mock strategies. Mirrors the Python and TypeScript SDKs.
16
40
  #
17
41
  # - "marked" : only spans declared with mock_on_replay: true return historical
@@ -85,6 +109,50 @@ module Bitfab
85
109
 
86
110
  module_function
87
111
 
112
+ def json_safe(value)
113
+ case value
114
+ when Exception
115
+ serialized = {type: value.class.name, message: value.message, backtrace: value.backtrace}
116
+ if value.is_a?(DbBranchReplayError)
117
+ serialized[:code] = value.code
118
+ serialized[:original_trace_id] = value.original_trace_id
119
+ serialized[:cause] = json_safe(value.cause) if value.cause
120
+ end
121
+ serialized
122
+ when Hash
123
+ value.to_h { |key, child| [key, json_safe(child)] }
124
+ when Array
125
+ value.map { |child| json_safe(child) }
126
+ else
127
+ value
128
+ end
129
+ end
130
+
131
+ def replay_item_error_message(error)
132
+ return error.message unless error.is_a?(DbBranchReplayError)
133
+
134
+ "Replay requested a database branch for trace #{error.original_trace_id} but it " \
135
+ "could not be resolved (#{error.code}): #{error.message}. The method was not run, " \
136
+ "because replaying it against the live database would produce a result that looks " \
137
+ "valid but did not use the historical data you asked for."
138
+ end
139
+
140
+ def public_replay_items(items)
141
+ items.map { |item| item.except(:_sdk_trace_id) }
142
+ end
143
+
144
+ def preserve_replay_failure(items, test_run_id, test_run_url)
145
+ yield
146
+ rescue => cause
147
+ error = ReplayError.new(
148
+ cause.message,
149
+ items: public_replay_items(items),
150
+ test_run_id:,
151
+ test_run_url:
152
+ )
153
+ raise error, cause:
154
+ end
155
+
88
156
  # Replay historical traces through a method and create a test run.
89
157
  #
90
158
  # Fetches the last N traces for the given trace function key, re-runs each
@@ -223,6 +291,7 @@ module Bitfab
223
291
  )
224
292
  test_run_id = replay_data["testRunId"]
225
293
  test_run_url = replay_data["testRunUrl"]
294
+ full_test_run_url = "#{client.service_url}#{test_run_url}"
226
295
  server_items = replay_data["items"] || []
227
296
 
228
297
  result_items = if server_items.any?
@@ -237,12 +306,16 @@ module Bitfab
237
306
  # to finalize once the server confirms every replay trace it queued: the
238
307
  # trace-ID mapping complete_replay builds would otherwise race the
239
308
  # in-flight batches and hand back nil for every item.
240
- wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
309
+ preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
310
+ wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
311
+ end
241
312
 
242
313
  # complete_replay finalizes the run and returns the token/diagnostic
243
314
  # mapping; its failures propagate loudly because a run that never
244
315
  # completed can't be finalized.
245
- complete_response = http_client.complete_replay(test_run_id)
316
+ complete_response = preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
317
+ http_client.complete_replay(test_run_id)
318
+ end
246
319
  trace_id_map = complete_response&.dig("traceIds")
247
320
  # Per-replay-trace token usage keyed by server trace id: the REPLAYED
248
321
  # run's tokens (span-aggregated server-side), used below to fill each
@@ -278,11 +351,18 @@ module Bitfab
278
351
  if completed_count.positive? && missing.length == completed_count
279
352
  trace_count = complete_response["traceCount"]
280
353
  server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
281
- raise "Replay completed but the server has no persisted trace for " \
354
+ cause = RuntimeError.new("Replay completed but the server has no persisted trace for " \
282
355
  "any of the #{completed_count} completed item(s) " \
283
356
  "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
284
357
  "flushed, so either the uploads failed or the replayed method is " \
285
- "not traced (no root span was emitted)."
358
+ "not traced (no root span was emitted).")
359
+ error = ReplayError.new(
360
+ cause.message,
361
+ items: public_replay_items(result_items),
362
+ test_run_id:,
363
+ test_run_url: full_test_run_url
364
+ )
365
+ raise error, cause:
286
366
  end
287
367
  # SOME completed items missing: per-item upload failure. Warn, but
288
368
  # return the run; the items that landed can still be labeled.
@@ -301,7 +381,7 @@ module Bitfab
301
381
  result = {
302
382
  items: result_items,
303
383
  test_run_id:,
304
- test_run_url: "#{client.service_url}#{test_run_url}"
384
+ test_run_url: full_test_run_url
305
385
  }
306
386
  # Persist the enriched result two ways so the Bitfab plugin never has to
307
387
  # parse the replay's stdout (which a dependency's logging can corrupt):
@@ -543,7 +623,7 @@ module Bitfab
543
623
  begin
544
624
  dir = File.dirname(result_path)
545
625
  FileUtils.mkdir_p(dir) unless dir.nil? || dir.empty? || dir == "."
546
- File.write(result_path, "#{JSON.pretty_generate(result)}\n")
626
+ File.write(result_path, "#{Bitfab.serialize_replay_result(result)}\n")
547
627
  rescue => e
548
628
  warn "Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (#{result_path}): #{e.message}"
549
629
  end
@@ -593,6 +673,8 @@ module Bitfab
593
673
  result: result[:result],
594
674
  original_output: result[:original_output],
595
675
  error:,
676
+ trace_error: result[:trace_error],
677
+ replay_error: result[:replay_error],
596
678
  duration_ms: result[:duration_ms],
597
679
  tokens: result[:tokens],
598
680
  model: result[:model],
@@ -664,19 +746,29 @@ module Bitfab
664
746
  lease_error = include_db_branch_lease ? server_item["dbBranchLeaseError"] : nil
665
747
  db_snapshot_ref = server_item["dbSnapshotRef"]
666
748
  if include_db_branch_lease && lease.nil? && lease_error.nil?
667
- resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
749
+ begin
750
+ resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
751
+ rescue => cause
752
+ error = DbBranchReplayError.new(
753
+ "lease_request_failed",
754
+ "Bitfab could not request the database branch: #{cause.message}",
755
+ original_trace_id
756
+ )
757
+ raise error, cause:
758
+ end
668
759
  lease = resolved["lease"]
669
760
  lease_error = resolved["leaseError"]
670
761
  db_snapshot_ref = resolved["dbSnapshotRef"] || db_snapshot_ref
671
762
  end
672
763
  if lease_error
673
- raise "Replay requested a database branch for trace #{original_trace_id} but it " \
674
- "could not be resolved (#{lease_error["code"]}): #{lease_error["message"]}. " \
675
- "The method was not run, because replaying it against the live database would " \
676
- "produce a result that looks valid but did not use the historical data you asked for."
764
+ raise DbBranchReplayError.new(
765
+ lease_error["code"].to_s,
766
+ lease_error["message"].to_s,
767
+ original_trace_id
768
+ )
677
769
  end
678
770
 
679
- span = http_client.get_external_span(original_span_id)
771
+ span = http_client.get_external_span(original_span_id, replay_view: true)
680
772
  item_data = extract_span_data(span)
681
773
 
682
774
  # Fetch the span tree when the base strategy needs recorded outputs
@@ -694,7 +786,11 @@ module Bitfab
694
786
  mock_tree = nil
695
787
  if mock_strategy == "all" || mock_strategy == "marked" || overrides_present
696
788
  begin
697
- tree = http_client.get_span_tree(original_span_id, include_outputs:)
789
+ tree = http_client.get_span_tree(
790
+ original_span_id,
791
+ include_outputs:,
792
+ include_root_output: false
793
+ )
698
794
  mock_tree = build_mock_tree(tree["root"] || {})
699
795
  rescue Exception => e # rubocop:disable Lint/RescueException
700
796
  raise if e.is_a?(SystemExit) || e.is_a?(SignalException)
@@ -748,7 +844,9 @@ module Bitfab
748
844
  input: [],
749
845
  result: nil,
750
846
  original_output: nil,
751
- error: e.message,
847
+ error: replay_item_error_message(e),
848
+ trace_error: nil,
849
+ replay_error: e,
752
850
  duration_ms: metrics&.dig(:duration_ms),
753
851
  tokens: metrics&.dig(:tokens),
754
852
  model: metrics&.dig(:model),
@@ -843,7 +941,7 @@ module Bitfab
843
941
  lambda do |external_span_id|
844
942
  return cache[external_span_id] if cache.key?(external_span_id)
845
943
 
846
- span = http_client.get_external_span(external_span_id)
944
+ span = http_client.get_external_span(external_span_id, replay_view: true)
847
945
  span_data = (span["rawData"] || {})["span_data"] || {}
848
946
  # Prefer the Ruby Marshal payload (output_serialized) written by this
849
947
  # SDK; fall back to another SDK's output_meta, then the raw JSON output.
@@ -937,51 +1035,64 @@ module Bitfab
937
1035
 
938
1036
  fn_result = nil
939
1037
  fn_error = nil
1038
+ replay_error = nil
940
1039
  # Client-side correlation id that tags this item's replay spans so the
941
1040
  # server can echo back the row id it minted (resolved in run()'s
942
1041
  # complete-replay loop). Carried on the item under :_sdk_trace_id, never
943
1042
  # surfaced as the public :trace_id.
944
1043
  sdk_trace_id = SecureRandom.uuid
945
1044
 
946
- ReplayContext.with_context(
947
- test_run_id:,
948
- input_source_span_id:,
949
- input_source_trace_id:,
950
- trace_id: sdk_trace_id,
951
- mock_tree:,
952
- mock_strategy:,
953
- mock_overrides:,
954
- fetch_span_output:,
955
- db_branch_lease:,
956
- source_bitfab_trace_id:
957
- ) do
958
- # Reshape recorded inputs onto the current signature when an adapter is
959
- # supplied. Inside the rescue so a raising adapter surfaces on this
960
- # item's :error instead of crashing the run; args is reported on :input.
961
- if adapt_inputs
962
- ctx = adapt_ctx || {
963
- original_trace_id: nil,
964
- original_span_id: input_source_span_id,
965
- # Deprecated aliases for original_trace_id/original_span_id.
966
- source_trace_id: nil,
967
- source_span_id: input_source_span_id
968
- }
969
- args, kwargs = adapt_inputs.call(args, kwargs, ctx)
970
- end
971
- fn_result = if kwargs.empty?
972
- receiver.send(method_name, *args)
973
- else
974
- receiver.send(method_name, *args, **kwargs)
1045
+ begin
1046
+ ReplayContext.with_context(
1047
+ test_run_id:,
1048
+ input_source_span_id:,
1049
+ input_source_trace_id:,
1050
+ trace_id: sdk_trace_id,
1051
+ mock_tree:,
1052
+ mock_strategy:,
1053
+ mock_overrides:,
1054
+ fetch_span_output:,
1055
+ db_branch_lease:,
1056
+ source_bitfab_trace_id:
1057
+ ) do
1058
+ begin
1059
+ if adapt_inputs
1060
+ ctx = adapt_ctx || {
1061
+ original_trace_id: nil,
1062
+ original_span_id: input_source_span_id,
1063
+ # Deprecated aliases for original_trace_id/original_span_id.
1064
+ source_trace_id: nil,
1065
+ source_span_id: input_source_span_id
1066
+ }
1067
+ args, kwargs = adapt_inputs.call(args, kwargs, ctx)
1068
+ end
1069
+ rescue => e
1070
+ replay_error = e
1071
+ end
1072
+ if replay_error.nil?
1073
+ begin
1074
+ fn_result = if kwargs.empty?
1075
+ receiver.send(method_name, *args)
1076
+ else
1077
+ receiver.send(method_name, *args, **kwargs)
1078
+ end
1079
+ rescue => e
1080
+ fn_error = e
1081
+ end
1082
+ end
975
1083
  end
976
1084
  rescue => e
977
- fn_error = e.message
1085
+ replay_error = e
978
1086
  end
979
1087
 
1088
+ item_error = fn_error || replay_error
980
1089
  {
981
1090
  input: args,
982
1091
  result: fn_result,
983
1092
  original_output: item["output"],
984
- error: fn_error,
1093
+ error: item_error&.message,
1094
+ trace_error: fn_error,
1095
+ replay_error:,
985
1096
  duration_ms: metrics[:duration_ms],
986
1097
  tokens: metrics[:tokens],
987
1098
  model: metrics[:model],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.36.1"
4
+ VERSION = "0.36.3"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -159,9 +159,14 @@ module Bitfab
159
159
  # succeeded, errored, and item (the object replay already passes to
160
160
  # on_progress)
161
161
  def report_replay_progress(progress)
162
- warn "#{BITFAB_PROGRESS_PREFIX}#{progress.to_json}"
162
+ warn "#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}"
163
163
  rescue
164
164
  nil
165
165
  end
166
+
167
+ # Serialize a replay result while retaining structured exception fields.
168
+ def serialize_replay_result(result)
169
+ JSON.pretty_generate(Replay.json_safe(result))
170
+ end
166
171
  end
167
172
  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.36.1
4
+ version: 0.36.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team