bitfab 0.59.0 → 0.59.1

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: 0f71ead77c070efe05df2af12495ef411048fd0164655e0f1fbe11a5457b518c
4
- data.tar.gz: 4e9a222f559e2977366ecf411f97429db22f89d1aca6b8b6de767b982594f8ce
3
+ metadata.gz: 9a45f5360238ae79c517f3514f96a7d02dbad382d4c1c3987ab3d2474d64d85d
4
+ data.tar.gz: 05af89e2fd6fbaed6ae530b4718234c72ad3a91001fc91da151b4a29cb4acc4b
5
5
  SHA512:
6
- metadata.gz: 406d7de1521d252c335432fa44c04fd7f3e11f0992281388d8939140ab05b5b055cb3cd5a4dc6bf30cfba15a40795f7c1f21782d989bbbebd1abeac62e5ff7fc
7
- data.tar.gz: 9e80632bfc746766ffd6011ceb45848a2875590e29b2f9c983b4c4ed344b722c310a15a760b6d4bd99045357f9def49b08904bb53618ad0dedbf5b33c2c4cffe
6
+ metadata.gz: 305b1b0ea90a88ef5306eec5c0aaa622f76f0f7f9caaf6d64986c690908ceb34598a0225a4049fbf0d88d5788dc196fc0246ce29f860b000943ec08c0322568e
7
+ data.tar.gz: 7a022d2714b3e617dfa0a24efb7eecaec531f9bf264a693c5c05b20076a282a4ffe0b85ebb1eff2deb5a75c737951597c975230a72a7f3316c327ed9984a0bfc
data/README.md CHANGED
@@ -256,7 +256,7 @@ def summarize(order)
256
256
  end
257
257
  ```
258
258
 
259
- Nodes support `name:`, `type:`, `capture:`, `test_run_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
259
+ Nodes support `name:`, `type:`, `capture:`, `experiment_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
260
260
 
261
261
  `bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After a span limit is reached, it stops reading inputs and outputs immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
262
262
 
data/lib/bitfab/client.rb CHANGED
@@ -106,7 +106,7 @@ module Bitfab
106
106
  tracing_enabled?
107
107
  end
108
108
 
109
- # Replay historical traces through a method and create a test run.
109
+ # Replay historical traces through a method and create an experiment.
110
110
  #
111
111
  # @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
112
112
  # @param method_name [Symbol] the method to replay
@@ -183,10 +183,16 @@ module Bitfab
183
183
  # @param on_item_start [#call, nil] optional callback invoked when a worker
184
184
  # begins processing each item. Pair it with on_item_finish to distinguish
185
185
  # queued work from in-flight work. A raising callback never crashes the run.
186
- # @return [Hash] with :items, :test_run_id, :test_run_url
186
+ # @param on_experiment_start [#call, nil] optional callback invoked once,
187
+ # right after the server creates the experiment and before any item runs,
188
+ # with a hash { experiment_id:, experiment_url: }. A raising callback never
189
+ # crashes the run.
190
+ # @return [Hash] with :items, :experiment_id, :experiment_url, and the
191
+ # deprecated aliases :test_run_id and :test_run_url carrying the same values
187
192
  def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
188
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,
189
- adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
194
+ 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)
190
196
  Replay.run(
191
197
  self,
192
198
  receiver,
@@ -213,7 +219,8 @@ module Bitfab
213
219
  db_branch:,
214
220
  on_item_start:,
215
221
  on_item_finish:,
216
- on_progress:
222
+ on_progress:,
223
+ on_experiment_start:
217
224
  )
218
225
  end
219
226
 
@@ -330,14 +337,15 @@ module Bitfab
330
337
  # Configure an existing method only when it is discovered beneath a
331
338
  # bitfab_trace root owned by this client.
332
339
  def node(klass, method_name, name: nil, type: "custom", capture: true,
333
- test_run_id: nil, mock_on_replay: nil, finalize: nil)
340
+ experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
341
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
334
342
  Bitfab::Traceable.node(
335
343
  klass,
336
344
  method_name,
337
345
  name:,
338
346
  type:,
339
347
  capture:,
340
- test_run_id:,
348
+ experiment_id:,
341
349
  mock_on_replay:,
342
350
  finalize:,
343
351
  client: self
@@ -376,7 +384,7 @@ module Bitfab
376
384
  # Called by Traceable, not intended for direct use.
377
385
  def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
378
386
  capture_when: "always", mock_on_replay: false, independent_root: false,
379
- explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil,
387
+ explicit_span_receiver: nil, explicit_span_method_name: nil, experiment_id: nil, finalize: nil,
380
388
  surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
381
389
  result_recorded_elsewhere: false, declared_node: false)
382
390
  return yield unless should_record?
@@ -405,7 +413,7 @@ module Bitfab
405
413
  parent_span_id = nil
406
414
  is_root_span = nil
407
415
  started_at = nil
408
- resolved_test_run_id = nil
416
+ resolved_experiment_id = nil
409
417
  resolved_input_source_span_id = nil
410
418
  begin
411
419
  replay_ctx = ReplayContext.current
@@ -433,7 +441,7 @@ module Bitfab
433
441
  )
434
442
  end
435
443
  started_at = Bitfab.now_iso_timestamp
436
- resolved_test_run_id = replay_ctx ? replay_ctx[:test_run_id] : test_run_id
444
+ resolved_experiment_id = replay_ctx ? replay_ctx[:experiment_id] : experiment_id
437
445
  resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
438
446
  resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
439
447
 
@@ -442,7 +450,7 @@ module Bitfab
442
450
  TraceState.create(
443
451
  trace_id,
444
452
  trace_function_key:,
445
- test_run_id: resolved_test_run_id,
453
+ experiment_id: resolved_experiment_id,
446
454
  input_source_trace_id: resolved_input_source_trace_id,
447
455
  replay_attempt: replay_ctx&.dig(:replay_attempt),
448
456
  db_snapshot: @db_snapshot
@@ -483,7 +491,7 @@ module Bitfab
483
491
  kwargs:,
484
492
  mocked_output:,
485
493
  started_at:,
486
- test_run_id: resolved_test_run_id,
494
+ experiment_id: resolved_experiment_id,
487
495
  input_source_span_id: resolved_input_source_span_id,
488
496
  mock_source:,
489
497
  instrumentation: (surface == :trace) ? "trace" : "span",
@@ -573,7 +581,7 @@ module Bitfab
573
581
  error: recorded_error,
574
582
  started_at:,
575
583
  ended_at:,
576
- test_run_id: resolved_test_run_id,
584
+ experiment_id: resolved_experiment_id,
577
585
  input_source_span_id: resolved_input_source_span_id
578
586
  )
579
587
 
@@ -931,8 +939,9 @@ module Bitfab
931
939
  if trace_state&.dig(:session_id)
932
940
  payload["sessionId"] = trace_state[:session_id]
933
941
  end
934
- if trace_state&.dig(:test_run_id)
935
- payload["testRunId"] = trace_state[:test_run_id]
942
+ if trace_state&.dig(:experiment_id)
943
+ payload["experimentId"] = trace_state[:experiment_id]
944
+ payload["testRunId"] = trace_state[:experiment_id]
936
945
  end
937
946
 
938
947
  @http_client.send_external_trace(payload)
@@ -943,7 +952,7 @@ module Bitfab
943
952
 
944
953
  def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
945
954
  span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
946
- started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false,
955
+ started_at:, ended_at:, experiment_id: nil, input_source_span_id: nil, mocked: false,
947
956
  mock_target: nil, mock_source: nil, span_data: {}, instrumentation: "span", declared_node: false,
948
957
  nearest_captured_ancestor_span_id: nil)
949
958
  extra_span_data = span_data
@@ -1016,7 +1025,10 @@ module Bitfab
1016
1025
  "rootTraceFunctionKey" => TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key,
1017
1026
  "rawSpan" => raw_span
1018
1027
  }
1019
- payload["testRunId"] = test_run_id if test_run_id
1028
+ if experiment_id
1029
+ payload["experimentId"] = experiment_id
1030
+ payload["testRunId"] = experiment_id
1031
+ end
1020
1032
  # Flag spans served from the original trace's recorded output during a
1021
1033
  # replay so the trace view can mark them (matches the Python and
1022
1034
  # TypeScript SDKs).
@@ -1163,12 +1175,12 @@ module Bitfab
1163
1175
  nil
1164
1176
  end
1165
1177
 
1166
- # Record a span entry for a mocked invocation so the test run reflects the
1178
+ # Record a span entry for a mocked invocation so the experiment reflects the
1167
1179
  # mocked execution. Mirrors send_span's payload shape but with the mocked
1168
1180
  # output as the result and no error.
1169
1181
  def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
1170
1182
  span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
1171
- started_at:, test_run_id:, input_source_span_id:, mock_source:, instrumentation: "span", declared_node: false)
1183
+ started_at:, experiment_id:, input_source_span_id:, mock_source:, instrumentation: "span", declared_node: false)
1172
1184
  ended_at = Bitfab.now_iso_timestamp
1173
1185
  send_span(
1174
1186
  trace_function_key:,
@@ -1186,7 +1198,7 @@ module Bitfab
1186
1198
  error: nil,
1187
1199
  started_at:,
1188
1200
  ended_at:,
1189
- test_run_id:,
1201
+ experiment_id:,
1190
1202
  input_source_span_id:,
1191
1203
  mocked: true,
1192
1204
  instrumentation:,
@@ -1234,7 +1246,8 @@ module Bitfab
1234
1246
  # values warn once and default to "always"
1235
1247
  # @param mock_on_replay [Boolean] mark this span for the default "marked"
1236
1248
  # mock strategy. A missing selected occurrence fails closed.
1237
- def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
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)
1250
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
1238
1251
  Bitfab::Traceable.wrap(
1239
1252
  klass,
1240
1253
  method_name,
@@ -1244,7 +1257,7 @@ module Bitfab
1244
1257
  capture_when:,
1245
1258
  mock_on_replay:,
1246
1259
  finalize:,
1247
- test_run_id:,
1260
+ experiment_id:,
1248
1261
  client: @client
1249
1262
  )
1250
1263
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ class << self
5
+ def resolve_experiment_id(experiment_id, test_run_id)
6
+ return experiment_id if test_run_id.nil?
7
+
8
+ if !experiment_id.nil? && experiment_id != test_run_id
9
+ raise ArgumentError, "experiment_id and test_run_id name the same experiment ID but were " \
10
+ "passed different values. Pass experiment_id only; test_run_id is deprecated."
11
+ end
12
+ warn_once("test-run-id-deprecated", "test_run_id: is deprecated; use experiment_id: instead.")
13
+ test_run_id
14
+ end
15
+ end
16
+ end
@@ -241,7 +241,7 @@ module Bitfab
241
241
  end
242
242
 
243
243
  # Start a replay session by fetching historical traces.
244
- # Blocking call. Returns hash with testRunId, testRunUrl, and items array.
244
+ # Blocking call. Returns hash with experimentId, experimentUrl, and items array.
245
245
  #
246
246
  # @param code_change_description [String, nil] optional rationale for the
247
247
  # code change being tested in this replay
@@ -250,7 +250,7 @@ module Bitfab
250
250
  # @param experiment_group_id [String, nil] optional UUID grouping multiple
251
251
  # replay runs into a single experiment batch
252
252
  # @param name [String, nil] optional display name for the resulting
253
- # experiment/test run
253
+ # experiment
254
254
  # @param dataset_ids [Array<String>, nil] optional UUIDs of the datasets this
255
255
  # replay runs against. The run replays the union of their traces, graded by
256
256
  # the union of their graders, and is attributed to every one of them. A
@@ -344,9 +344,9 @@ module Bitfab
344
344
  get(endpoint, timeout: 30)
345
345
  end
346
346
 
347
- # Mark a replay test run as completed. Blocking call.
348
- def complete_replay(test_run_id)
349
- request("/api/sdk/replay/complete", {"testRunId" => test_run_id}, timeout: 30)
347
+ # Mark a replay experiment as completed. Blocking call.
348
+ def complete_replay(experiment_id)
349
+ request("/api/sdk/replay/complete", {"experimentId" => experiment_id, "testRunId" => experiment_id}, timeout: 30)
350
350
  end
351
351
 
352
352
  # Release a previously-resolved DB branch by deleting its Neon branch.
@@ -356,8 +356,8 @@ module Bitfab
356
356
  request("/api/sdk/replay/releaseDbBranchLease", {"neonBranchId" => neon_branch_id}, timeout: 30)
357
357
  end
358
358
 
359
- def resolve_db_branch_lease(test_run_id, trace_id, db_branch_settings = nil, attempt = 0)
360
- payload = {"testRunId" => test_run_id, "traceId" => trace_id}
359
+ def resolve_db_branch_lease(experiment_id, trace_id, db_branch_settings = nil, attempt = 0)
360
+ payload = {"experimentId" => experiment_id, "testRunId" => experiment_id, "traceId" => trace_id}
361
361
  payload["attempt"] = attempt if attempt.positive?
362
362
  payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
363
363
  request(
@@ -398,10 +398,10 @@ module Bitfab
398
398
  end
399
399
 
400
400
  # Read the replay traces the server has fully persisted so far.
401
- def get_replay_status(test_run_id, expected_span_counts)
401
+ def get_replay_status(experiment_id, expected_span_counts)
402
402
  request(
403
403
  "/api/sdk/replay/status",
404
- {"testRunId" => test_run_id, "expectedSpanCounts" => expected_span_counts},
404
+ {"experimentId" => experiment_id, "testRunId" => experiment_id, "expectedSpanCounts" => expected_span_counts},
405
405
  timeout: 30
406
406
  )
407
407
  end
data/lib/bitfab/labels.rb CHANGED
@@ -15,29 +15,36 @@ module Bitfab
15
15
  end
16
16
 
17
17
  def save(label:, annotation:, trace_id: nil, original_trace_id: nil, attempt: nil,
18
- confidence: nil, test_run_id: nil, assertion_id: nil, evidence: EVIDENCE_UNSET)
18
+ confidence: nil, experiment_id: nil, assertion_id: nil, evidence: EVIDENCE_UNSET, test_run_id: nil)
19
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
19
20
  update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
20
21
  update["label"] = label
21
22
  update["annotation"] = annotation
22
23
  update["confidence"] = confidence unless confidence.nil?
23
24
  update["evidence"] = evidence unless evidence.equal?(EVIDENCE_UNSET)
24
- save_all([update], test_run_id:).first
25
+ save_all([update], experiment_id:).first
25
26
  end
26
27
 
27
- def save_all(labels, test_run_id: nil)
28
+ def save_all(labels, experiment_id: nil, test_run_id: nil)
29
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
28
30
  payload = {"labels" => labels}
29
- payload["testRunId"] = test_run_id unless test_run_id.nil?
31
+ unless experiment_id.nil?
32
+ payload["experimentId"] = experiment_id
33
+ payload["testRunId"] = experiment_id
34
+ end
30
35
  @http_client.request(LABELS_PATH, payload)["labels"]
31
36
  end
32
37
 
33
- def skip(trace_id: nil, original_trace_id: nil, attempt: nil, test_run_id: nil, assertion_id: nil)
38
+ def skip(trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil, assertion_id: nil, test_run_id: nil)
39
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
34
40
  update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
35
- save_all([update.merge("skip" => true)], test_run_id:).first
41
+ save_all([update.merge("skip" => true)], experiment_id:).first
36
42
  end
37
43
 
38
- def archive(trace_id: nil, original_trace_id: nil, attempt: nil, test_run_id: nil, assertion_id: nil)
44
+ def archive(trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil, assertion_id: nil, test_run_id: nil)
45
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
39
46
  update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
40
- save_all([update.merge("archive" => true)], test_run_id:).first
47
+ save_all([update.merge("archive" => true)], experiment_id:).first
41
48
  end
42
49
 
43
50
  def save_human(label:, annotation:, trace_id:, confidence: nil, assertion_id: nil,
@@ -77,7 +84,7 @@ module Bitfab
77
84
  def label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
78
85
  if trace_id.nil? == original_trace_id.nil?
79
86
  raise ArgumentError, "Pass exactly one of trace_id or original_trace_id. " \
80
- "Use original_trace_id for a replay verdict, with the test_run_id it ran under."
87
+ "Use original_trace_id for a replay verdict, with the experiment_id it ran under."
81
88
  end
82
89
 
83
90
  update = {}
data/lib/bitfab/replay.rb CHANGED
@@ -27,13 +27,23 @@ module Bitfab
27
27
 
28
28
  # Whole-run replay failure with every item collected before the run failed.
29
29
  class ReplayError < StandardError
30
- attr_reader :items, :test_run_id, :test_run_url
30
+ attr_reader :items, :experiment_id, :experiment_url
31
31
 
32
- def initialize(message, items:, test_run_id:, test_run_url:)
32
+ def initialize(message, items:, experiment_id: nil, experiment_url: nil, test_run_id: nil, test_run_url: nil)
33
33
  super(message)
34
34
  @items = items
35
- @test_run_id = test_run_id
36
- @test_run_url = test_run_url
35
+ @experiment_id = experiment_id || test_run_id
36
+ @experiment_url = experiment_url || test_run_url
37
+ end
38
+
39
+ # @deprecated Use {#experiment_id}.
40
+ def test_run_id
41
+ experiment_id
42
+ end
43
+
44
+ # @deprecated Use {#experiment_url}.
45
+ def test_run_url
46
+ experiment_url
37
47
  end
38
48
  end
39
49
 
@@ -73,13 +83,13 @@ module Bitfab
73
83
 
74
84
  # Execute a block with replay context set for every fiber on the current
75
85
  # thread. Child threads and processes intentionally do not inherit it.
76
- def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
86
+ def with_context(experiment_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
77
87
  mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
78
88
  db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil, replay_attempt: nil)
79
89
  previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
80
90
  ctx = {
81
91
  replay_attempt:,
82
- test_run_id:,
92
+ experiment_id:,
83
93
  input_source_span_id:,
84
94
  input_source_trace_id:,
85
95
  trace_id:,
@@ -127,7 +137,7 @@ module Bitfab
127
137
  end
128
138
  end
129
139
 
130
- # Replay historical traces through a traced method and create a test run.
140
+ # Replay historical traces through a traced method and create an experiment.
131
141
  module Replay
132
142
  CODE_CHANGE_UNSET = Object.new.freeze
133
143
  PERSISTENCE_TIMEOUT_SECONDS = 30.0
@@ -167,19 +177,19 @@ module Bitfab
167
177
  items.map { |item| item.except(:_sdk_trace_id) }
168
178
  end
169
179
 
170
- def preserve_replay_failure(items, test_run_id, test_run_url)
180
+ def preserve_replay_failure(items, experiment_id, experiment_url)
171
181
  yield
172
182
  rescue => cause
173
183
  error = ReplayError.new(
174
184
  cause.message,
175
185
  items: public_replay_items(items),
176
- test_run_id:,
177
- test_run_url:
186
+ experiment_id:,
187
+ experiment_url:
178
188
  )
179
189
  raise error, cause:
180
190
  end
181
191
 
182
- # Replay historical traces through a method and create a test run.
192
+ # Replay historical traces through a method and create an experiment.
183
193
  #
184
194
  # Fetches the last N traces for the given trace function key, re-runs each
185
195
  # through the provided receiver and method, and returns comparison data.
@@ -193,7 +203,7 @@ module Bitfab
193
203
  # already determines how many traces replay. Supplying trace_ids also
194
204
  # emits a warning.
195
205
  # @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
196
- # @param name [String, nil] optional display name for the resulting experiment/test run
206
+ # @param name [String, nil] optional display name for the resulting experiment
197
207
  # @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
198
208
  # @param code_change_description [String, nil] optional rationale for the
199
209
  # code change being tested in this replay (stored on the experiment).
@@ -255,11 +265,18 @@ module Bitfab
255
265
  # begins processing each item, before replay setup and customer code run.
256
266
  # Pair it with on_item_finish to distinguish queued work from in-flight work.
257
267
  # A raising callback never crashes the run.
258
- # @return [Hash] with :items, :test_run_id, :test_run_url
268
+ # @param on_experiment_start [#call, nil] optional callback invoked once,
269
+ # right after the server creates the experiment and before any item runs,
270
+ # with a hash { experiment_id:, experiment_url: }. Use it to show the
271
+ # experiment link even if the run later fails. A raising callback never
272
+ # crashes the run.
273
+ # @return [Hash] with :items, :experiment_id, :experiment_url, and the
274
+ # deprecated aliases :test_run_id and :test_run_url carrying the same values
259
275
  def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
260
276
  concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil,
261
277
  dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
262
- adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
278
+ 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)
263
280
  concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
264
281
  attempts = concurrency_options.attempts
265
282
  max_concurrency = concurrency_options.max_concurrency
@@ -293,7 +310,7 @@ module Bitfab
293
310
  # Reject a trace_function_key that contradicts the method's declared key.
294
311
  # replay() fetches historical traces by trace_function_key but records the
295
312
  # replayed spans under the method's own bitfab_span key (via send below),
296
- # so a mismatch produces an incoherent test run (traces fetched for one
313
+ # so a mismatch produces an incoherent experiment (traces fetched for one
297
314
  # function, recorded under another). Only fires when the method's key is
298
315
  # introspectable; an untraced method falls through to the persistence
299
316
  # check in complete_replay. Mirrors the TypeScript/Python SDKs.
@@ -358,9 +375,16 @@ module Bitfab
358
375
  include_original_metadata: !adapt_inputs.nil?,
359
376
  db_branch_settings: resolved_db_branch_settings
360
377
  )
361
- test_run_id = replay_data["testRunId"]
362
- test_run_url = replay_data["testRunUrl"]
363
- full_test_run_url = "#{client.service_url}#{test_run_url}"
378
+ experiment_id = replay_data["experimentId"] || replay_data["testRunId"]
379
+ experiment_url = replay_data["experimentUrl"] || replay_data["testRunUrl"]
380
+ full_experiment_url = "#{client.service_url}#{experiment_url}"
381
+ if on_experiment_start
382
+ begin
383
+ on_experiment_start.call({experiment_id:, experiment_url: full_experiment_url})
384
+ rescue => e
385
+ warn "Bitfab: replay on_experiment_start callback raised: #{e.message}"
386
+ end
387
+ end
364
388
  server_items = attempts.times.flat_map do |attempt|
365
389
  (replay_data["items"] || []).map do |source|
366
390
  item = source.merge("attempt" => attempt)
@@ -373,7 +397,7 @@ module Bitfab
373
397
  end
374
398
 
375
399
  result_items = if server_items.any?
376
- process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock.to_s,
400
+ process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock.to_s,
377
401
  adapt_inputs, include_db_branch_lease, resolved_db_branch_settings, on_item_start:,
378
402
  on_item_finish: item_finish_callback,
379
403
  mock_overrides: resolved_overrides, dry_run:, process_executor:)
@@ -383,11 +407,14 @@ module Bitfab
383
407
 
384
408
  if dry_run
385
409
  begin
386
- http_client.complete_replay(test_run_id)
410
+ http_client.complete_replay(experiment_id)
387
411
  rescue => e
388
412
  warn "Bitfab: could not finalize dry run: #{e.message}"
389
413
  end
390
- result = {items: public_replay_items(result_items), test_run_id:, test_run_url: full_test_run_url, attempts:}
414
+ result = {
415
+ items: public_replay_items(result_items), experiment_id:, experiment_url: full_experiment_url,
416
+ test_run_id: experiment_id, test_run_url: full_experiment_url, attempts:
417
+ }
391
418
  write_replay_result_file(result)
392
419
  return result
393
420
  end
@@ -396,8 +423,8 @@ module Bitfab
396
423
  # to finalize once the server confirms every replay trace it queued: the
397
424
  # trace-ID mapping complete_replay builds would otherwise race the
398
425
  # in-flight batches and hand back nil for every item.
399
- delivered_trace_ids = preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
400
- wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
426
+ delivered_trace_ids = preserve_replay_failure(result_items, experiment_id, full_experiment_url) do
427
+ wait_for_replay_persistence(http_client, experiment_id, result_items.map { |item| item[:_sdk_trace_id] })
401
428
  end
402
429
 
403
430
  # Primary source for item[:trace_id]: the server's assigned traces.id, read
@@ -414,8 +441,8 @@ module Bitfab
414
441
  # complete_replay finalizes the run and returns the token/diagnostic
415
442
  # mapping; its failures propagate loudly because a run that never
416
443
  # completed can't be finalized.
417
- complete_response = preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
418
- http_client.complete_replay(test_run_id)
444
+ complete_response = preserve_replay_failure(result_items, experiment_id, full_experiment_url) do
445
+ http_client.complete_replay(experiment_id)
419
446
  end
420
447
  trace_id_map = complete_response&.dig("traceIds")
421
448
  # Per-replay-trace token usage keyed by server trace id: the REPLAYED
@@ -431,7 +458,7 @@ module Bitfab
431
458
  # now that the row exists, attach each item's server-aggregated token usage,
432
459
  # and detect a systemic upload failure early. Verdict persistence does NOT
433
460
  # use this map: it is keyed by the original-trace lineage (:original_trace_id +
434
- # test_run_id), which needs no client-held server id. Older servers that omit
461
+ # experiment_id), which needs no client-held server id. Older servers that omit
435
462
  # the map yield no replay tokens and leave item[:trace_id] nil.
436
463
  unless trace_id_map.nil?
437
464
  missing = []
@@ -461,14 +488,14 @@ module Bitfab
461
488
  server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
462
489
  cause = RuntimeError.new("Replay completed but the server has no persisted trace for " \
463
490
  "any of the #{completed_count} completed item(s) " \
464
- "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
491
+ "(experiment #{experiment_id}).#{server_count} Trace uploads were " \
465
492
  "flushed, so either the uploads failed or the replayed method is " \
466
493
  "not traced (no root span was emitted).")
467
494
  error = ReplayError.new(
468
495
  cause.message,
469
496
  items: public_replay_items(result_items),
470
- test_run_id:,
471
- test_run_url: full_test_run_url
497
+ experiment_id:,
498
+ experiment_url: full_experiment_url
472
499
  )
473
500
  raise error, cause:
474
501
  end
@@ -477,7 +504,7 @@ module Bitfab
477
504
  if missing.any?
478
505
  warn "Bitfab: server has no persisted trace for #{missing.length} of " \
479
506
  "#{completed_count} completed replay item(s) " \
480
- "(test_run_id #{test_run_id}). Their replay token usage is " \
507
+ "(experiment #{experiment_id}). Their replay token usage is " \
481
508
  "unavailable and they cannot be labeled."
482
509
  end
483
510
  end
@@ -489,8 +516,10 @@ module Bitfab
489
516
  result = {
490
517
  attempts:,
491
518
  items: result_items,
492
- test_run_id:,
493
- test_run_url: full_test_run_url
519
+ experiment_id:,
520
+ experiment_url: full_experiment_url,
521
+ test_run_id: experiment_id,
522
+ test_run_url: full_experiment_url
494
523
  }
495
524
  # Persist the enriched result so the Bitfab plugin never has to parse the
496
525
  # replay's stdout, which a dependency's logging can corrupt.
@@ -503,7 +532,8 @@ module Bitfab
503
532
  begin
504
533
  on_progress.call({
505
534
  type: "complete",
506
- test_run_id:,
535
+ experiment_id:,
536
+ test_run_id: experiment_id,
507
537
  completed: total,
508
538
  total:,
509
539
  succeeded: total - errored,
@@ -744,7 +774,7 @@ module Bitfab
744
774
  end
745
775
 
746
776
  # Process all replay items, optionally in parallel using threads.
747
- def process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock_strategy,
777
+ def process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock_strategy,
748
778
  adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, on_item_start: nil, on_item_finish: nil,
749
779
  mock_overrides: [], dry_run: false, process_executor: nil)
750
780
  concurrency = max_concurrency || server_items.length
@@ -774,7 +804,7 @@ module Bitfab
774
804
 
775
805
  begin
776
806
  on_item_start.call({
777
- type: "started", test_run_id:, started:, completed:, total:, succeeded:, errored:,
807
+ type: "started", experiment_id:, test_run_id: experiment_id, started:, completed:, total:, succeeded:, errored:,
778
808
  item: {
779
809
  attempt:,
780
810
  original_trace_id:,
@@ -804,7 +834,7 @@ module Bitfab
804
834
  warn "Bitfab: replay per-item flush failed: #{e.message}"
805
835
  end
806
836
 
807
- report = lambda do |result, original_trace_id, original_span_id, test_run_id|
837
+ report = lambda do |result, original_trace_id, original_span_id, experiment_id|
808
838
  # Deliver this item's trace now, then read its server id back off the
809
839
  # ingest response. A flush failure never crashes the run: the id stays
810
840
  # nil and the end-of-run barrier remains the authority on persistence.
@@ -818,7 +848,7 @@ module Bitfab
818
848
 
819
849
  begin
820
850
  on_item_finish.call({
821
- test_run_id:, completed:, total:, succeeded:, errored:,
851
+ experiment_id:, test_run_id: experiment_id, completed:, total:, succeeded:, errored:,
822
852
  item: {
823
853
  attempt: result[:attempt],
824
854
  ingestion_type: result[:ingestion_type],
@@ -854,9 +884,9 @@ module Bitfab
854
884
 
855
885
  execute = lambda do |item|
856
886
  if process_executor
857
- process_executor.call(item, test_run_id, mock_strategy, include_db_branch_lease, db_branch_settings, dry_run:)
887
+ process_executor.call(item, experiment_id, mock_strategy, include_db_branch_lease, db_branch_settings, dry_run:)
858
888
  else
859
- process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
889
+ process_single_item(http_client, item, receiver, method_name, experiment_id, mock_strategy,
860
890
  adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:, dry_run:)
861
891
  end
862
892
  end
@@ -865,7 +895,7 @@ module Bitfab
865
895
  server_items.map do |item|
866
896
  report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
867
897
  result = execute.call(item)
868
- report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
898
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), experiment_id)
869
899
  result
870
900
  end
871
901
  else
@@ -883,7 +913,7 @@ module Bitfab
883
913
  report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
884
914
  result = execute.call(item)
885
915
  results_mutex.synchronize { results[idx] = result }
886
- report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
916
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), experiment_id)
887
917
  end
888
918
  end
889
919
  end
@@ -899,7 +929,7 @@ module Bitfab
899
929
  # deserializing inputs is captured on the returned item's :error rather
900
930
  # than propagated, so one bad trace never aborts the whole replay run
901
931
  # (mirrors the TypeScript and Python SDKs' per-item rescue).
902
- def process_single_item(http_client, server_item, receiver, method_name, test_run_id, mock_strategy,
932
+ def process_single_item(http_client, server_item, receiver, method_name, experiment_id, mock_strategy,
903
933
  adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [], dry_run: false)
904
934
  metrics = extract_server_item_metrics(server_item)
905
935
  attempt = server_item["attempt"] || 0
@@ -925,7 +955,7 @@ module Bitfab
925
955
  db_branch_timings = include_db_branch_lease ? server_item["dbBranchTimings"] : nil
926
956
  if include_db_branch_lease && lease.nil? && lease_error.nil?
927
957
  begin
928
- resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings, attempt)
958
+ resolved = http_client.resolve_db_branch_lease(experiment_id, original_trace_id, db_branch_settings, attempt)
929
959
  rescue => cause
930
960
  error = DbBranchReplayError.new(
931
961
  "lease_request_failed",
@@ -1001,7 +1031,7 @@ module Bitfab
1001
1031
  item_data,
1002
1032
  receiver,
1003
1033
  method_name,
1004
- test_run_id,
1034
+ experiment_id,
1005
1035
  span["id"],
1006
1036
  metrics,
1007
1037
  input_source_trace_id: span["externalTraceId"],
@@ -1180,7 +1210,7 @@ module Bitfab
1180
1210
  # repeat itself. Acks cannot settle a request that timed out client-side
1181
1211
  # after the server committed, so anything short of fully delivered falls
1182
1212
  # through to the server poll, which stays the authority there.
1183
- def wait_for_replay_persistence(http_client, test_run_id, sdk_trace_ids, timeout: nil)
1213
+ def wait_for_replay_persistence(http_client, experiment_id, sdk_trace_ids, timeout: nil)
1184
1214
  timeout ||= PERSISTENCE_TIMEOUT_SECONDS
1185
1215
  trace_ids = sdk_trace_ids.compact
1186
1216
 
@@ -1212,7 +1242,7 @@ module Bitfab
1212
1242
  missing = expected_span_counts.keys
1213
1243
 
1214
1244
  while missing.any?
1215
- status = http_client.get_replay_status(test_run_id, expected_span_counts)
1245
+ status = http_client.get_replay_status(experiment_id, expected_span_counts)
1216
1246
  ready = status["traceIds"]
1217
1247
  ready = {} unless ready.is_a?(Hash)
1218
1248
  read_back_trace_ids.merge!(ready)
@@ -1230,7 +1260,7 @@ module Bitfab
1230
1260
  "spans likely never reached the server."
1231
1261
  end
1232
1262
  raise "Replay traces were not fully persisted before the delivery deadline " \
1233
- "(test_run_id #{test_run_id}, missing #{missing.length} of " \
1263
+ "(experiment #{experiment_id}, missing #{missing.length} of " \
1234
1264
  "#{expected_span_counts.length} trace(s)).#{cause}"
1235
1265
  end
1236
1266
 
@@ -1249,7 +1279,7 @@ module Bitfab
1249
1279
  end
1250
1280
 
1251
1281
  # Execute a single replay item: deserialize inputs, call method with replay context.
1252
- def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil, metrics = {},
1282
+ def execute_item(item, receiver, method_name, experiment_id, input_source_span_id = nil, metrics = {},
1253
1283
  input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
1254
1284
  fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
1255
1285
  source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil, attempt: 0, dry_run: false)
@@ -1273,7 +1303,7 @@ module Bitfab
1273
1303
  begin
1274
1304
  ReplayContext.with_context(
1275
1305
  replay_attempt: attempt,
1276
- test_run_id:,
1306
+ experiment_id:,
1277
1307
  input_source_span_id:,
1278
1308
  input_source_trace_id:,
1279
1309
  trace_id: sdk_trace_id,
@@ -31,6 +31,7 @@ module Bitfab
31
31
  run(argv:, stdout:, stderr:)
32
32
  0
33
33
  rescue => error
34
+ stderr.puts "Experiment #{error.experiment_id}: #{error.experiment_url}" if error.is_a?(ReplayError) && error.experiment_id
34
35
  stderr.puts error.message
35
36
  1
36
37
  end
@@ -46,15 +46,15 @@ module Bitfab
46
46
  result = nil
47
47
  Bitfab.collect_delivery_problems do
48
48
  result = Replay.process_single_item(http, assignment.fetch("item"), receiver, method_name,
49
- assignment.fetch("test_run_id"), assignment.fetch("mock"), options[:adapt_inputs],
49
+ assignment.fetch("experiment_id"), assignment.fetch("mock"), options[:adapt_inputs],
50
50
  assignment.fetch("include_db_branch_lease"), assignment["db_branch_settings"],
51
51
  mock_overrides: overrides, dry_run: assignment.fetch("dry_run"))
52
52
  unless assignment["dry_run"]
53
53
  begin
54
- mapped = Replay.wait_for_replay_persistence(http, assignment.fetch("test_run_id"), [result[:_sdk_trace_id]], timeout: delivery_timeout)
54
+ mapped = Replay.wait_for_replay_persistence(http, assignment.fetch("experiment_id"), [result[:_sdk_trace_id]], timeout: delivery_timeout)
55
55
  result[:trace_id] = mapped[result[:_sdk_trace_id]] || result[:trace_id]
56
56
  if result[:_sdk_trace_id] && result[:trace_id].nil?
57
- status = http.get_replay_status(assignment.fetch("test_run_id"), {result[:_sdk_trace_id] => 1})
57
+ status = http.get_replay_status(assignment.fetch("experiment_id"), {result[:_sdk_trace_id] => 1})
58
58
  result[:trace_id] = status.fetch("traceIds", {})[result[:_sdk_trace_id]]
59
59
  end
60
60
  raise "Child replay trace persistence could not be confirmed" if result[:error].nil? && result[:trace_id].nil?
@@ -67,7 +67,7 @@ module Bitfab
67
67
  hook = options[:concurrency]&.on_item_finish_in_child_process
68
68
  if hook && !assignment["dry_run"]
69
69
  begin
70
- hook.call({test_run_id: assignment.fetch("test_run_id"), item: Replay.public_replay_items([result]).first})
70
+ hook.call({experiment_id: assignment.fetch("experiment_id"), test_run_id: assignment.fetch("experiment_id"), item: Replay.public_replay_items([result]).first})
71
71
  rescue => error
72
72
  stderr.puts "[replay] child grading hook failed for #{result[:original_trace_id]} attempt #{result[:attempt]}: #{error}"
73
73
  end
@@ -130,14 +130,14 @@ module Bitfab
130
130
  @throttle = ReplayMemoryThrottle.new(enabled: concurrency.memory_throttle)
131
131
  end
132
132
 
133
- def call(item, test_run_id, mock, include_db_branch_lease, db_branch_settings, dry_run: false)
133
+ def call(item, experiment_id, mock, include_db_branch_lease, db_branch_settings, dry_run: false)
134
134
  index = @mutex.synchronize { @sequence += 1 }
135
135
  @throttle.admit(index)
136
136
  result_path = File.join(@directory, "#{index}.result")
137
137
  assignment_path = File.join(@directory, "#{index}.json")
138
138
  stdout_path, stderr_path = File.join(@directory, "#{index}.out"), File.join(@directory, "#{index}.err")
139
139
  delivery_problems_path = File.join(@directory, "#{index}.delivery")
140
- assignment = {pipeline: @args.fetch(:pipeline), params: @params, item:, test_run_id:, mock:, include_db_branch_lease:, db_branch_settings:, dry_run:, result_path:, delivery_problems_path:}
140
+ assignment = {pipeline: @args.fetch(:pipeline), params: @params, item:, experiment_id:, mock:, include_db_branch_lease:, db_branch_settings:, dry_run:, result_path:, delivery_problems_path:}
141
141
  File.write(assignment_path, JSON.generate(assignment), mode: "w", perm: 0o600)
142
142
  executable = File.expand_path("../../exe/bitfab-replay", __dir__)
143
143
  pid = @mutex.synchronize do
@@ -240,6 +240,9 @@ module Bitfab
240
240
 
241
241
  stderr.puts "[replay] Replaying #{describe_selection(options)} from \"#{registration.trace_function_key}\"..."
242
242
 
243
+ options[:on_experiment_start] = lambda do |experiment|
244
+ stderr.puts "[replay] Experiment #{experiment[:experiment_id]}: #{experiment[:experiment_url]}"
245
+ end
243
246
  result = begin
244
247
  registration.client.replay(
245
248
  registration.receiver,
@@ -428,7 +431,7 @@ module Bitfab
428
431
  stderr.puts " Missed expected: #{missed}"
429
432
  end
430
433
  stderr.puts " Errors: #{errors}" if errors > 0
431
- stderr.puts "\n #{result[:test_run_url]}"
434
+ stderr.puts "\n #{result[:experiment_url]}"
432
435
  end
433
436
  end
434
437
  end
@@ -187,7 +187,7 @@ module Bitfab
187
187
  @states_mutex.synchronize { @states[trace_id] }
188
188
  end
189
189
 
190
- def create(trace_id, test_run_id: nil, input_source_trace_id: nil, replay_attempt: nil, trace_function_key: nil, db_snapshot: nil)
190
+ def create(trace_id, experiment_id: nil, input_source_trace_id: nil, replay_attempt: nil, trace_function_key: nil, db_snapshot: nil)
191
191
  @states_mutex.synchronize do
192
192
  @states[trace_id] ||= begin
193
193
  started_at = Bitfab.now_iso_timestamp
@@ -195,7 +195,7 @@ module Bitfab
195
195
  trace_id:,
196
196
  trace_function_key:,
197
197
  started_at:,
198
- test_run_id:,
198
+ experiment_id:,
199
199
  input_source_trace_id:,
200
200
  replay_attempt:,
201
201
  # Capture the wall clock now, before the wrapped function runs.
@@ -50,7 +50,7 @@ module Bitfab
50
50
  :started_at,
51
51
  :error,
52
52
  :span_type,
53
- :test_run_id,
53
+ :experiment_id,
54
54
  :span_data,
55
55
  :declared_node,
56
56
  keyword_init: true
@@ -83,7 +83,7 @@ module Bitfab
83
83
  class ConfiguredCall
84
84
  attr_reader :span_data, :owner
85
85
 
86
- def initialize(receiver:, method_name:, name:, type:, args:, kwargs:, test_run_id: nil,
86
+ def initialize(receiver:, method_name:, name:, type:, args:, kwargs:, experiment_id: nil,
87
87
  nested_trace_key: nil)
88
88
  @receiver = receiver
89
89
  @method_name = method_name
@@ -93,7 +93,7 @@ module Bitfab
93
93
  @records = Subtree.active_sessions.map do |session|
94
94
  omitted = session.excluded?(method_name)
95
95
  entry = unless omitted
96
- session.configured_entry(name:, type:, args:, kwargs:, test_run_id:)
96
+ session.configured_entry(name:, type:, args:, kwargs:, experiment_id:)
97
97
  end
98
98
  [session, entry, !omitted]
99
99
  end
@@ -189,7 +189,7 @@ module Bitfab
189
189
  trace_function_key:,
190
190
  trace_id: span[:trace_id],
191
191
  root_span_id: span[:span_id],
192
- test_run_id: replay&.dig(:test_run_id),
192
+ experiment_id: replay&.dig(:experiment_id),
193
193
  input_source_span_id: replay&.dig(:input_source_span_id),
194
194
  config:,
195
195
  max_depth:,
@@ -416,7 +416,7 @@ module Bitfab
416
416
  @parent_session = session.parent_session if @parent_session.equal?(session)
417
417
  end
418
418
 
419
- def initialize(client:, trace_function_key:, trace_id:, root_span_id:, test_run_id:,
419
+ def initialize(client:, trace_function_key:, trace_id:, root_span_id:, experiment_id:,
420
420
  input_source_span_id:, config:, max_depth:, include_wrappers:,
421
421
  root_receiver:, root_method_name:, max_captured_subtree_spans: DEFAULT_MAX_CAPTURED_SUBTREE_SPANS, mock_on_replay_default: false)
422
422
  @mock_on_replay_default = mock_on_replay_default
@@ -424,7 +424,7 @@ module Bitfab
424
424
  @trace_function_key = trace_function_key
425
425
  @trace_id = trace_id
426
426
  @root_span_id = root_span_id
427
- @test_run_id = test_run_id
427
+ @experiment_id = experiment_id
428
428
  @input_source_span_id = input_source_span_id
429
429
  @root = config.root
430
430
  @root_path = config.root_path
@@ -532,13 +532,13 @@ module Bitfab
532
532
  entry&.span_data&.dig("content_off_by_simulation_plan") == true
533
533
  end
534
534
 
535
- def configured_entry(name:, type:, args:, kwargs:, test_run_id:)
535
+ def configured_entry(name:, type:, args:, kwargs:, experiment_id:)
536
536
  return unless reserve_node
537
537
 
538
538
  entry = OpenSpan.new(
539
539
  span_id: SecureRandom.uuid,
540
540
  parent_span_id: current_parent_span_id,
541
- name:, span_type: type, args:, kwargs:, test_run_id:, started_at: now,
541
+ name:, span_type: type, args:, kwargs:, experiment_id:, started_at: now,
542
542
  span_data: {}, declared_node: true
543
543
  )
544
544
  @configured_entries[entry.object_id] = entry
@@ -744,7 +744,7 @@ module Bitfab
744
744
  error: entry.error,
745
745
  started_at: entry.started_at,
746
746
  ended_at: now,
747
- test_run_id: entry.test_run_id || @test_run_id,
747
+ experiment_id: entry.experiment_id || @experiment_id,
748
748
  input_source_span_id: @input_source_span_id
749
749
  )
750
750
  rescue Exception # rubocop:disable Lint/RescueException
@@ -53,7 +53,8 @@ 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, test_run_id: nil, client: nil)
56
+ capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, client: nil, test_run_id: nil)
57
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
57
58
  method_name_str = method_name.to_s
58
59
  bound_client = client
59
60
  wrapper = Module.new do
@@ -66,7 +67,7 @@ module Bitfab
66
67
  trace_function_key:,
67
68
  span_name: name || Subtree.qualified_name(self, method_name),
68
69
  span_type: type, function_name: method_name_str, args:, kwargs:,
69
- capture_when:, mock_on_replay:, finalize:, test_run_id:, surface: :span,
70
+ capture_when:, mock_on_replay:, finalize:, experiment_id:, surface: :span,
70
71
  explicit_span_receiver: self, explicit_span_method_name: method_name) do
71
72
  super(*args, **kwargs, &block)
72
73
  end
@@ -134,7 +135,8 @@ module Bitfab
134
135
  # Configure an existing method only when it is discovered beneath a
135
136
  # bitfab_trace root. Outside subtree capture the method runs unchanged.
136
137
  def self.node(klass, method_name, name: nil, type: "custom", capture: true,
137
- test_run_id: nil, mock_on_replay: nil, finalize: nil, client: nil)
138
+ experiment_id: nil, mock_on_replay: nil, finalize: nil, client: nil, test_run_id: nil)
139
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
138
140
  validate_node_options!(capture:, mock_on_replay:)
139
141
  method_name_str = method_name.to_s
140
142
  bound_client = client
@@ -153,7 +155,7 @@ module Bitfab
153
155
 
154
156
  span_name = name || Subtree.qualified_name(self, method_name)
155
157
  call = Subtree::ConfiguredCall.new(receiver: self, method_name:, name: span_name,
156
- type:, args:, kwargs:, test_run_id:)
158
+ type:, args:, kwargs:, experiment_id:)
157
159
  unless call.owner
158
160
  next call.run { super(*args, **kwargs, &block) }
159
161
  end
@@ -164,7 +166,7 @@ module Bitfab
164
166
  span_name:, span_type: type, function_name: method_name_str,
165
167
  args:, kwargs:, capture_when: "nested", mock_on_replay: policy,
166
168
  explicit_span_receiver: self, explicit_span_method_name: method_name,
167
- test_run_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?, declared_node: true,
169
+ experiment_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?, declared_node: true,
168
170
  on_start: call.method(:start), on_complete: call.method(:complete),
169
171
  around_body: call.method(:run)) { super(*args, **kwargs, &block) }
170
172
  end
@@ -214,7 +216,7 @@ module Bitfab
214
216
  # replay() uses this to reject a trace_function_key that contradicts the
215
217
  # method's declared key: a mismatch fetches one function's historical
216
218
  # traces but re-records the replay under the method's own key, producing
217
- # an incoherent test run. Mirrors the TypeScript and Python SDKs, which
219
+ # an incoherent experiment. Mirrors the TypeScript and Python SDKs, which
218
220
  # throw on the same key mismatch.
219
221
  def self.trace_function_key_for(receiver, method_name)
220
222
  name = method_name.to_s
@@ -265,7 +267,8 @@ module Bitfab
265
267
  # When true, `client.replay(...)` returns this span's
266
268
  # historical output instead of executing the wrapped method.
267
269
  def bitfab_span(method_name, trace_function_key: nil, name: nil, type: "custom",
268
- capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
270
+ capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, test_run_id: nil)
271
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
269
272
  trace_function_key ||= @bitfab_function_key
270
273
  unless trace_function_key
271
274
  raise "No trace function key provided. Pass `trace_function_key:` to `bitfab_span` " \
@@ -275,7 +278,7 @@ module Bitfab
275
278
  # If the method already exists (inline or after-method style), wrap it immediately
276
279
  if method_defined?(method_name) || private_method_defined?(method_name)
277
280
  _bitfab_wrap_method(
278
- method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, finalize:, test_run_id:
281
+ method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, finalize:, experiment_id:
279
282
  )
280
283
  else
281
284
  # Method doesn't exist yet (before-method style) - register for method_added hook
@@ -287,7 +290,7 @@ module Bitfab
287
290
  capture_when:,
288
291
  mock_on_replay:,
289
292
  finalize:,
290
- test_run_id:
293
+ experiment_id:
291
294
  }
292
295
  end
293
296
  end
@@ -325,9 +328,10 @@ module Bitfab
325
328
  # EXPERIMENTAL. Configure one method discovered beneath bitfab_trace.
326
329
  # The node inherits the active trace key and never traces by itself.
327
330
  def bitfab_node(method_name, name: nil, type: "custom", capture: true,
328
- test_run_id: nil, mock_on_replay: nil, finalize: nil)
331
+ experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
332
+ experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
329
333
  Traceable.send(:validate_node_options!, capture:, mock_on_replay:)
330
- config = {name:, type:, capture:, test_run_id:, mock_on_replay:, finalize:}
334
+ config = {name:, type:, capture:, experiment_id:, mock_on_replay:, finalize:}
331
335
  if method_defined?(method_name) || private_method_defined?(method_name)
332
336
  _bitfab_node_method(method_name, **config)
333
337
  else
@@ -361,14 +365,14 @@ module Bitfab
361
365
  end
362
366
 
363
367
  def _bitfab_node_method(method_name, name: nil, type: "custom", capture: true,
364
- test_run_id: nil, mock_on_replay: nil, finalize: nil)
368
+ experiment_id: nil, mock_on_replay: nil, finalize: nil)
365
369
  Traceable.node(
366
370
  self,
367
371
  method_name,
368
372
  name:,
369
373
  type:,
370
374
  capture:,
371
- test_run_id:,
375
+ experiment_id:,
372
376
  mock_on_replay:,
373
377
  finalize:,
374
378
  client: nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.59.0"
4
+ VERSION = "0.59.1"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
  require_relative "bitfab/version"
6
6
  require_relative "bitfab/constants"
7
7
  require_relative "bitfab/warn_once"
8
+ require_relative "bitfab/experiment_id"
8
9
  require_relative "bitfab/payload_budget"
9
10
  require_relative "bitfab/serialize"
10
11
  require_relative "bitfab/db_snapshot"
@@ -175,7 +176,10 @@ module Bitfab
175
176
  # succeeded, errored, and item (the object replay already passes to
176
177
  # on_item_finish)
177
178
  def report_replay_progress(progress)
178
- warn("#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}")
179
+ payload = Replay.json_safe(progress)
180
+ experiment_id = progress[:experiment_id] if progress.is_a?(Hash)
181
+ payload = payload.merge(experimentId: experiment_id) unless experiment_id.nil?
182
+ warn("#{BITFAB_PROGRESS_PREFIX}#{payload.to_json}")
179
183
  rescue
180
184
  nil
181
185
  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.59.0
4
+ version: 0.59.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -167,6 +167,7 @@ files:
167
167
  - lib/bitfab/db_snapshot.rb
168
168
  - lib/bitfab/detached_trace.rb
169
169
  - lib/bitfab/env.rb
170
+ - lib/bitfab/experiment_id.rb
170
171
  - lib/bitfab/git_command.rb
171
172
  - lib/bitfab/git_state.rb
172
173
  - lib/bitfab/graders.rb