bitfab 0.29.0 → 0.30.0

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: 61a89926a13fa008e693f825bb810dea734a1b17bed7ab9b9821daec3e36830f
4
- data.tar.gz: 5026a5ee5252b0a9cf7a8653287e79ac626fa144bd160e7ecf138131dd8c5dba
3
+ metadata.gz: 1711bd0806a246ec36f15e2bedd0b31d8fe8c9df0aacc922d7ad15a20badf629
4
+ data.tar.gz: 2e619cf976e2909bdf1bf629cb350e6b761115d4ec9d8372beaacfe5b46adbbf
5
5
  SHA512:
6
- metadata.gz: 803c6b775c40a3c7dad53d4b380c2b844d94c908b56e58ea899e359e674b86f553d4dd14f9c6aa1e7ad6a6552991fec8c24688a482df57f3cdedda4345a0483b
7
- data.tar.gz: 4086ecadb3d8a1359632d8fee0df1b6bd6febac396b7ac0f5374259fba3c3e0b4ec347c47c3ecc2094da0c02753a780de632a08c7b946389f27a96a8af1467d5
6
+ metadata.gz: 88d118025460c8aed53e48d4ca16815133c67aa04986688bf1ed84ed506a16c41f11e76fd012cb9c18f6ee06a44f1b42e7de811e78834da737b9e4b260d5a024
7
+ data.tar.gz: 6c0e746e23dfdf5995c502e3a40cc12c03888c5e783b924049d2cd4152660c8a226b9ce852532440687630449fe84f027a75082602f175619fbe28e38121e9ba
data/lib/bitfab/client.rb CHANGED
@@ -83,13 +83,18 @@ module Bitfab
83
83
  # replay runs into a single experiment batch
84
84
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
85
85
  # runs against, stored on the resulting experiment for durable attribution
86
+ # @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
87
+ # directly to this experiment, graded as the union with the dataset's
88
+ # runnable graders at completion; each must be an active/live grader in the
89
+ # same org and trace function or the server rejects the replay
86
90
  # @param mock [String] mock strategy for child spans: "marked" (default),
87
91
  # "none", or "all". "marked" only mocks spans declared with
88
92
  # mock_on_replay: true; "all" mocks every child span.
89
93
  # @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
90
94
  # onto the method's current signature when its shape changed after the
91
95
  # traces were captured. Receives (args, kwargs, ctx) where ctx is
92
- # { trace_id:, source_span_id: }, and returns [new_args, new_kwargs].
96
+ # { original_trace_id:, original_span_id: } (with deprecated
97
+ # source_trace_id/source_span_id aliases), and returns [new_args, new_kwargs].
93
98
  # @param mock_override [Hash, Array<Hash>, nil] optional selective mock
94
99
  # override(s), each a { match:, value: } hash. match is a callable:
95
100
  # match.call(node) selects spans to substitute (node is
@@ -108,12 +113,12 @@ module Bitfab
108
113
  # A raising callback never crashes the run.
109
114
  # @return [Hash] with :items, :test_run_id, :test_run_url
110
115
  def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
111
- name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "marked",
116
+ name: nil, code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
112
117
  adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
113
118
  Replay.run(
114
119
  self, receiver, method_name,
115
120
  trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
116
- code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, mock:, adapt_inputs:,
121
+ code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
117
122
  mock_override:, environment:,
118
123
  on_progress:
119
124
  )
@@ -375,6 +380,9 @@ module Bitfab
375
380
  {
376
381
  neon_branch_id: lease["neonBranchId"],
377
382
  snapshot_timestamp: lease["snapshotTimestamp"],
383
+ original_trace_id: replay_ctx[:source_bitfab_trace_id],
384
+ # Deprecated wire alias, kept so this SDK still reports usage
385
+ # against servers that predate the rename.
378
386
  source_trace_id: replay_ctx[:source_bitfab_trace_id],
379
387
  accessed: replay_ctx[:db_snapshot_accessed] == true
380
388
  }
@@ -542,10 +550,11 @@ module Bitfab
542
550
  # db_snapshot_usage: replay DB branch usage record, present only when a
543
551
  # lease was attached to the replay item. Serialized as `db_snapshot_usage`
544
552
  # on the raw trace so the server can stamp the trace's metadata at ingest:
545
- # { neon_branch_id:, snapshot_timestamp: (optional), source_trace_id:
546
- # (optional), accessed: } with :accessed true if customer code obtained
547
- # the branch URL and false if it ignored it. nil outside replay or when no
548
- # lease was attached, in which case the key is omitted entirely.
553
+ # { neon_branch_id:, snapshot_timestamp: (optional), original_trace_id:
554
+ # (optional, with its deprecated source_trace_id alias), accessed: } with
555
+ # :accessed true if customer code obtained the branch URL and false if it
556
+ # ignored it. nil outside replay or when no lease was attached, in which
557
+ # case the key is omitted entirely.
549
558
  def send_trace_completion(trace_function_key:, trace_id:, started_at:, ended_at:, db_snapshot_usage: nil)
550
559
  trace_state = TraceState.get(trace_id)
551
560
  trace_started_at = trace_state&.dig(:started_at) || started_at
@@ -573,8 +582,11 @@ module Bitfab
573
582
  if db_snapshot_usage[:snapshot_timestamp]
574
583
  usage["snapshot_timestamp"] = db_snapshot_usage[:snapshot_timestamp]
575
584
  end
576
- if db_snapshot_usage[:source_trace_id]
577
- usage["source_trace_id"] = db_snapshot_usage[:source_trace_id]
585
+ if db_snapshot_usage[:original_trace_id]
586
+ usage["original_trace_id"] = db_snapshot_usage[:original_trace_id]
587
+ # Deprecated wire alias, kept so this SDK still reports usage
588
+ # against servers that predate the rename.
589
+ usage["source_trace_id"] = db_snapshot_usage[:original_trace_id]
578
590
  end
579
591
  usage["accessed"] = db_snapshot_usage[:accessed]
580
592
  raw_trace["db_snapshot_usage"] = usage
@@ -112,8 +112,13 @@ module Bitfab
112
112
  # experiment/test run
113
113
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
114
114
  # runs against, stored on the resulting experiment for durable attribution
115
+ # @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
116
+ # directly to this experiment, graded as the union with the dataset's
117
+ # runnable graders at completion; each must be an active/live grader in the
118
+ # same org and trace function or the server rejects the replay
115
119
  def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
116
- code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil)
120
+ code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_id: nil,
121
+ grader_ids: nil)
117
122
  payload = {
118
123
  "traceFunctionKey" => trace_function_key
119
124
  }
@@ -127,6 +132,7 @@ module Bitfab
127
132
  payload["experimentGroupId"] = experiment_group_id unless experiment_group_id.nil?
128
133
  payload["includeDbBranchLease"] = true if include_db_branch_lease
129
134
  payload["datasetId"] = dataset_id unless dataset_id.nil?
135
+ payload["graderIds"] = grader_ids unless grader_ids.nil?
130
136
 
131
137
  # When DB branching is on, the server resolves a Neon preview branch per
132
138
  # item (snapshot + restore + poll), which can run several seconds each.
data/lib/bitfab/replay.rb CHANGED
@@ -104,27 +104,36 @@ module Bitfab
104
104
  # replay runs into a single experiment batch
105
105
  # @param dataset_id [String, nil] optional UUID of the dataset this replay
106
106
  # runs against, stored on the resulting experiment for durable attribution
107
+ # @param grader_ids [Array<String>, nil] optional UUIDs of graders attached
108
+ # directly to this experiment, graded as the union with the dataset's
109
+ # runnable graders at completion; each must be an active/live grader in the
110
+ # same org and trace function or the server rejects the replay
107
111
  # @param mock [String] mock strategy for child spans: "marked" (default),
108
112
  # "none", or "all". "marked" only mocks spans declared with
109
113
  # mock_on_replay: true; "all" mocks every child span.
110
114
  # @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
111
115
  # onto the method's current signature when its shape changed after the
112
116
  # traces were captured. Receives (args, kwargs, ctx) where ctx is
113
- # { trace_id:, source_span_id: }, and returns [new_args, new_kwargs]. Runs
114
- # per item inside the same rescue as the method, so a raising adapter sets
115
- # that item's :error rather than crashing the run.
117
+ # { original_trace_id:, original_span_id: } (with deprecated
118
+ # source_trace_id/source_span_id aliases), and returns [new_args, new_kwargs].
119
+ # Runs per item inside the same rescue as the method, so a raising adapter
120
+ # sets that item's :error rather than crashing the run.
116
121
  # @param on_progress [#call, nil] optional callback invoked once per item as
117
122
  # it finishes, with a running-totals hash { completed:, total:, succeeded:,
118
- # errored:, item: } where item is { trace_id:, error:, duration_ms: } for
119
- # the single item that just settled. trace_id is the SOURCE (historical)
120
- # trace that was replayed, error is that item's replay error or nil, and
123
+ # errored:, item: } where item is { trace_id:, original_trace_id:,
124
+ # original_span_id:, error:, duration_ms: } for the single item that just
125
+ # settled (source_trace_id/source_span_id remain as deprecated aliases).
126
+ # trace_id is the new server replay trace id, written in after the run
127
+ # completes (nil during progress callbacks); original_trace_id is the
128
+ # ORIGINAL (historical) trace that was replayed, error is that item's replay
129
+ # error or nil, and
121
130
  # duration_ms is how long that one trace took to replay. Use it to
122
131
  # render replay progress (e.g. a per-trace log). A raising callback never
123
132
  # crashes the run.
124
133
  # @return [Hash] with :items, :test_run_id, :test_run_url
125
134
  def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
126
135
  max_concurrency: 10, code_change_description: nil, code_change_files: nil, experiment_group_id: nil,
127
- dataset_id: nil, mock: "marked",
136
+ dataset_id: nil, grader_ids: nil, mock: "marked",
128
137
  adapt_inputs: nil, mock_override: nil, environment: nil, on_progress: nil)
129
138
  unless MOCK_STRATEGIES.include?(mock.to_s)
130
139
  raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
@@ -179,7 +188,8 @@ module Bitfab
179
188
  code_change_files:,
180
189
  experiment_group_id:,
181
190
  include_db_branch_lease:,
182
- dataset_id:
191
+ dataset_id:,
192
+ grader_ids:
183
193
  )
184
194
  test_run_id = replay_data["testRunId"]
185
195
  test_run_url = replay_data["testRunUrl"]
@@ -194,10 +204,9 @@ module Bitfab
194
204
 
195
205
  # Every item joined its own trace-persistence threads (span uploads +
196
206
  # completion) in execute_item, so all replay traces are on the server
197
- # by now: no flush needed, and complete_replay's trace-ID mapping is
198
- # deterministic. complete_replay failures propagate: a missing mapping
199
- # means verdicts can't be persisted, which callers must hear about
200
- # loudly.
207
+ # by now: no flush needed. complete_replay finalizes the run and returns
208
+ # the token/diagnostic mapping; its failures propagate loudly because a
209
+ # run that never completed can't be finalized.
201
210
  complete_response = http_client.complete_replay(test_run_id)
202
211
  trace_id_map = complete_response&.dig("traceIds")
203
212
  # Per-replay-trace token usage keyed by server trace id: the REPLAYED
@@ -205,66 +214,85 @@ module Bitfab
205
214
  # item's :tokens.
206
215
  replay_tokens = complete_response&.dig("tokens") || {}
207
216
 
208
- if trace_id_map.nil?
209
- # Older servers don't return the mapping. Preserve the legacy
210
- # nil-trace_id behavior but say why.
211
- warn "Bitfab: server did not return replay trace IDs; item trace_id " \
212
- "will be nil (server upgrade required for verdict persistence)"
213
- result_items.each { |item| item[:trace_id] = nil }
214
- else
215
- # Map each item's locally-generated trace ID to the server's trace
216
- # row ID. A completed item with no mapping means its trace was sent
217
- # but the server has no record: a nil trace_id blocks verdict
218
- # persistence and the Studio experiments view downstream, so this
219
- # must never be silent.
220
- #
221
- # Severity splits on scope:
222
- # - ALL completed items missing: systemic (the replayed method is
223
- # not traced, or uploads are wholesale broken). Raise; the run's
224
- # results are unusable for persistence.
225
- # - SOME completed items missing: per-item upload failure (transient
226
- # network blip, one oversized payload). Nil those items and warn
227
- # loudly, but return the run so callers can persist verdicts for
228
- # the items that landed.
217
+ # trace_id_map maps each item's client-side correlation id (:_sdk_trace_id,
218
+ # which tagged that item's spans during the run) to the server's trace row
219
+ # id. We use it to write the real server replay trace id into item[:trace_id]
220
+ # now that the row exists, attach each item's server-aggregated token usage,
221
+ # and detect a systemic upload failure early. Verdict persistence does NOT
222
+ # use this map: it is keyed by the original-trace lineage (:original_trace_id +
223
+ # test_run_id), which needs no client-held server id. Older servers that omit
224
+ # the map yield no replay tokens and leave item[:trace_id] nil.
225
+ unless trace_id_map.nil?
229
226
  missing = []
230
227
  completed_count = 0
231
228
  result_items.each do |item|
232
- next unless item[:trace_id]
233
-
234
- mapped = trace_id_map[item[:trace_id]]
229
+ local_id = item[:_sdk_trace_id]
230
+ mapped = local_id && trace_id_map[local_id]
231
+ # Write the real server replay trace id in as it comes back; the item
232
+ # held nil until now (the client correlation id is never surfaced).
233
+ item[:trace_id] = mapped
235
234
  if item[:error].nil?
236
235
  completed_count += 1
237
- missing << item[:trace_id] if mapped.nil?
236
+ missing << local_id if mapped.nil?
238
237
  end
239
- # Pull this item's replayed-run tokens by its server trace id, before
240
- # :trace_id is overwritten with that id below.
241
238
  item[:tokens] = normalize_tokens(replay_tokens[mapped]) if mapped
242
- item[:trace_id] = mapped
243
239
  end
244
- if missing.any?
240
+ # ALL completed items missing: systemic (the replayed method is not
241
+ # traced, or uploads are wholesale broken). Raise; the run's traces
242
+ # never persisted, so nothing can be labeled.
243
+ if completed_count.positive? && missing.length == completed_count
245
244
  trace_count = complete_response["traceCount"]
246
245
  server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
247
- if missing.length == completed_count
248
- raise "Replay completed but the server has no persisted trace for " \
249
- "any of the #{completed_count} completed item(s) " \
250
- "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
251
- "joined, so either the uploads failed or the replayed method is " \
252
- "not traced (no root span was emitted)."
253
- end
246
+ raise "Replay completed but the server has no persisted trace for " \
247
+ "any of the #{completed_count} completed item(s) " \
248
+ "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
249
+ "joined, so either the uploads failed or the replayed method is " \
250
+ "not traced (no root span was emitted)."
251
+ end
252
+ # SOME completed items missing: per-item upload failure. Warn, but
253
+ # return the run; the items that landed can still be labeled.
254
+ if missing.any?
254
255
  warn "Bitfab: server has no persisted trace for #{missing.length} of " \
255
256
  "#{completed_count} completed replay item(s) " \
256
- "(test_run_id #{test_run_id}).#{server_count} Their trace_id is nil " \
257
- "and verdicts cannot be persisted for them. Missing: #{missing.join(", ")}"
257
+ "(test_run_id #{test_run_id}). Their replay token usage is " \
258
+ "unavailable and they cannot be labeled."
258
259
  end
259
260
  end
260
261
 
261
- replay_result = {
262
+ # Strip the internal correlation handle so returned items expose only the
263
+ # public shape (runs against older servers that omit the map too).
264
+ result_items.each { |item| item.delete(:_sdk_trace_id) }
265
+
266
+ result = {
262
267
  items: result_items,
263
268
  test_run_id:,
264
269
  test_run_url: "#{client.service_url}#{test_run_url}"
265
270
  }
266
- write_replay_result_file(replay_result)
267
- replay_result
271
+ # Persist the enriched result two ways so the Bitfab plugin never has to
272
+ # parse the replay's stdout (which a dependency's logging can corrupt):
273
+ # write it to BITFAB_REPLAY_RESULT_PATH when the plugin set that env var,
274
+ # and stream a terminal "complete" progress event. The plugin prefers the
275
+ # streamed event and falls back to the file. The event routes through
276
+ # on_progress so only progress-reporting runs emit it.
277
+ write_replay_result_file(result)
278
+ if on_progress
279
+ errored = result_items.count { |item| !item[:error].nil? }
280
+ total = result_items.length
281
+ begin
282
+ on_progress.call({
283
+ type: "complete",
284
+ test_run_id:,
285
+ completed: total,
286
+ total:,
287
+ succeeded: total - errored,
288
+ errored:,
289
+ result:
290
+ })
291
+ rescue => e
292
+ warn "Bitfab: replay on_progress callback raised: #{e.message}"
293
+ end
294
+ end
295
+ result
268
296
  end
269
297
 
270
298
  def write_replay_result_file(result)
@@ -296,11 +324,13 @@ module Bitfab
296
324
  succeeded = 0
297
325
  errored = 0
298
326
  # Each event carries the single item that just settled so a progress UI
299
- # can render per-trace pass/fail as the run streams. trace_id is the
300
- # SOURCE (historical) trace that was replayed, taken from the server
301
- # item: the result's own :trace_id at this stage is the new replay
302
- # trace id (assigned in run() after complete_replay), not the source.
303
- report = lambda do |result, source_trace_id, test_run_id|
327
+ # can render per-trace pass/fail as the run streams. The item's :trace_id
328
+ # is nil at this stage: the server replay trace id isn't known until run()
329
+ # writes it in after complete_replay, and the client correlation id is
330
+ # never surfaced. original_trace_id (the historical trace being replayed,
331
+ # taken from the server item) is what a UI keys on to identify what just
332
+ # settled. source_trace_id/source_span_id are kept as deprecated aliases.
333
+ report = lambda do |result, original_trace_id, original_span_id, test_run_id|
304
334
  return unless on_progress
305
335
 
306
336
  progress_mutex.synchronize do
@@ -311,8 +341,12 @@ module Bitfab
311
341
  on_progress.call({
312
342
  test_run_id:, completed:, total:, succeeded:, errored:,
313
343
  item: {
314
- trace_id: source_trace_id,
315
- replay_trace_id: result[:trace_id],
344
+ trace_id: result[:trace_id],
345
+ original_trace_id:,
346
+ original_span_id:,
347
+ # Deprecated aliases for original_trace_id/original_span_id.
348
+ source_trace_id: original_trace_id,
349
+ source_span_id: original_span_id,
316
350
  input: result[:input],
317
351
  result: result[:result],
318
352
  original_output: result[:original_output],
@@ -333,7 +367,7 @@ module Bitfab
333
367
  server_items.map do |item|
334
368
  result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
335
369
  adapt_inputs, include_db_branch_lease, mock_overrides:)
336
- report.call(result, item["traceId"], test_run_id)
370
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
337
371
  result
338
372
  end
339
373
  else
@@ -351,7 +385,7 @@ module Bitfab
351
385
  result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
352
386
  adapt_inputs, include_db_branch_lease, mock_overrides:)
353
387
  results_mutex.synchronize { results[idx] = result }
354
- report.call(result, item["traceId"], test_run_id)
388
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
355
389
  end
356
390
  end
357
391
  end
@@ -370,6 +404,11 @@ module Bitfab
370
404
  def process_single_item(http_client, server_item, receiver, method_name, test_run_id, mock_strategy,
371
405
  adapt_inputs = nil, include_db_branch_lease = false, mock_overrides: [])
372
406
  metrics = extract_server_item_metrics(server_item)
407
+ # The ORIGINAL (historical) trace/span this item replays. Canonical
408
+ # keys are originalTraceId/originalSpanId; older servers send them under
409
+ # the deprecated sourceTraceId/sourceSpanId aliases (see *_of helpers).
410
+ original_trace_id = original_trace_id_of(server_item)
411
+ original_span_id = original_span_id_of(server_item)
373
412
  # The server resolves a Neon preview branch per item during /replay/start
374
413
  # (only when include_db_branch_lease was sent). Release it in the +ensure+
375
414
  # below so any raise (span fetch, mock-tree build, or the replayed
@@ -378,7 +417,7 @@ module Bitfab
378
417
  # lease (env.active? is false for those).
379
418
  lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
380
419
 
381
- span = http_client.get_external_span(server_item["externalSpanId"])
420
+ span = http_client.get_external_span(original_span_id)
382
421
  item_data = extract_span_data(span)
383
422
 
384
423
  # Fetch the span tree when the base strategy needs recorded outputs
@@ -396,7 +435,7 @@ module Bitfab
396
435
  mock_tree = nil
397
436
  if mock_strategy == "all" || mock_strategy == "marked" || overrides_present
398
437
  begin
399
- tree = http_client.get_span_tree(server_item["externalSpanId"], include_outputs:)
438
+ tree = http_client.get_span_tree(original_span_id, include_outputs:)
400
439
  mock_tree = build_mock_tree(tree["root"] || {})
401
440
  rescue Exception => e # rubocop:disable Lint/RescueException
402
441
  raise if e.is_a?(SystemExit) || e.is_a?(SignalException)
@@ -418,7 +457,13 @@ module Bitfab
418
457
  # override's get_original_output).
419
458
  fetch_span_output = (mock_tree && !include_outputs) ? build_span_output_fetcher(http_client) : nil
420
459
 
421
- adapt_ctx = {trace_id: server_item["traceId"], source_span_id: server_item["externalSpanId"]}
460
+ adapt_ctx = {
461
+ original_trace_id:,
462
+ original_span_id:,
463
+ # Deprecated aliases for original_trace_id/original_span_id.
464
+ source_trace_id: original_trace_id,
465
+ source_span_id: original_span_id
466
+ }
422
467
 
423
468
  execute_item(
424
469
  item_data,
@@ -435,11 +480,11 @@ module Bitfab
435
480
  adapt_inputs:,
436
481
  adapt_ctx:,
437
482
  db_branch_lease: lease,
438
- source_bitfab_trace_id: server_item["traceId"],
483
+ source_bitfab_trace_id: original_trace_id,
439
484
  db_snapshot_ref: server_item["dbSnapshotRef"]
440
485
  )
441
486
  rescue => e
442
- warn "Bitfab: replay item for span #{server_item["externalSpanId"]} failed before execution: #{e.message}"
487
+ warn "Bitfab: replay item for span #{original_span_id} failed before execution: #{e.message}"
443
488
  {
444
489
  input: [],
445
490
  result: nil,
@@ -449,12 +494,28 @@ module Bitfab
449
494
  tokens: metrics&.dig(:tokens),
450
495
  model: metrics&.dig(:model),
451
496
  trace_id: nil,
497
+ original_trace_id:,
498
+ original_span_id:,
499
+ # Deprecated aliases for original_trace_id/original_span_id.
500
+ source_trace_id: original_trace_id,
501
+ source_span_id: original_span_id,
452
502
  db_snapshot_ref: server_item["dbSnapshotRef"]
453
503
  }
454
504
  ensure
455
505
  release_db_branch_lease(http_client, lease) if lease
456
506
  end
457
507
 
508
+ # The ORIGINAL (historical) trace/span an item replays. Canonical server
509
+ # keys are originalTraceId/originalSpanId; older servers send them under
510
+ # the deprecated sourceTraceId/sourceSpanId aliases.
511
+ def original_trace_id_of(server_item)
512
+ server_item["originalTraceId"] || server_item["sourceTraceId"]
513
+ end
514
+
515
+ def original_span_id_of(server_item)
516
+ server_item["originalSpanId"] || server_item["sourceSpanId"]
517
+ end
518
+
458
519
  # Delete the per-item Neon preview branch. Best-effort: a failure is warned
459
520
  # but never raised: the server-side TTL janitor reaps orphans.
460
521
  def release_db_branch_lease(http_client, lease)
@@ -583,6 +644,10 @@ module Bitfab
583
644
 
584
645
  fn_result = nil
585
646
  fn_error = nil
647
+ # Client-side correlation id that tags this item's replay spans so the
648
+ # server can echo back the row id it minted (resolved in run()'s
649
+ # complete-replay loop). Carried on the item under :_sdk_trace_id, never
650
+ # surfaced as the public :trace_id.
586
651
  sdk_trace_id = SecureRandom.uuid
587
652
  # Collects the root span's persistence threads (span uploads + trace
588
653
  # completion). Joined below so this item's trace is on the server
@@ -607,7 +672,13 @@ module Bitfab
607
672
  # supplied. Inside the rescue so a raising adapter surfaces on this
608
673
  # item's :error instead of crashing the run; args is reported on :input.
609
674
  if adapt_inputs
610
- ctx = adapt_ctx || {trace_id: nil, source_span_id: input_source_span_id}
675
+ ctx = adapt_ctx || {
676
+ original_trace_id: nil,
677
+ original_span_id: input_source_span_id,
678
+ # Deprecated aliases for original_trace_id/original_span_id.
679
+ source_trace_id: nil,
680
+ source_span_id: input_source_span_id
681
+ }
611
682
  args, kwargs = adapt_inputs.call(args, kwargs, ctx)
612
683
  end
613
684
  fn_result = if kwargs.empty?
@@ -633,7 +704,16 @@ module Bitfab
633
704
  duration_ms: metrics[:duration_ms],
634
705
  tokens: metrics[:tokens],
635
706
  model: metrics[:model],
636
- trace_id: sdk_trace_id,
707
+ # Written in by run() from the complete-replay response once the server
708
+ # has minted this replay trace's row. Nil until then: the client-side
709
+ # correlation id (below) is never surfaced as the public :trace_id.
710
+ trace_id: nil,
711
+ _sdk_trace_id: sdk_trace_id,
712
+ original_trace_id: source_bitfab_trace_id,
713
+ original_span_id: input_source_span_id,
714
+ # Deprecated aliases for original_trace_id/original_span_id.
715
+ source_trace_id: source_bitfab_trace_id,
716
+ source_span_id: input_source_span_id,
637
717
  db_snapshot_ref:
638
718
  }
639
719
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.29.0"
4
+ VERSION = "0.30.0"
5
5
  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.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team