bitfab 0.61.0 → 0.61.2
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 +42 -8
- data/lib/bitfab/http_client.rb +4 -2
- data/lib/bitfab/replay.rb +32 -11
- data/lib/bitfab/replay_registry.rb +9 -3
- data/lib/bitfab/selective_replay.rb +235 -0
- data/lib/bitfab/simulation_plan.rb +1 -1
- data/lib/bitfab/subtree.rb +6 -1
- data/lib/bitfab/traceable.rb +11 -9
- data/lib/bitfab/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3ee5133d6797e0d931936274c5d05bea38532e5879f8a55567a5417cabdd04d
|
|
4
|
+
data.tar.gz: 57666b69b7277a81a3715a3f386d8660d47d6568ef027adc7a4a10fafc02f6c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a7d3b6f1f05402242f92004139803d1246be8407c3e71a8246331e8cb2d49ed811f0e54005843549490ae385e2bec32be681392241c2a866d13d502339e2e51
|
|
7
|
+
data.tar.gz: 3e2a653d2de7a0d4e64424f3a40ada97e5922968e7862701241efb41a2e3a679e715feb1899ac0e7ace5144bbb4161269b52f2bf5f66e53eef0e9d854397bcf0
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -120,6 +120,13 @@ module Bitfab
|
|
|
120
120
|
# Passed alongside dataset_id or dataset_ids it pins which members of that
|
|
121
121
|
# selection replay, and the server rejects any ID none of those datasets
|
|
122
122
|
# contains
|
|
123
|
+
# @param name [String, nil] What this run is testing, in a few words, such
|
|
124
|
+
# as 'baseline' or 'shorter system prompt'. Bitfab records the commit,
|
|
125
|
+
# branch, tree state, datasets, and who ran it with every experiment, so
|
|
126
|
+
# do not repeat them here.
|
|
127
|
+
# @param notes [String, nil] Run conditions Bitfab cannot see on its own,
|
|
128
|
+
# such as an environment override or a forced feature flag. Kept on the
|
|
129
|
+
# experiment next to its name (max 10,000 characters).
|
|
123
130
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
124
131
|
# @param code_change_description [String, nil] optional rationale for the
|
|
125
132
|
# code change being tested in this replay (stored on the experiment).
|
|
@@ -190,9 +197,9 @@ module Bitfab
|
|
|
190
197
|
# @return [Hash] with :items, :experiment_id, :experiment_url, and the
|
|
191
198
|
# deprecated aliases :test_run_id and :test_run_url carrying the same values
|
|
192
199
|
def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
|
|
193
|
-
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
|
|
200
|
+
name: nil, notes: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
|
|
194
201
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
|
|
195
|
-
on_experiment_start: nil)
|
|
202
|
+
on_experiment_start: nil, experimental_selective_replay: nil)
|
|
196
203
|
Replay.run(
|
|
197
204
|
self,
|
|
198
205
|
receiver,
|
|
@@ -201,6 +208,7 @@ module Bitfab
|
|
|
201
208
|
limit:,
|
|
202
209
|
trace_ids:,
|
|
203
210
|
name:,
|
|
211
|
+
notes:,
|
|
204
212
|
concurrency:,
|
|
205
213
|
max_concurrency:,
|
|
206
214
|
attempts:,
|
|
@@ -220,7 +228,8 @@ module Bitfab
|
|
|
220
228
|
on_item_start:,
|
|
221
229
|
on_item_finish:,
|
|
222
230
|
on_progress:,
|
|
223
|
-
on_experiment_start
|
|
231
|
+
on_experiment_start:,
|
|
232
|
+
experimental_selective_replay:
|
|
224
233
|
)
|
|
225
234
|
end
|
|
226
235
|
|
|
@@ -337,7 +346,7 @@ module Bitfab
|
|
|
337
346
|
# Configure an existing method only when it is discovered beneath a
|
|
338
347
|
# bitfab_trace root owned by this client.
|
|
339
348
|
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
340
|
-
experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
|
|
349
|
+
experiment_id: nil, mock_on_replay: nil, replay_reusable: false, finalize: nil, test_run_id: nil)
|
|
341
350
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
342
351
|
Bitfab::Traceable.node(
|
|
343
352
|
klass,
|
|
@@ -347,6 +356,7 @@ module Bitfab
|
|
|
347
356
|
capture:,
|
|
348
357
|
experiment_id:,
|
|
349
358
|
mock_on_replay:,
|
|
359
|
+
replay_reusable:,
|
|
350
360
|
finalize:,
|
|
351
361
|
client: self
|
|
352
362
|
)
|
|
@@ -383,12 +393,14 @@ module Bitfab
|
|
|
383
393
|
# Execute a block inside a span context, sending trace data on completion.
|
|
384
394
|
# Called by Traceable, not intended for direct use.
|
|
385
395
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
386
|
-
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
396
|
+
capture_when: "always", mock_on_replay: false, replay_reusable: false, independent_root: false,
|
|
387
397
|
explicit_span_receiver: nil, explicit_span_method_name: nil, experiment_id: nil, finalize: nil,
|
|
388
398
|
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
|
|
389
|
-
result_recorded_elsewhere: false, declared_node: false)
|
|
399
|
+
result_recorded_elsewhere: false, declared_node: false, input_block: false)
|
|
390
400
|
return yield unless should_record?
|
|
391
401
|
|
|
402
|
+
selective_input = !input_block && (replay_reusable || mock_on_replay) && SelectiveReplay.inputs(args, kwargs)
|
|
403
|
+
|
|
392
404
|
@http_client.refresh_simulation_plan
|
|
393
405
|
|
|
394
406
|
resolved_capture_when = capture_when.to_s
|
|
@@ -465,7 +477,20 @@ module Bitfab
|
|
|
465
477
|
# line up with `build_mock_tree`'s sequential numbering for the same
|
|
466
478
|
# (key, name) pair. Different (key, name) pairs have independent
|
|
467
479
|
# counters: they cannot shift each other.
|
|
468
|
-
|
|
480
|
+
selective = replay_ctx&.dig(:selective_replay)
|
|
481
|
+
if selective
|
|
482
|
+
mocked, output = selective.enter(trace_function_key:, span_name:, span_id:, parent_span_id:,
|
|
483
|
+
args:, kwargs:, safety_mock: mock_on_replay, reuse_allowed: replay_reusable, can_reuse_output: finalize.nil? && !input_block)
|
|
484
|
+
if mocked
|
|
485
|
+
send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:, span_name:, span_type:,
|
|
486
|
+
function_name:, args:, kwargs:, mocked_output: output, started_at:,
|
|
487
|
+
experiment_id: resolved_experiment_id, input_source_span_id: resolved_input_source_span_id,
|
|
488
|
+
mock_source: "recorded", instrumentation: (surface == :trace) ? "trace" : "span", declared_node:)
|
|
489
|
+
on_complete&.call(result: output, error: nil, ended_at: Bitfab.now_iso_timestamp)
|
|
490
|
+
return output
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
call_index = selective ? nil : advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
469
494
|
if call_index
|
|
470
495
|
mock_result = check_mock_replay(
|
|
471
496
|
replay_ctx,
|
|
@@ -561,6 +586,14 @@ module Bitfab
|
|
|
561
586
|
|
|
562
587
|
on_complete&.call(result: recorded_result, error: recorded_error, ended_at:)
|
|
563
588
|
|
|
589
|
+
span_data = span_data.merge("replay_reusable" => replay_reusable)
|
|
590
|
+
output_json = finalize.nil? && !result.is_a?(Enumerator) && SelectiveReplay.fingerprint(final_result)
|
|
591
|
+
if selective_input && output_json && recorded_error.nil? && !content_off
|
|
592
|
+
recorded_args, recorded_kwargs = JSON.parse(selective_input)
|
|
593
|
+
span_data = span_data.merge("replay_json_safe" => true,
|
|
594
|
+
"replay_recording" => {"input" => recorded_args, "kwargs" => recorded_kwargs, "output" => JSON.parse(output_json)})
|
|
595
|
+
end
|
|
596
|
+
|
|
564
597
|
send_span(
|
|
565
598
|
span_data: content_off ? span_data.merge("content_off_by_simulation_plan" => true) : span_data,
|
|
566
599
|
instrumentation: recorded_instrumentation,
|
|
@@ -1246,7 +1279,7 @@ module Bitfab
|
|
|
1246
1279
|
# values warn once and default to "always"
|
|
1247
1280
|
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
1248
1281
|
# mock strategy. A missing selected occurrence fails closed.
|
|
1249
|
-
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
1282
|
+
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, replay_reusable: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
1250
1283
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
1251
1284
|
Bitfab::Traceable.wrap(
|
|
1252
1285
|
klass,
|
|
@@ -1256,6 +1289,7 @@ module Bitfab
|
|
|
1256
1289
|
type:,
|
|
1257
1290
|
capture_when:,
|
|
1258
1291
|
mock_on_replay:,
|
|
1292
|
+
replay_reusable:,
|
|
1259
1293
|
finalize:,
|
|
1260
1294
|
experiment_id:,
|
|
1261
1295
|
client: @client
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -261,8 +261,8 @@ module Bitfab
|
|
|
261
261
|
# runnable graders at completion; each must be an active/live grader in the
|
|
262
262
|
# same org and trace function or the server rejects the replay
|
|
263
263
|
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
|
|
264
|
-
code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_ids: nil,
|
|
265
|
-
grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, judge_assertions: false, include_original_metadata: false)
|
|
264
|
+
code_change_files: nil, experiment_group_id: nil, name: nil, notes: nil, include_db_branch_lease: false, dataset_ids: nil,
|
|
265
|
+
grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, judge_assertions: false, include_original_metadata: false, experimental_selective_replay: nil)
|
|
266
266
|
payload = {
|
|
267
267
|
"traceFunctionKey" => trace_function_key
|
|
268
268
|
}
|
|
@@ -271,6 +271,7 @@ module Bitfab
|
|
|
271
271
|
payload["limit"] = limit unless limit.nil?
|
|
272
272
|
payload["traceIds"] = trace_ids if trace_ids
|
|
273
273
|
payload["name"] = name unless name.nil?
|
|
274
|
+
payload["notes"] = notes unless notes.nil?
|
|
274
275
|
payload["codeChangeDescription"] = code_change_description unless code_change_description.nil?
|
|
275
276
|
payload["codeChangeFiles"] = normalize_code_change_files(code_change_files) unless code_change_files.nil?
|
|
276
277
|
payload["experimentGroupId"] = experiment_group_id unless experiment_group_id.nil?
|
|
@@ -288,6 +289,7 @@ module Bitfab
|
|
|
288
289
|
payload["onlyWithAssertions"] = true if only_with_assertions
|
|
289
290
|
payload["judgeAssertions"] = true if judge_assertions
|
|
290
291
|
payload["includeOriginalMetadata"] = true if include_original_metadata
|
|
292
|
+
payload["experimentalSelectiveReplay"] = experimental_selective_replay if experimental_selective_replay
|
|
291
293
|
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
292
294
|
git_state = GitState.resolved
|
|
293
295
|
payload["git"] = git_state if git_state
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "constants"
|
|
|
9
9
|
require_relative "git_command"
|
|
10
10
|
require_relative "mock_override"
|
|
11
11
|
require_relative "serialize"
|
|
12
|
+
require_relative "selective_replay"
|
|
12
13
|
require_relative "traceable"
|
|
13
14
|
require_relative "transport"
|
|
14
15
|
|
|
@@ -85,10 +86,11 @@ module Bitfab
|
|
|
85
86
|
# thread. Child threads and processes intentionally do not inherit it.
|
|
86
87
|
def with_context(experiment_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
87
88
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
88
|
-
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil, replay_attempt: nil)
|
|
89
|
+
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil, replay_attempt: nil, selective_replay: nil)
|
|
89
90
|
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
90
91
|
ctx = {
|
|
91
92
|
replay_attempt:,
|
|
93
|
+
selective_replay:,
|
|
92
94
|
experiment_id:,
|
|
93
95
|
input_source_span_id:,
|
|
94
96
|
input_source_trace_id:,
|
|
@@ -203,7 +205,13 @@ module Bitfab
|
|
|
203
205
|
# already determines how many traces replay. Supplying trace_ids also
|
|
204
206
|
# emits a warning.
|
|
205
207
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
206
|
-
# @param name [String, nil]
|
|
208
|
+
# @param name [String, nil] What this run is testing, in a few words, such
|
|
209
|
+
# as 'baseline' or 'shorter system prompt'. Bitfab records the commit,
|
|
210
|
+
# branch, tree state, datasets, and who ran it with every experiment, so
|
|
211
|
+
# do not repeat them here.
|
|
212
|
+
# @param notes [String, nil] Run conditions Bitfab cannot see on its own,
|
|
213
|
+
# such as an environment override or a forced feature flag. Kept on the
|
|
214
|
+
# experiment next to its name (max 10,000 characters).
|
|
207
215
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
208
216
|
# @param code_change_description [String, nil] optional rationale for the
|
|
209
217
|
# code change being tested in this replay (stored on the experiment).
|
|
@@ -272,11 +280,11 @@ module Bitfab
|
|
|
272
280
|
# crashes the run.
|
|
273
281
|
# @return [Hash] with :items, :experiment_id, :experiment_url, and the
|
|
274
282
|
# deprecated aliases :test_run_id and :test_run_url carrying the same values
|
|
275
|
-
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
283
|
+
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil, notes: nil,
|
|
276
284
|
concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil,
|
|
277
285
|
dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
|
|
278
286
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
|
|
279
|
-
on_experiment_start: nil)
|
|
287
|
+
on_experiment_start: nil, experimental_selective_replay: nil)
|
|
280
288
|
concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
|
|
281
289
|
attempts = concurrency_options.attempts
|
|
282
290
|
max_concurrency = concurrency_options.max_concurrency
|
|
@@ -337,6 +345,12 @@ module Bitfab
|
|
|
337
345
|
# both take precedence over the base mock strategy.
|
|
338
346
|
registered_overrides = client.instance_variable_get(:@mock_overrides) || []
|
|
339
347
|
resolved_overrides = MockOverride.normalize(mock_override) + registered_overrides
|
|
348
|
+
if experimental_selective_replay
|
|
349
|
+
SelectiveReplay.wire_options(experimental_selective_replay)
|
|
350
|
+
unless mock.to_s == "marked" && resolved_overrides.empty? && concurrency_options.primitive != "process"
|
|
351
|
+
raise ArgumentError, "experimental_selective_replay requires marked mocks, no overrides, and in-process replay"
|
|
352
|
+
end
|
|
353
|
+
end
|
|
340
354
|
|
|
341
355
|
# limit is meaningless with explicit trace_ids (the ID list determines
|
|
342
356
|
# the count), so it's omitted from the request entirely.
|
|
@@ -363,6 +377,7 @@ module Bitfab
|
|
|
363
377
|
effective_limit,
|
|
364
378
|
trace_ids:,
|
|
365
379
|
name:,
|
|
380
|
+
notes:,
|
|
366
381
|
code_change_description:,
|
|
367
382
|
code_change_files:,
|
|
368
383
|
experiment_group_id:,
|
|
@@ -373,7 +388,8 @@ module Bitfab
|
|
|
373
388
|
only_with_assertions:,
|
|
374
389
|
judge_assertions:,
|
|
375
390
|
include_original_metadata: !adapt_inputs.nil?,
|
|
376
|
-
db_branch_settings: resolved_db_branch_settings
|
|
391
|
+
db_branch_settings: resolved_db_branch_settings,
|
|
392
|
+
**(experimental_selective_replay ? {experimental_selective_replay: SelectiveReplay.wire_options(experimental_selective_replay)} : {})
|
|
377
393
|
)
|
|
378
394
|
experiment_id = replay_data["experimentId"] || replay_data["testRunId"]
|
|
379
395
|
experiment_url = replay_data["experimentUrl"] || replay_data["testRunUrl"]
|
|
@@ -400,7 +416,7 @@ module Bitfab
|
|
|
400
416
|
process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock.to_s,
|
|
401
417
|
adapt_inputs, include_db_branch_lease, resolved_db_branch_settings, on_item_start:,
|
|
402
418
|
on_item_finish: item_finish_callback,
|
|
403
|
-
mock_overrides: resolved_overrides, dry_run:, process_executor:)
|
|
419
|
+
mock_overrides: resolved_overrides, dry_run:, process_executor:, experimental_selective_replay:)
|
|
404
420
|
else
|
|
405
421
|
[]
|
|
406
422
|
end
|
|
@@ -776,7 +792,7 @@ module Bitfab
|
|
|
776
792
|
# Process all replay items, optionally in parallel using threads.
|
|
777
793
|
def process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock_strategy,
|
|
778
794
|
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, on_item_start: nil, on_item_finish: nil,
|
|
779
|
-
mock_overrides: [], dry_run: false, process_executor: nil)
|
|
795
|
+
mock_overrides: [], dry_run: false, process_executor: nil, experimental_selective_replay: nil)
|
|
780
796
|
concurrency = max_concurrency || server_items.length
|
|
781
797
|
|
|
782
798
|
# Lifecycle callbacks run from worker threads in the parallel path, so the
|
|
@@ -887,7 +903,7 @@ module Bitfab
|
|
|
887
903
|
process_executor.call(item, experiment_id, mock_strategy, include_db_branch_lease, db_branch_settings, dry_run:)
|
|
888
904
|
else
|
|
889
905
|
process_single_item(http_client, item, receiver, method_name, experiment_id, mock_strategy,
|
|
890
|
-
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:, dry_run:)
|
|
906
|
+
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:, dry_run:, experimental_selective_replay:)
|
|
891
907
|
end
|
|
892
908
|
end
|
|
893
909
|
|
|
@@ -930,7 +946,7 @@ module Bitfab
|
|
|
930
946
|
# than propagated, so one bad trace never aborts the whole replay run
|
|
931
947
|
# (mirrors the TypeScript and Python SDKs' per-item rescue).
|
|
932
948
|
def process_single_item(http_client, server_item, receiver, method_name, experiment_id, mock_strategy,
|
|
933
|
-
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [], dry_run: false)
|
|
949
|
+
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [], dry_run: false, experimental_selective_replay: nil)
|
|
934
950
|
metrics = extract_server_item_metrics(server_item)
|
|
935
951
|
attempt = server_item["attempt"] || 0
|
|
936
952
|
# The ORIGINAL (historical) trace/span this item replays. Canonical
|
|
@@ -945,6 +961,7 @@ module Bitfab
|
|
|
945
961
|
# snapshot ref arrive without a lease, so the app uses its normal DB
|
|
946
962
|
# path. Unsafe calls on that path still require replay mocking.
|
|
947
963
|
lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
|
|
964
|
+
selective_replay = experimental_selective_replay && SelectiveReplay.new(server_item["selectiveReplayPlan"], experimental_selective_replay)
|
|
948
965
|
# A resolve that was ATTEMPTED and failed is different: the caller asked
|
|
949
966
|
# for a branch, so running their method against live data would produce a
|
|
950
967
|
# result that looks valid and is not. Fail the item instead, loudly.
|
|
@@ -994,7 +1011,7 @@ module Bitfab
|
|
|
994
1011
|
overrides_present = !mock_overrides.nil? && !mock_overrides.empty?
|
|
995
1012
|
include_outputs = mock_strategy == "all"
|
|
996
1013
|
mock_tree = nil
|
|
997
|
-
if !dry_run && (mock_strategy == "all" || mock_strategy == "marked" || overrides_present)
|
|
1014
|
+
if !selective_replay && !dry_run && (mock_strategy == "all" || mock_strategy == "marked" || overrides_present)
|
|
998
1015
|
begin
|
|
999
1016
|
tree = http_client.get_span_tree(
|
|
1000
1017
|
original_span_id,
|
|
@@ -1035,6 +1052,7 @@ module Bitfab
|
|
|
1035
1052
|
span["id"],
|
|
1036
1053
|
metrics,
|
|
1037
1054
|
input_source_trace_id: span["externalTraceId"],
|
|
1055
|
+
selective_replay:,
|
|
1038
1056
|
mock_strategy:,
|
|
1039
1057
|
mock_tree:,
|
|
1040
1058
|
mock_overrides:,
|
|
@@ -1282,7 +1300,7 @@ module Bitfab
|
|
|
1282
1300
|
def execute_item(item, receiver, method_name, experiment_id, input_source_span_id = nil, metrics = {},
|
|
1283
1301
|
input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
|
|
1284
1302
|
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
|
|
1285
|
-
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil, attempt: 0, dry_run: false)
|
|
1303
|
+
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil, attempt: 0, dry_run: false, selective_replay: nil)
|
|
1286
1304
|
args, kwargs = Serialize.deserialize_inputs(item)
|
|
1287
1305
|
|
|
1288
1306
|
fn_result = nil
|
|
@@ -1303,6 +1321,7 @@ module Bitfab
|
|
|
1303
1321
|
begin
|
|
1304
1322
|
ReplayContext.with_context(
|
|
1305
1323
|
replay_attempt: attempt,
|
|
1324
|
+
selective_replay:,
|
|
1306
1325
|
experiment_id:,
|
|
1307
1326
|
input_source_span_id:,
|
|
1308
1327
|
input_source_trace_id:,
|
|
@@ -1338,6 +1357,7 @@ module Bitfab
|
|
|
1338
1357
|
receiver.send(method_name, *args, **kwargs)
|
|
1339
1358
|
end
|
|
1340
1359
|
fn_result = fn_result.to_a if fn_result.is_a?(Enumerator)
|
|
1360
|
+
selective_replay&.assert_no_safety_conflict!
|
|
1341
1361
|
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1342
1362
|
rescue => e
|
|
1343
1363
|
# The method ran and raised, so it still has a duration.
|
|
@@ -1357,6 +1377,7 @@ module Bitfab
|
|
|
1357
1377
|
|
|
1358
1378
|
item_error = fn_error || replay_error
|
|
1359
1379
|
{
|
|
1380
|
+
**(selective_replay ? {selective_replay: selective_replay.report} : {}),
|
|
1360
1381
|
attempt:,
|
|
1361
1382
|
ingestion_type: metrics[:ingestion_type],
|
|
1362
1383
|
input: kwargs.empty? ? args : [*args, kwargs],
|
|
@@ -19,7 +19,7 @@ module Bitfab
|
|
|
19
19
|
# Project-owned registry of production replay roots.
|
|
20
20
|
class ReplayRegistry
|
|
21
21
|
OPTION_NAMES = %i[
|
|
22
|
-
limit trace_ids name concurrency max_concurrency code_change_description
|
|
22
|
+
limit trace_ids name notes concurrency max_concurrency code_change_description
|
|
23
23
|
code_change_files experiment_group_id dataset_id dataset_ids grader_ids mock
|
|
24
24
|
mock_override adapt_inputs db_branch attempts only_with_assertions judge_assertions dry_run on_item_finish
|
|
25
25
|
].freeze
|
|
@@ -186,7 +186,7 @@ module Bitfab
|
|
|
186
186
|
options[:limit] = bound || 10
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
-
%i[name max_concurrency experiment_group_id dataset_ids grader_ids mock attempts only_with_assertions dry_run].each do |key|
|
|
189
|
+
%i[name notes max_concurrency experiment_group_id dataset_ids grader_ids mock attempts only_with_assertions dry_run].each do |key|
|
|
190
190
|
options[key] = args[key] unless args[key].nil?
|
|
191
191
|
end
|
|
192
192
|
|
|
@@ -284,7 +284,13 @@ module Bitfab
|
|
|
284
284
|
options.on("--seed PATH", "--cases PATH") { |value| args[:seed] = value }
|
|
285
285
|
options.on("--run") { args[:run] = true }
|
|
286
286
|
options.on("--from-trace IDS") { |value| args[:from_trace] = comma_separated("--from-trace", value) }
|
|
287
|
-
options.on("--name NAME"
|
|
287
|
+
options.on("--name NAME",
|
|
288
|
+
"What this run is testing, in a few words, such as 'baseline' or 'shorter system prompt'. " \
|
|
289
|
+
"Bitfab records the commit, branch, tree state, datasets, and who ran it with every experiment, " \
|
|
290
|
+
"so do not repeat them here.") { |value| args[:name] = value }
|
|
291
|
+
options.on("--notes TEXT",
|
|
292
|
+
"Run conditions Bitfab cannot see on its own, such as an environment override or a forced feature flag. " \
|
|
293
|
+
"Kept on the experiment next to its name.") { |value| args[:notes] = value }
|
|
288
294
|
options.on("--concurrency N", Integer) do |value|
|
|
289
295
|
args[:max_concurrency] = positive_integer("--concurrency", value)
|
|
290
296
|
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class SelectiveReplay
|
|
7
|
+
def self.wire_options(options)
|
|
8
|
+
valid = options.is_a?(Hash) && options.keys == [:must_run] && options[:must_run].is_a?(Array) &&
|
|
9
|
+
options[:must_run].length.between?(1, 100) && options[:must_run].all? do |node|
|
|
10
|
+
node.is_a?(Hash) && node.keys.sort == [:span_name, :trace_function_key] &&
|
|
11
|
+
node.values.all? { |value| value.is_a?(String) && value.length.between?(1, 500) }
|
|
12
|
+
end
|
|
13
|
+
raise ArgumentError, "experimental_selective_replay accepts only 1–100 must_run identities" unless valid
|
|
14
|
+
|
|
15
|
+
{"mustRun" => options[:must_run].map { |node| {"traceFunctionKey" => node[:trace_function_key], "spanName" => node[:span_name]} }}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Strict JSON avoids silently changing Ruby classes, symbol keys, numeric
|
|
19
|
+
# types, aliases, or insertion order when a recorded output is returned.
|
|
20
|
+
def self.fingerprint(value)
|
|
21
|
+
seen = Set.new
|
|
22
|
+
validate = lambda do |item|
|
|
23
|
+
case item
|
|
24
|
+
when NilClass, TrueClass, FalseClass
|
|
25
|
+
true
|
|
26
|
+
when String
|
|
27
|
+
item.instance_of?(String) && seen.add?(item.object_id) && !item.start_with?("<unserializable")
|
|
28
|
+
when Integer
|
|
29
|
+
item.abs <= (2**53 - 1)
|
|
30
|
+
when Float
|
|
31
|
+
item.finite? && item != item.to_i
|
|
32
|
+
when Array, Hash
|
|
33
|
+
next false unless item.instance_of?(Array) || item.instance_of?(Hash)
|
|
34
|
+
next false unless seen.add?(item.object_id)
|
|
35
|
+
|
|
36
|
+
if item.is_a?(Array)
|
|
37
|
+
item.all? { |child| validate.call(child) }
|
|
38
|
+
else
|
|
39
|
+
item.default.nil? && item.default_proc.nil? && !item.compare_by_identity? && item.all? do |key, child|
|
|
40
|
+
key.instance_of?(String) && !key.match?(/\A(?:0|[1-9][0-9]*)\z/) && validate.call(child)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
validate.call(value) ? JSON.generate(value) : nil
|
|
48
|
+
rescue StandardError, SystemStackError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.inputs(args, kwargs)
|
|
53
|
+
return nil unless kwargs.keys.all? { |key| key.is_a?(Symbol) }
|
|
54
|
+
|
|
55
|
+
fingerprint([args, kwargs.transform_keys(&:to_s)])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(plan, options)
|
|
59
|
+
expected = self.class.wire_options(options)
|
|
60
|
+
unless plan.is_a?(Hash) && plan["version"] == 4 && plan["nodes"].is_a?(Array) &&
|
|
61
|
+
plan["nodes"].length.between?(1, 500) && plan["assertions"].is_a?(Array) &&
|
|
62
|
+
plan["unresolvedMustRun"].is_a?(Array)
|
|
63
|
+
raise ArgumentError, "Server did not return a compatible selective replay plan"
|
|
64
|
+
end
|
|
65
|
+
raise ArgumentError, "Selective replay plan does not match must_run" unless plan["options"] == expected
|
|
66
|
+
|
|
67
|
+
@plan = JSON.parse(JSON.generate(plan))
|
|
68
|
+
@required = expected["mustRun"].map { |node| key(node) }.to_set
|
|
69
|
+
@nodes = {}
|
|
70
|
+
@children = Hash.new { |hash, id| hash[id] = [] }
|
|
71
|
+
@recordings = {}
|
|
72
|
+
@parents = {}
|
|
73
|
+
@consumed = Hash.new { |hash, id| hash[id] = Set.new }
|
|
74
|
+
@required_scopes = Set.new
|
|
75
|
+
@assertion_scopes = Set.new
|
|
76
|
+
@assertion_names = Set.new
|
|
77
|
+
@unresolved_assertions = []
|
|
78
|
+
@observed = Hash.new(0)
|
|
79
|
+
@reached = Set.new
|
|
80
|
+
@decisions = []
|
|
81
|
+
@mutex = Mutex.new
|
|
82
|
+
@plan["nodes"].each do |node|
|
|
83
|
+
valid_node = node.is_a?(Hash) && node["id"].is_a?(String) && !@nodes.key?(node["id"]) &&
|
|
84
|
+
node["spanName"].is_a?(String) && node["traceFunctionKey"].is_a?(String) &&
|
|
85
|
+
[true, false].include?(node["mustRun"]) && [true, false].include?(node["reusable"]) &&
|
|
86
|
+
node.key?("parentId") && (node["parentId"].nil? || node["parentId"].is_a?(String))
|
|
87
|
+
raise ArgumentError, "Invalid selective replay node" unless valid_node
|
|
88
|
+
@nodes[node["id"]] = node
|
|
89
|
+
@children[node["parentId"]] << node unless node["parentId"].nil?
|
|
90
|
+
recording = node["recording"]
|
|
91
|
+
if recording.is_a?(Hash) && recording.key?("output")
|
|
92
|
+
@recordings[node["id"]] = [self.class.fingerprint([recording["input"], recording.fetch("kwargs", {})]), self.class.fingerprint(recording["output"])]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
root = @nodes[@plan["rootId"]]
|
|
96
|
+
raise ArgumentError, "Selective replay requires a live recorded root" unless root && root["parentId"].nil? && root["mustRun"]
|
|
97
|
+
|
|
98
|
+
@full_trace = false
|
|
99
|
+
ids = Set.new
|
|
100
|
+
@plan["assertions"].each do |assertion|
|
|
101
|
+
unless assertion.is_a?(Hash) && assertion["id"].is_a?(String) && !assertion["id"].empty? && ids.add?(assertion["id"]) && assertion.key?("target")
|
|
102
|
+
raise ArgumentError, "Invalid selective replay assertion"
|
|
103
|
+
end
|
|
104
|
+
target = assertion["target"]
|
|
105
|
+
if target.nil?
|
|
106
|
+
@full_trace = true
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
raise ArgumentError, "Invalid selective replay assertion target" unless target.is_a?(Hash) && %w[output span].include?(target["kind"])
|
|
110
|
+
next if target["kind"] == "output"
|
|
111
|
+
|
|
112
|
+
occurrence = target.fetch("occurrence", "first")
|
|
113
|
+
valid_target = target["name"].is_a?(String) && !target["name"].empty? &&
|
|
114
|
+
(%w[first last].include?(occurrence) || (occurrence.is_a?(Integer) && occurrence >= 0))
|
|
115
|
+
raise ArgumentError, "Invalid selective replay assertion target" unless valid_target
|
|
116
|
+
@assertion_names.add(target["name"])
|
|
117
|
+
count = @nodes.values.count { |node| node["spanName"] == target["name"] }
|
|
118
|
+
@unresolved_assertions << assertion["id"] if count <= (occurrence.is_a?(Integer) ? occurrence : 0)
|
|
119
|
+
end
|
|
120
|
+
@full_trace ||= !@unresolved_assertions.empty?
|
|
121
|
+
unresolved = @required - @nodes.values.map { |node| key(node) }.to_set
|
|
122
|
+
unless unresolved == @plan["unresolvedMustRun"].map { |node| key(node) }.to_set
|
|
123
|
+
raise ArgumentError, "Selective replay plan has incomplete must_run coverage"
|
|
124
|
+
end
|
|
125
|
+
@nodes.each_value do |node|
|
|
126
|
+
ancestors = Set.new([node["id"]])
|
|
127
|
+
required = @required.include?(key(node)) || !unresolved.empty?
|
|
128
|
+
asserted = @full_trace || @assertion_names.include?(node["spanName"])
|
|
129
|
+
parent_id = node["parentId"]
|
|
130
|
+
until parent_id.nil?
|
|
131
|
+
parent = @nodes[parent_id]
|
|
132
|
+
unless parent && ancestors.add?(parent_id)
|
|
133
|
+
raise ArgumentError, "Invalid selective replay ancestry"
|
|
134
|
+
end
|
|
135
|
+
if node["mustRun"] && !parent["mustRun"]
|
|
136
|
+
raise ArgumentError, "Invalid selective replay ancestry"
|
|
137
|
+
end
|
|
138
|
+
required ||= @required.include?(key(parent))
|
|
139
|
+
asserted ||= @assertion_names.include?(parent["spanName"])
|
|
140
|
+
parent_id = parent["parentId"]
|
|
141
|
+
end
|
|
142
|
+
if !ancestors.include?(@plan["rootId"]) || ((required || asserted) && !node["mustRun"])
|
|
143
|
+
raise ArgumentError, "Selective replay plan could hide required code or assertion evidence"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def enter(trace_function_key:, span_name:, span_id:, parent_span_id:, args:, kwargs:, safety_mock:, reuse_allowed:, can_reuse_output: true)
|
|
149
|
+
@mutex.synchronize do
|
|
150
|
+
identity = [trace_function_key, span_name]
|
|
151
|
+
input = self.class.inputs(args, kwargs)
|
|
152
|
+
node = if parent_span_id.nil?
|
|
153
|
+
@nodes[@plan["rootId"]]
|
|
154
|
+
else
|
|
155
|
+
candidates = @children[@parents[parent_span_id]].select { |candidate| key(candidate) == identity && !@consumed[parent_span_id].include?(candidate["id"]) }
|
|
156
|
+
exact = candidates.select { |candidate| input && @recordings.dig(candidate["id"], 0) == input }
|
|
157
|
+
match = if exact.length == 1
|
|
158
|
+
exact.first
|
|
159
|
+
else
|
|
160
|
+
((exact.empty? && candidates.length == 1) ? candidates.first : nil)
|
|
161
|
+
end
|
|
162
|
+
(match ? [match] : candidates).each { |candidate| @consumed[parent_span_id].add(candidate["id"]) }
|
|
163
|
+
match
|
|
164
|
+
end
|
|
165
|
+
@parents[span_id] = node&.dig("id")
|
|
166
|
+
required = @required.include?(identity) || @required_scopes.include?(parent_span_id)
|
|
167
|
+
@required_scopes.add(span_id) if required
|
|
168
|
+
asserted = @full_trace || @assertion_names.include?(span_name) || @assertion_scopes.include?(parent_span_id)
|
|
169
|
+
@assertion_scopes.add(span_id) if asserted
|
|
170
|
+
must_run = parent_span_id.nil? || required || asserted || node&.dig("mustRun") || !@plan["unresolvedMustRun"].empty?
|
|
171
|
+
recorded_input, recorded_output = @recordings[node&.dig("id")]
|
|
172
|
+
reusable = input && input == recorded_input && recorded_output && can_reuse_output
|
|
173
|
+
reason = if required
|
|
174
|
+
"must-run"
|
|
175
|
+
elsif asserted
|
|
176
|
+
"assertion"
|
|
177
|
+
elsif !@plan["unresolvedMustRun"].empty?
|
|
178
|
+
"unresolved-must-run"
|
|
179
|
+
elsif !node
|
|
180
|
+
"unmatched-or-ambiguous-call"
|
|
181
|
+
elsif !reusable
|
|
182
|
+
"input-drift-or-missing-recording"
|
|
183
|
+
else
|
|
184
|
+
node["reason"]
|
|
185
|
+
end
|
|
186
|
+
decision = {trace_function_key:, span_name:, original_span_id: node&.dig("id"), reason:}
|
|
187
|
+
if safety_mock && (must_run || !reusable)
|
|
188
|
+
@decisions << decision.merge(action: "blocked", reason: "safety-conflict:#{reason}")
|
|
189
|
+
@failure = RuntimeError.new("Selective replay cannot execute or safely reuse safety-mocked node #{span_name} (#{reason})")
|
|
190
|
+
raise @failure
|
|
191
|
+
end
|
|
192
|
+
if !must_run && reusable && (safety_mock || (node["reusable"] && reuse_allowed))
|
|
193
|
+
@decisions << decision.merge(action: "mock", reason: safety_mock ? "safety-mock" : "unchanged-inputs")
|
|
194
|
+
return [true, JSON.parse(recorded_output)]
|
|
195
|
+
end
|
|
196
|
+
@decisions << decision.merge(action: "run")
|
|
197
|
+
@observed[span_name] += 1
|
|
198
|
+
@reached.add(identity) if @required.include?(identity)
|
|
199
|
+
[false, nil]
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def assert_no_safety_conflict!
|
|
204
|
+
raise @failure if @failure
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def report
|
|
208
|
+
@mutex.synchronize do
|
|
209
|
+
{
|
|
210
|
+
decisions: @decisions.map(&:dup),
|
|
211
|
+
must_run_not_reached: @plan["options"]["mustRun"].reject { |node| @reached.include?(key(node)) }.map { |node| identity(node) },
|
|
212
|
+
unresolved_must_run: @plan["unresolvedMustRun"].map { |node| identity(node) },
|
|
213
|
+
assertion_ids: @plan["assertions"].map { |assertion| assertion["id"] },
|
|
214
|
+
unresolved_assertions: @unresolved_assertions.dup,
|
|
215
|
+
assertion_targets_not_reached: @plan["assertions"].filter_map do |assertion|
|
|
216
|
+
target = assertion["target"]
|
|
217
|
+
if target && target["kind"] == "span" && @observed[target["name"]] <= (target["occurrence"].is_a?(Integer) ? target["occurrence"] : 0)
|
|
218
|
+
assertion["id"]
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def key(node)
|
|
228
|
+
[node.fetch("traceFunctionKey"), node.fetch("spanName")]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def identity(node)
|
|
232
|
+
{trace_function_key: node["traceFunctionKey"], span_name: node["spanName"]}
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Bitfab
|
|
4
4
|
class SimulationPlan
|
|
5
|
-
CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized prompt].freeze
|
|
5
|
+
CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized replay_recording prompt].freeze
|
|
6
6
|
FRAMEWORKS = %w[openai-agents langgraph claude-agent-sdk vercel-ai].freeze
|
|
7
7
|
READ_TIMEOUT = 5.0
|
|
8
8
|
REFRESH_SECONDS = 60.0
|
data/lib/bitfab/subtree.rb
CHANGED
|
@@ -667,10 +667,15 @@ module Bitfab
|
|
|
667
667
|
active_parent[:span_id]
|
|
668
668
|
end
|
|
669
669
|
subtree_parent_span_id = stack.reverse_each.find { |entry| entry.span_id }&.span_id
|
|
670
|
+
parent_span_id = subtree_parent_span_id || @fiber_parents.dig(fiber_id, :span_id) || active_parent_span_id || @root_span_id
|
|
671
|
+
ReplayContext.current&.dig(:selective_replay)&.enter(
|
|
672
|
+
trace_function_key: @trace_function_key, span_name: metadata.name, span_id:, parent_span_id:,
|
|
673
|
+
args:, kwargs:, safety_mock: false, reuse_allowed: false, can_reuse_output: false
|
|
674
|
+
)
|
|
670
675
|
stack << OpenSpan.new(
|
|
671
676
|
identity: metadata.identity,
|
|
672
677
|
span_id:,
|
|
673
|
-
parent_span_id
|
|
678
|
+
parent_span_id:,
|
|
674
679
|
name: metadata.name,
|
|
675
680
|
args:,
|
|
676
681
|
kwargs:,
|
data/lib/bitfab/traceable.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Bitfab
|
|
|
53
53
|
# preserve the bound client through the fluent wrapper, matching Python's
|
|
54
54
|
# `BitfabFunction.span()` and TypeScript's `BitfabFunction.withSpan()`.
|
|
55
55
|
def self.wrap(klass, method_name, trace_function_key:, name: nil, type: "custom",
|
|
56
|
-
capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, client: nil, test_run_id: nil)
|
|
56
|
+
capture_when: "always", mock_on_replay: false, replay_reusable: false, finalize: nil, experiment_id: nil, client: nil, test_run_id: nil)
|
|
57
57
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
58
58
|
method_name_str = method_name.to_s
|
|
59
59
|
bound_client = client
|
|
@@ -67,7 +67,7 @@ module Bitfab
|
|
|
67
67
|
trace_function_key:,
|
|
68
68
|
span_name: name || Subtree.qualified_name(self, method_name),
|
|
69
69
|
span_type: type, function_name: method_name_str, args:, kwargs:,
|
|
70
|
-
capture_when:, mock_on_replay:, finalize:, experiment_id:, surface: :span,
|
|
70
|
+
capture_when:, mock_on_replay:, replay_reusable:, finalize:, experiment_id:, surface: :span, input_block: !block.nil?,
|
|
71
71
|
explicit_span_receiver: self, explicit_span_method_name: method_name) do
|
|
72
72
|
super(*args, **kwargs, &block)
|
|
73
73
|
end
|
|
@@ -135,7 +135,7 @@ module Bitfab
|
|
|
135
135
|
# Configure an existing method only when it is discovered beneath a
|
|
136
136
|
# bitfab_trace root. Outside subtree capture the method runs unchanged.
|
|
137
137
|
def self.node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
138
|
-
experiment_id: nil, mock_on_replay: nil, finalize: nil, client: nil, test_run_id: nil)
|
|
138
|
+
experiment_id: nil, mock_on_replay: nil, replay_reusable: false, finalize: nil, client: nil, test_run_id: nil)
|
|
139
139
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
140
140
|
validate_node_options!(capture:, mock_on_replay:)
|
|
141
141
|
method_name_str = method_name.to_s
|
|
@@ -164,7 +164,7 @@ module Bitfab
|
|
|
164
164
|
call.owner.client.send(:execute_span,
|
|
165
165
|
trace_function_key: call.owner.trace_function_key,
|
|
166
166
|
span_name:, span_type: type, function_name: method_name_str,
|
|
167
|
-
args:, kwargs:, capture_when: "nested", mock_on_replay: policy,
|
|
167
|
+
args:, kwargs:, capture_when: "nested", mock_on_replay: policy, replay_reusable:, input_block: !block.nil?,
|
|
168
168
|
explicit_span_receiver: self, explicit_span_method_name: method_name,
|
|
169
169
|
experiment_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?, declared_node: true,
|
|
170
170
|
on_start: call.method(:start), on_complete: call.method(:complete),
|
|
@@ -267,7 +267,7 @@ module Bitfab
|
|
|
267
267
|
# When true, `client.replay(...)` returns this span's
|
|
268
268
|
# historical output instead of executing the wrapped method.
|
|
269
269
|
def bitfab_span(method_name, trace_function_key: nil, name: nil, type: "custom",
|
|
270
|
-
capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
270
|
+
capture_when: "always", mock_on_replay: false, replay_reusable: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
271
271
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
272
272
|
trace_function_key ||= @bitfab_function_key
|
|
273
273
|
unless trace_function_key
|
|
@@ -278,7 +278,7 @@ module Bitfab
|
|
|
278
278
|
# If the method already exists (inline or after-method style), wrap it immediately
|
|
279
279
|
if method_defined?(method_name) || private_method_defined?(method_name)
|
|
280
280
|
_bitfab_wrap_method(
|
|
281
|
-
method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, finalize:, experiment_id:
|
|
281
|
+
method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, replay_reusable:, finalize:, experiment_id:
|
|
282
282
|
)
|
|
283
283
|
else
|
|
284
284
|
# Method doesn't exist yet (before-method style) - register for method_added hook
|
|
@@ -289,6 +289,7 @@ module Bitfab
|
|
|
289
289
|
type:,
|
|
290
290
|
capture_when:,
|
|
291
291
|
mock_on_replay:,
|
|
292
|
+
replay_reusable:,
|
|
292
293
|
finalize:,
|
|
293
294
|
experiment_id:
|
|
294
295
|
}
|
|
@@ -328,10 +329,10 @@ module Bitfab
|
|
|
328
329
|
# EXPERIMENTAL. Configure one method discovered beneath bitfab_trace.
|
|
329
330
|
# The node inherits the active trace key and never traces by itself.
|
|
330
331
|
def bitfab_node(method_name, name: nil, type: "custom", capture: true,
|
|
331
|
-
experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
|
|
332
|
+
experiment_id: nil, mock_on_replay: nil, replay_reusable: false, finalize: nil, test_run_id: nil)
|
|
332
333
|
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
333
334
|
Traceable.send(:validate_node_options!, capture:, mock_on_replay:)
|
|
334
|
-
config = {name:, type:, capture:, experiment_id:, mock_on_replay:, finalize:}
|
|
335
|
+
config = {name:, type:, capture:, experiment_id:, mock_on_replay:, replay_reusable:, finalize:}
|
|
335
336
|
if method_defined?(method_name) || private_method_defined?(method_name)
|
|
336
337
|
_bitfab_node_method(method_name, **config)
|
|
337
338
|
else
|
|
@@ -365,7 +366,7 @@ module Bitfab
|
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
def _bitfab_node_method(method_name, name: nil, type: "custom", capture: true,
|
|
368
|
-
experiment_id: nil, mock_on_replay: nil, finalize: nil)
|
|
369
|
+
experiment_id: nil, mock_on_replay: nil, replay_reusable: false, finalize: nil)
|
|
369
370
|
Traceable.node(
|
|
370
371
|
self,
|
|
371
372
|
method_name,
|
|
@@ -374,6 +375,7 @@ module Bitfab
|
|
|
374
375
|
capture:,
|
|
375
376
|
experiment_id:,
|
|
376
377
|
mock_on_replay:,
|
|
378
|
+
replay_reusable:,
|
|
377
379
|
finalize:,
|
|
378
380
|
client: nil
|
|
379
381
|
)
|
data/lib/bitfab/version.rb
CHANGED
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.61.
|
|
4
|
+
version: 0.61.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/bitfab/seed.rb
|
|
189
189
|
- lib/bitfab/seed_cli.rb
|
|
190
190
|
- lib/bitfab/seed_context.rb
|
|
191
|
+
- lib/bitfab/selective_replay.rb
|
|
191
192
|
- lib/bitfab/serialize.rb
|
|
192
193
|
- lib/bitfab/simulation_plan.rb
|
|
193
194
|
- lib/bitfab/span_context.rb
|