bitfab 0.36.2 → 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: 44af81a548095fe4bf8a347b6e58bb1c3bdcf2726becd78f029ec5c7c9507ca9
4
- data.tar.gz: 4a00a95452cf45e5992e7eee4754850923eed8767713368a063fa1fed84d4b7c
3
+ metadata.gz: deea3052760a7b677216225f0cd030f4931995b1d10e6b7396c97a57a27ab9c1
4
+ data.tar.gz: 5f64a512db5c8c79f092e5bbf92e16adfc2e20a4a07c869eafab48fb42db5665
5
5
  SHA512:
6
- metadata.gz: 9043c05ffb79c8830299127c39de0dc77b77dc3fa1fd2a573813696bcd9a7ba3fa95d7a06a7371ff596ed04781f439311f773bdff24afe055b252b36e0d36bb5
7
- data.tar.gz: d73327657254114a22c091bb58503c17f1f8d0fcc19f0179c283b452df4c7240d446ff8d36962b16fcc47ea13e0315c80f554280b79f47b684f24ba50b11ce23
6
+ metadata.gz: f9024ab4975b3963e4d2c91aa903b3e4bc7880791aa1f0a4132a9ee847e54e2132339246cbc1067beba8a2130e813738474fcc40a38a1d3b9a0e19ba92633834
7
+ data.tar.gz: b3c4b7c252a566192362e6d1a3e4d31bcad55f752a70527fce5de87ebc9ab75c4839a02fd5d67589823ee36003f3fcb49d5bdade0a315dc8a22971b9769ea9c3
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,16 +746,26 @@ 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
771
  span = http_client.get_external_span(original_span_id, replay_view: true)
@@ -752,7 +844,9 @@ module Bitfab
752
844
  input: [],
753
845
  result: nil,
754
846
  original_output: nil,
755
- error: e.message,
847
+ error: replay_item_error_message(e),
848
+ trace_error: nil,
849
+ replay_error: e,
756
850
  duration_ms: metrics&.dig(:duration_ms),
757
851
  tokens: metrics&.dig(:tokens),
758
852
  model: metrics&.dig(:model),
@@ -941,51 +1035,64 @@ module Bitfab
941
1035
 
942
1036
  fn_result = nil
943
1037
  fn_error = nil
1038
+ replay_error = nil
944
1039
  # Client-side correlation id that tags this item's replay spans so the
945
1040
  # server can echo back the row id it minted (resolved in run()'s
946
1041
  # complete-replay loop). Carried on the item under :_sdk_trace_id, never
947
1042
  # surfaced as the public :trace_id.
948
1043
  sdk_trace_id = SecureRandom.uuid
949
1044
 
950
- ReplayContext.with_context(
951
- test_run_id:,
952
- input_source_span_id:,
953
- input_source_trace_id:,
954
- trace_id: sdk_trace_id,
955
- mock_tree:,
956
- mock_strategy:,
957
- mock_overrides:,
958
- fetch_span_output:,
959
- db_branch_lease:,
960
- source_bitfab_trace_id:
961
- ) do
962
- # Reshape recorded inputs onto the current signature when an adapter is
963
- # supplied. Inside the rescue so a raising adapter surfaces on this
964
- # item's :error instead of crashing the run; args is reported on :input.
965
- if adapt_inputs
966
- ctx = adapt_ctx || {
967
- original_trace_id: nil,
968
- original_span_id: input_source_span_id,
969
- # Deprecated aliases for original_trace_id/original_span_id.
970
- source_trace_id: nil,
971
- source_span_id: input_source_span_id
972
- }
973
- args, kwargs = adapt_inputs.call(args, kwargs, ctx)
974
- end
975
- fn_result = if kwargs.empty?
976
- receiver.send(method_name, *args)
977
- else
978
- 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
979
1083
  end
980
1084
  rescue => e
981
- fn_error = e.message
1085
+ replay_error = e
982
1086
  end
983
1087
 
1088
+ item_error = fn_error || replay_error
984
1089
  {
985
1090
  input: args,
986
1091
  result: fn_result,
987
1092
  original_output: item["output"],
988
- error: fn_error,
1093
+ error: item_error&.message,
1094
+ trace_error: fn_error,
1095
+ replay_error:,
989
1096
  duration_ms: metrics[:duration_ms],
990
1097
  tokens: metrics[:tokens],
991
1098
  model: metrics[:model],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.36.2"
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.2
4
+ version: 0.36.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team