bitfab 0.29.0 → 0.29.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: 61a89926a13fa008e693f825bb810dea734a1b17bed7ab9b9821daec3e36830f
4
- data.tar.gz: 5026a5ee5252b0a9cf7a8653287e79ac626fa144bd160e7ecf138131dd8c5dba
3
+ metadata.gz: 1f26df032729a0c1fbad20c807e1581ec1c68bb9019e0b8f32a85237c785a6ed
4
+ data.tar.gz: 6acf7a7e153565cf4fb272b651f2740b4d3cb5722b67b6508552aa1b2e81d042
5
5
  SHA512:
6
- metadata.gz: 803c6b775c40a3c7dad53d4b380c2b844d94c908b56e58ea899e359e674b86f553d4dd14f9c6aa1e7ad6a6552991fec8c24688a482df57f3cdedda4345a0483b
7
- data.tar.gz: 4086ecadb3d8a1359632d8fee0df1b6bd6febac396b7ac0f5374259fba3c3e0b4ec347c47c3ecc2094da0c02753a780de632a08c7b946389f27a96a8af1467d5
6
+ metadata.gz: 5fdcbb34b7174b5308d46405c95735d0a986db51f1af6ddbeb26613ebcdd9c35fade8a40553b1f88b78dc699261146d8cd9d4d7abffda560fbf06f40527717ce
7
+ data.tar.gz: ea2cd76d972b4b67f1554e1baf28b2aeeaf8560d1329eeff1f6df721273ce6f6e27193d20e0c0995ea77077544a5ab6f64cbb06908276eb8a27c6a6d590171b7
data/lib/bitfab/client.rb CHANGED
@@ -89,7 +89,8 @@ module Bitfab
89
89
  # @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
90
90
  # onto the method's current signature when its shape changed after the
91
91
  # traces were captured. Receives (args, kwargs, ctx) where ctx is
92
- # { trace_id:, source_span_id: }, and returns [new_args, new_kwargs].
92
+ # { original_trace_id:, original_span_id: } (with deprecated
93
+ # source_trace_id/source_span_id aliases), and returns [new_args, new_kwargs].
93
94
  # @param mock_override [Hash, Array<Hash>, nil] optional selective mock
94
95
  # override(s), each a { match:, value: } hash. match is a callable:
95
96
  # match.call(node) selects spans to substitute (node is
@@ -375,6 +376,9 @@ module Bitfab
375
376
  {
376
377
  neon_branch_id: lease["neonBranchId"],
377
378
  snapshot_timestamp: lease["snapshotTimestamp"],
379
+ original_trace_id: replay_ctx[:source_bitfab_trace_id],
380
+ # Deprecated wire alias, kept so this SDK still reports usage
381
+ # against servers that predate the rename.
378
382
  source_trace_id: replay_ctx[:source_bitfab_trace_id],
379
383
  accessed: replay_ctx[:db_snapshot_accessed] == true
380
384
  }
@@ -542,10 +546,11 @@ module Bitfab
542
546
  # db_snapshot_usage: replay DB branch usage record, present only when a
543
547
  # lease was attached to the replay item. Serialized as `db_snapshot_usage`
544
548
  # 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.
549
+ # { neon_branch_id:, snapshot_timestamp: (optional), original_trace_id:
550
+ # (optional, with its deprecated source_trace_id alias), accessed: } with
551
+ # :accessed true if customer code obtained the branch URL and false if it
552
+ # ignored it. nil outside replay or when no lease was attached, in which
553
+ # case the key is omitted entirely.
549
554
  def send_trace_completion(trace_function_key:, trace_id:, started_at:, ended_at:, db_snapshot_usage: nil)
550
555
  trace_state = TraceState.get(trace_id)
551
556
  trace_started_at = trace_state&.dig(:started_at) || started_at
@@ -573,8 +578,11 @@ module Bitfab
573
578
  if db_snapshot_usage[:snapshot_timestamp]
574
579
  usage["snapshot_timestamp"] = db_snapshot_usage[:snapshot_timestamp]
575
580
  end
576
- if db_snapshot_usage[:source_trace_id]
577
- usage["source_trace_id"] = db_snapshot_usage[:source_trace_id]
581
+ if db_snapshot_usage[:original_trace_id]
582
+ usage["original_trace_id"] = db_snapshot_usage[:original_trace_id]
583
+ # Deprecated wire alias, kept so this SDK still reports usage
584
+ # against servers that predate the rename.
585
+ usage["source_trace_id"] = db_snapshot_usage[:original_trace_id]
578
586
  end
579
587
  usage["accessed"] = db_snapshot_usage[:accessed]
580
588
  raw_trace["db_snapshot_usage"] = usage
data/lib/bitfab/replay.rb CHANGED
@@ -110,14 +110,19 @@ module Bitfab
110
110
  # @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
111
111
  # onto the method's current signature when its shape changed after the
112
112
  # 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.
113
+ # { original_trace_id:, original_span_id: } (with deprecated
114
+ # source_trace_id/source_span_id aliases), and returns [new_args, new_kwargs].
115
+ # Runs per item inside the same rescue as the method, so a raising adapter
116
+ # sets that item's :error rather than crashing the run.
116
117
  # @param on_progress [#call, nil] optional callback invoked once per item as
117
118
  # 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
119
+ # errored:, item: } where item is { trace_id:, original_trace_id:,
120
+ # original_span_id:, error:, duration_ms: } for the single item that just
121
+ # settled (source_trace_id/source_span_id remain as deprecated aliases).
122
+ # trace_id is the new server replay trace id, written in after the run
123
+ # completes (nil during progress callbacks); original_trace_id is the
124
+ # ORIGINAL (historical) trace that was replayed, error is that item's replay
125
+ # error or nil, and
121
126
  # duration_ms is how long that one trace took to replay. Use it to
122
127
  # render replay progress (e.g. a per-trace log). A raising callback never
123
128
  # crashes the run.
@@ -194,10 +199,9 @@ module Bitfab
194
199
 
195
200
  # Every item joined its own trace-persistence threads (span uploads +
196
201
  # 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.
202
+ # by now: no flush needed. complete_replay finalizes the run and returns
203
+ # the token/diagnostic mapping; its failures propagate loudly because a
204
+ # run that never completed can't be finalized.
201
205
  complete_response = http_client.complete_replay(test_run_id)
202
206
  trace_id_map = complete_response&.dig("traceIds")
203
207
  # Per-replay-trace token usage keyed by server trace id: the REPLAYED
@@ -205,66 +209,85 @@ module Bitfab
205
209
  # item's :tokens.
206
210
  replay_tokens = complete_response&.dig("tokens") || {}
207
211
 
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.
212
+ # trace_id_map maps each item's client-side correlation id (:_sdk_trace_id,
213
+ # which tagged that item's spans during the run) to the server's trace row
214
+ # id. We use it to write the real server replay trace id into item[:trace_id]
215
+ # now that the row exists, attach each item's server-aggregated token usage,
216
+ # and detect a systemic upload failure early. Verdict persistence does NOT
217
+ # use this map: it is keyed by the original-trace lineage (:original_trace_id +
218
+ # test_run_id), which needs no client-held server id. Older servers that omit
219
+ # the map yield no replay tokens and leave item[:trace_id] nil.
220
+ unless trace_id_map.nil?
229
221
  missing = []
230
222
  completed_count = 0
231
223
  result_items.each do |item|
232
- next unless item[:trace_id]
233
-
234
- mapped = trace_id_map[item[:trace_id]]
224
+ local_id = item[:_sdk_trace_id]
225
+ mapped = local_id && trace_id_map[local_id]
226
+ # Write the real server replay trace id in as it comes back; the item
227
+ # held nil until now (the client correlation id is never surfaced).
228
+ item[:trace_id] = mapped
235
229
  if item[:error].nil?
236
230
  completed_count += 1
237
- missing << item[:trace_id] if mapped.nil?
231
+ missing << local_id if mapped.nil?
238
232
  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
233
  item[:tokens] = normalize_tokens(replay_tokens[mapped]) if mapped
242
- item[:trace_id] = mapped
243
234
  end
244
- if missing.any?
235
+ # ALL completed items missing: systemic (the replayed method is not
236
+ # traced, or uploads are wholesale broken). Raise; the run's traces
237
+ # never persisted, so nothing can be labeled.
238
+ if completed_count.positive? && missing.length == completed_count
245
239
  trace_count = complete_response["traceCount"]
246
240
  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
241
+ raise "Replay completed but the server has no persisted trace for " \
242
+ "any of the #{completed_count} completed item(s) " \
243
+ "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
244
+ "joined, so either the uploads failed or the replayed method is " \
245
+ "not traced (no root span was emitted)."
246
+ end
247
+ # SOME completed items missing: per-item upload failure. Warn, but
248
+ # return the run; the items that landed can still be labeled.
249
+ if missing.any?
254
250
  warn "Bitfab: server has no persisted trace for #{missing.length} of " \
255
251
  "#{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(", ")}"
252
+ "(test_run_id #{test_run_id}). Their replay token usage is " \
253
+ "unavailable and they cannot be labeled."
258
254
  end
259
255
  end
260
256
 
261
- replay_result = {
257
+ # Strip the internal correlation handle so returned items expose only the
258
+ # public shape (runs against older servers that omit the map too).
259
+ result_items.each { |item| item.delete(:_sdk_trace_id) }
260
+
261
+ result = {
262
262
  items: result_items,
263
263
  test_run_id:,
264
264
  test_run_url: "#{client.service_url}#{test_run_url}"
265
265
  }
266
- write_replay_result_file(replay_result)
267
- replay_result
266
+ # Persist the enriched result two ways so the Bitfab plugin never has to
267
+ # parse the replay's stdout (which a dependency's logging can corrupt):
268
+ # write it to BITFAB_REPLAY_RESULT_PATH when the plugin set that env var,
269
+ # and stream a terminal "complete" progress event. The plugin prefers the
270
+ # streamed event and falls back to the file. The event routes through
271
+ # on_progress so only progress-reporting runs emit it.
272
+ write_replay_result_file(result)
273
+ if on_progress
274
+ errored = result_items.count { |item| !item[:error].nil? }
275
+ total = result_items.length
276
+ begin
277
+ on_progress.call({
278
+ type: "complete",
279
+ test_run_id:,
280
+ completed: total,
281
+ total:,
282
+ succeeded: total - errored,
283
+ errored:,
284
+ result:
285
+ })
286
+ rescue => e
287
+ warn "Bitfab: replay on_progress callback raised: #{e.message}"
288
+ end
289
+ end
290
+ result
268
291
  end
269
292
 
270
293
  def write_replay_result_file(result)
@@ -296,11 +319,13 @@ module Bitfab
296
319
  succeeded = 0
297
320
  errored = 0
298
321
  # 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|
322
+ # can render per-trace pass/fail as the run streams. The item's :trace_id
323
+ # is nil at this stage: the server replay trace id isn't known until run()
324
+ # writes it in after complete_replay, and the client correlation id is
325
+ # never surfaced. original_trace_id (the historical trace being replayed,
326
+ # taken from the server item) is what a UI keys on to identify what just
327
+ # settled. source_trace_id/source_span_id are kept as deprecated aliases.
328
+ report = lambda do |result, original_trace_id, original_span_id, test_run_id|
304
329
  return unless on_progress
305
330
 
306
331
  progress_mutex.synchronize do
@@ -311,8 +336,12 @@ module Bitfab
311
336
  on_progress.call({
312
337
  test_run_id:, completed:, total:, succeeded:, errored:,
313
338
  item: {
314
- trace_id: source_trace_id,
315
- replay_trace_id: result[:trace_id],
339
+ trace_id: result[:trace_id],
340
+ original_trace_id:,
341
+ original_span_id:,
342
+ # Deprecated aliases for original_trace_id/original_span_id.
343
+ source_trace_id: original_trace_id,
344
+ source_span_id: original_span_id,
316
345
  input: result[:input],
317
346
  result: result[:result],
318
347
  original_output: result[:original_output],
@@ -333,7 +362,7 @@ module Bitfab
333
362
  server_items.map do |item|
334
363
  result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
335
364
  adapt_inputs, include_db_branch_lease, mock_overrides:)
336
- report.call(result, item["traceId"], test_run_id)
365
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
337
366
  result
338
367
  end
339
368
  else
@@ -351,7 +380,7 @@ module Bitfab
351
380
  result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
352
381
  adapt_inputs, include_db_branch_lease, mock_overrides:)
353
382
  results_mutex.synchronize { results[idx] = result }
354
- report.call(result, item["traceId"], test_run_id)
383
+ report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
355
384
  end
356
385
  end
357
386
  end
@@ -370,6 +399,11 @@ module Bitfab
370
399
  def process_single_item(http_client, server_item, receiver, method_name, test_run_id, mock_strategy,
371
400
  adapt_inputs = nil, include_db_branch_lease = false, mock_overrides: [])
372
401
  metrics = extract_server_item_metrics(server_item)
402
+ # The ORIGINAL (historical) trace/span this item replays. Canonical
403
+ # keys are originalTraceId/originalSpanId; older servers send them under
404
+ # the deprecated sourceTraceId/sourceSpanId aliases (see *_of helpers).
405
+ original_trace_id = original_trace_id_of(server_item)
406
+ original_span_id = original_span_id_of(server_item)
373
407
  # The server resolves a Neon preview branch per item during /replay/start
374
408
  # (only when include_db_branch_lease was sent). Release it in the +ensure+
375
409
  # below so any raise (span fetch, mock-tree build, or the replayed
@@ -378,7 +412,7 @@ module Bitfab
378
412
  # lease (env.active? is false for those).
379
413
  lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
380
414
 
381
- span = http_client.get_external_span(server_item["externalSpanId"])
415
+ span = http_client.get_external_span(original_span_id)
382
416
  item_data = extract_span_data(span)
383
417
 
384
418
  # Fetch the span tree when the base strategy needs recorded outputs
@@ -396,7 +430,7 @@ module Bitfab
396
430
  mock_tree = nil
397
431
  if mock_strategy == "all" || mock_strategy == "marked" || overrides_present
398
432
  begin
399
- tree = http_client.get_span_tree(server_item["externalSpanId"], include_outputs:)
433
+ tree = http_client.get_span_tree(original_span_id, include_outputs:)
400
434
  mock_tree = build_mock_tree(tree["root"] || {})
401
435
  rescue Exception => e # rubocop:disable Lint/RescueException
402
436
  raise if e.is_a?(SystemExit) || e.is_a?(SignalException)
@@ -418,7 +452,13 @@ module Bitfab
418
452
  # override's get_original_output).
419
453
  fetch_span_output = (mock_tree && !include_outputs) ? build_span_output_fetcher(http_client) : nil
420
454
 
421
- adapt_ctx = {trace_id: server_item["traceId"], source_span_id: server_item["externalSpanId"]}
455
+ adapt_ctx = {
456
+ original_trace_id:,
457
+ original_span_id:,
458
+ # Deprecated aliases for original_trace_id/original_span_id.
459
+ source_trace_id: original_trace_id,
460
+ source_span_id: original_span_id
461
+ }
422
462
 
423
463
  execute_item(
424
464
  item_data,
@@ -435,11 +475,11 @@ module Bitfab
435
475
  adapt_inputs:,
436
476
  adapt_ctx:,
437
477
  db_branch_lease: lease,
438
- source_bitfab_trace_id: server_item["traceId"],
478
+ source_bitfab_trace_id: original_trace_id,
439
479
  db_snapshot_ref: server_item["dbSnapshotRef"]
440
480
  )
441
481
  rescue => e
442
- warn "Bitfab: replay item for span #{server_item["externalSpanId"]} failed before execution: #{e.message}"
482
+ warn "Bitfab: replay item for span #{original_span_id} failed before execution: #{e.message}"
443
483
  {
444
484
  input: [],
445
485
  result: nil,
@@ -449,12 +489,28 @@ module Bitfab
449
489
  tokens: metrics&.dig(:tokens),
450
490
  model: metrics&.dig(:model),
451
491
  trace_id: nil,
492
+ original_trace_id:,
493
+ original_span_id:,
494
+ # Deprecated aliases for original_trace_id/original_span_id.
495
+ source_trace_id: original_trace_id,
496
+ source_span_id: original_span_id,
452
497
  db_snapshot_ref: server_item["dbSnapshotRef"]
453
498
  }
454
499
  ensure
455
500
  release_db_branch_lease(http_client, lease) if lease
456
501
  end
457
502
 
503
+ # The ORIGINAL (historical) trace/span an item replays. Canonical server
504
+ # keys are originalTraceId/originalSpanId; older servers send them under
505
+ # the deprecated sourceTraceId/sourceSpanId aliases.
506
+ def original_trace_id_of(server_item)
507
+ server_item["originalTraceId"] || server_item["sourceTraceId"]
508
+ end
509
+
510
+ def original_span_id_of(server_item)
511
+ server_item["originalSpanId"] || server_item["sourceSpanId"]
512
+ end
513
+
458
514
  # Delete the per-item Neon preview branch. Best-effort: a failure is warned
459
515
  # but never raised: the server-side TTL janitor reaps orphans.
460
516
  def release_db_branch_lease(http_client, lease)
@@ -583,6 +639,10 @@ module Bitfab
583
639
 
584
640
  fn_result = nil
585
641
  fn_error = nil
642
+ # Client-side correlation id that tags this item's replay spans so the
643
+ # server can echo back the row id it minted (resolved in run()'s
644
+ # complete-replay loop). Carried on the item under :_sdk_trace_id, never
645
+ # surfaced as the public :trace_id.
586
646
  sdk_trace_id = SecureRandom.uuid
587
647
  # Collects the root span's persistence threads (span uploads + trace
588
648
  # completion). Joined below so this item's trace is on the server
@@ -607,7 +667,13 @@ module Bitfab
607
667
  # supplied. Inside the rescue so a raising adapter surfaces on this
608
668
  # item's :error instead of crashing the run; args is reported on :input.
609
669
  if adapt_inputs
610
- ctx = adapt_ctx || {trace_id: nil, source_span_id: input_source_span_id}
670
+ ctx = adapt_ctx || {
671
+ original_trace_id: nil,
672
+ original_span_id: input_source_span_id,
673
+ # Deprecated aliases for original_trace_id/original_span_id.
674
+ source_trace_id: nil,
675
+ source_span_id: input_source_span_id
676
+ }
611
677
  args, kwargs = adapt_inputs.call(args, kwargs, ctx)
612
678
  end
613
679
  fn_result = if kwargs.empty?
@@ -633,7 +699,16 @@ module Bitfab
633
699
  duration_ms: metrics[:duration_ms],
634
700
  tokens: metrics[:tokens],
635
701
  model: metrics[:model],
636
- trace_id: sdk_trace_id,
702
+ # Written in by run() from the complete-replay response once the server
703
+ # has minted this replay trace's row. Nil until then: the client-side
704
+ # correlation id (below) is never surfaced as the public :trace_id.
705
+ trace_id: nil,
706
+ _sdk_trace_id: sdk_trace_id,
707
+ original_trace_id: source_bitfab_trace_id,
708
+ original_span_id: input_source_span_id,
709
+ # Deprecated aliases for original_trace_id/original_span_id.
710
+ source_trace_id: source_bitfab_trace_id,
711
+ source_span_id: input_source_span_id,
637
712
  db_snapshot_ref:
638
713
  }
639
714
  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.29.1"
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.29.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team