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 +4 -4
- data/lib/bitfab/http_client.rb +10 -5
- data/lib/bitfab/replay.rb +157 -46
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +6 -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: deea3052760a7b677216225f0cd030f4931995b1d10e6b7396c97a57a27ab9c1
|
|
4
|
+
data.tar.gz: 5f64a512db5c8c79f092e5bbf92e16adfc2e20a4a07c869eafab48fb42db5665
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9024ab4975b3963e4d2c91aa903b3e4bc7880791aa1f0a4132a9ee847e54e2132339246cbc1067beba8a2130e813738474fcc40a38a1d3b9a0e19ba92633834
|
|
7
|
+
data.tar.gz: b3c4b7c252a566192362e6d1a3e4d31bcad55f752a70527fce5de87ebc9ab75c4839a02fd5d67589823ee36003f3fcb49d5bdade0a315dc8a22971b9769ea9c3
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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, "#{
|
|
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
|
-
|
|
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
|
|
674
|
-
|
|
675
|
-
"
|
|
676
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
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:
|
|
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],
|
data/lib/bitfab/version.rb
CHANGED
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
|