bitfab 0.74.2 → 0.75.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: 2533a42fd8e3080a107eb28117106080a950076630082392805c6a29d86ab7d8
4
- data.tar.gz: 77cf546b4d9b69b22370e8e8a5b6eec12fff1b8122a4b598b11a0a077953f2ec
3
+ metadata.gz: 6976d07abea8e8c456185b0f5a76bbe861148e2a10baa1c3a65ac1a694b6993d
4
+ data.tar.gz: 072af5081f4c15e748478dbd058a8cc4c249804b345dcc70ed47b3e47f3c5830
5
5
  SHA512:
6
- metadata.gz: cef1f28295d0d4c140bd23b003bb712d460a10fe9225ce730526159c083e20a2bf8a423d4641eeffb369b4a6509b0ef86cbfc992ae5d1f5641c91594d0d0868f
7
- data.tar.gz: 99f81dace42263bf6e770451cbe266ec5e4a5566ecc559e86984d87f7d82f67c214a610a74f358ec188d2dd52383ef84c4f4e4c4be52cf2ce609f7ec818febcc
6
+ metadata.gz: 5189232282c64908bada35335eac364e16bb6fcadfe9b677cdc5b3330735f59dbdebc6ff71fe4a2984ad1a5c8386b45ade841710c0b2ec1ac0d26c0d07c7d44f
7
+ data.tar.gz: 90b5fd0acb0d266e6f2e7d2b6d04dc4a08c66d3e1b25ce46f51425e5020cf372167c40ba141cf05c9f7e4f2348cc9a106f1888e9a5b792bbbe9a4f08d2bb0b80
data/lib/bitfab/client.rb CHANGED
@@ -1021,7 +1021,7 @@ module Bitfab
1021
1021
  "type" => "sdk-function",
1022
1022
  "source" => "ruby-sdk-function",
1023
1023
  "traceFunctionKey" => trace_function_key,
1024
- "externalTrace" => raw_trace,
1024
+ "rawTrace" => raw_trace,
1025
1025
  "completed" => true
1026
1026
  }
1027
1027
 
@@ -1037,7 +1037,7 @@ module Bitfab
1037
1037
  payload["testRunId"] = trace_state[:experiment_id]
1038
1038
  end
1039
1039
 
1040
- @http_client.send_external_trace(payload)
1040
+ @http_client.send_trace_payload(payload)
1041
1041
 
1042
1042
  # Clean up trace state
1043
1043
  TraceState.delete(trace_id)
@@ -1146,7 +1146,7 @@ module Bitfab
1146
1146
  }]
1147
1147
  end
1148
1148
 
1149
- @http_client.send_external_span(payload, declared_node, snapshot)
1149
+ @http_client.send_span_payload(payload, declared_node, snapshot)
1150
1150
  end
1151
1151
 
1152
1152
  # Advance the per-(key, name) call counter when this invocation is a
@@ -1180,10 +1180,6 @@ module Bitfab
1180
1180
  def check_mock_replay(replay_ctx, trace_function_key, span_name, call_index, span_type:, args:, kwargs:, mock_on_replay:)
1181
1181
  mock_entry = replay_ctx[:mock_tree]["#{trace_function_key}:#{span_name}:#{call_index}"]
1182
1182
 
1183
- # 1) Selective overrides. The matcher runs on structural metadata only;
1184
- # the recorded output is fetched (lazily, by externalSpanId) only if the
1185
- # processor actually asks for it via get_original_output, so a pure
1186
- # synthetic override triggers zero output fetches.
1187
1183
  overrides = replay_ctx[:mock_overrides]
1188
1184
  if overrides&.any?
1189
1185
  node = {
@@ -1243,7 +1239,7 @@ module Bitfab
1243
1239
  # Resolve a mock-tree entry's recorded output. Prefers an inline output when
1244
1240
  # the tree carried one (eager "all", or an older server that ignores
1245
1241
  # includeOutputs and returns outputs); otherwise lazily fetches it by
1246
- # externalSpanId through the per-item memoized fetcher on the replay
1242
+ # span ID through the per-item memoized fetcher on the replay
1247
1243
  # context, so only the spans actually mocked pull their output.
1248
1244
  #
1249
1245
  # Deserialization is type-preserving when Ruby-side Marshal+Base64 metadata
@@ -1258,9 +1254,9 @@ module Bitfab
1258
1254
  end
1259
1255
 
1260
1256
  fetcher = replay_ctx[:fetch_span_output]
1261
- external_span_id = mock_entry[:external_span_id]
1262
- if fetcher && external_span_id
1263
- return fetcher.call(external_span_id)
1257
+ span_row_id = mock_entry[:span_row_id]
1258
+ if fetcher && span_row_id
1259
+ return fetcher.call(span_row_id)
1264
1260
  end
1265
1261
 
1266
1262
  # No inline output and nothing to lazily fetch. This is a span that never
@@ -192,8 +192,8 @@ module Bitfab
192
192
  raise last_error
193
193
  end
194
194
 
195
- # Queue an external span on this client's trace transport (fire-and-forget).
196
- def send_external_span(payload, declared_node = false, snapshot = nil)
195
+ # Queue a span payload on this client's trace transport (fire-and-forget).
196
+ def send_span_payload(payload, declared_node = false, snapshot = nil)
197
197
  if (data = payload.dig("rawSpan", "span_data")).is_a?(Hash) && !data.key?("runtime")
198
198
  payload = payload.merge("rawSpan" => payload["rawSpan"].merge("span_data" => data.merge("runtime" => ThreadPropagation.runtime)))
199
199
  end
@@ -334,11 +334,12 @@ module Bitfab
334
334
  request("/api/sdk/replay/start", payload, timeout:)
335
335
  end
336
336
 
337
- # Fetch an external span by ID. Blocking GET request. The replay view keeps
338
- # only the input/output serialization fields used by replay.
339
- def get_external_span(span_id, replay_view: false)
337
+ # Fetch a span's raw record by its Bitfab row ID. Blocking GET request.
338
+ # The replay view keeps only the input/output serialization fields used
339
+ # by replay.
340
+ def get_span_raw(span_id, replay_view: false)
340
341
  query = replay_view ? "?view=replay" : ""
341
- get("/api/sdk/externalSpans/#{span_id}#{query}", timeout: 30)
342
+ get("/api/sdk/spans/#{span_id}/raw#{query}", timeout: 30)
342
343
  end
343
344
 
344
345
  def get_trace_span(trace_id, id: nil, name: nil, type: nil, occurrence: "last")
@@ -359,19 +360,19 @@ module Bitfab
359
360
  get("/api/sdk/spans/#{encoded_span_id}", timeout: 30)["span"]
360
361
  end
361
362
 
362
- # Fetch the span tree rooted at an external span. Blocking GET request.
363
+ # Fetch the span tree rooted at a span. Blocking GET request.
363
364
  # Used by replay when a mock strategy is active so child spans can be
364
365
  # matched against their historical outputs.
365
366
  #
366
367
  # Returns a hash shaped { "root" => SpanTreeNode } where each node has
367
- # sourceSpanId, externalSpanId, traceFunctionKey, spanName, type, and
368
+ # sourceSpanId, spanId, traceFunctionKey, spanName, type, and
368
369
  # children. When include_outputs is true each node also carries its recorded
369
370
  # output (and optional outputMeta); when false the server omits the output
370
371
  # payloads so only the spans actually mocked are fetched later (by
371
- # externalSpanId), avoiding dragging down every span's output when only a
372
+ # spanId), avoiding dragging down every span's output when only a
372
373
  # few are mocked.
373
- def get_span_tree(external_span_id, include_outputs: true, include_root_output: true)
374
- endpoint = "/api/sdk/replay/spanTree/#{external_span_id}"
374
+ def get_span_tree(span_id, include_outputs: true, include_root_output: true)
375
+ endpoint = "/api/sdk/replay/spanTree/#{span_id}"
375
376
  query = []
376
377
  query << "includeOutputs=false" unless include_outputs
377
378
  query << "includeRootOutput=false" unless include_root_output
@@ -402,23 +403,23 @@ module Bitfab
402
403
  )
403
404
  end
404
405
 
405
- # Queue an external trace on this client's trace transport (fire-and-forget).
406
- def send_external_trace(payload)
406
+ # Queue a trace payload on this client's trace transport (fire-and-forget).
407
+ def send_trace_payload(payload)
407
408
  submit = ->(record) {
408
409
  ref = carrier_ref(record)
409
410
  if record["completed"] == true && ref
410
411
  @trace_completion.close(ref.trace_id, dropped: record["dropped"] == true) do |count|
411
- submit_external_trace(record.merge("expectedSpanCount" => count))
412
+ submit_trace_payload(record.merge("expectedSpanCount" => count))
412
413
  end
413
414
  else
414
415
  @trace_completion.open(ref.trace_id) if ref
415
- submit_external_trace(record)
416
+ submit_trace_payload(record)
416
417
  end
417
418
  }
418
419
  @simulation_plan ? @simulation_plan.send_trace(payload, &submit) : submit.call(payload)
419
420
  end
420
421
 
421
- def submit_external_trace(payload)
422
+ def submit_trace_payload(payload)
422
423
  trace_transport&.submit(
423
424
  "external_trace",
424
425
  payload.merge("sdkVersion" => VERSION),
@@ -473,7 +474,7 @@ module Bitfab
473
474
  def payload_timestamp(payload, field)
474
475
  raw = payload.dig("rawSpan", field) if payload["rawSpan"].is_a?(Hash)
475
476
  if raw.nil?
476
- raw_trace = payload["externalTrace"] || payload["rawTrace"]
477
+ raw_trace = payload["rawTrace"] || payload["externalTrace"]
477
478
  raw = raw_trace[field] if raw_trace.is_a?(Hash)
478
479
  end
479
480
  return nil unless raw.is_a?(String)
@@ -496,7 +497,7 @@ module Bitfab
496
497
  def carrier_ref(payload)
497
498
  trace_id = payload["sourceTraceId"]
498
499
  unless trace_id.is_a?(String)
499
- raw_trace = payload["externalTrace"] || payload["rawTrace"]
500
+ raw_trace = payload["rawTrace"] || payload["externalTrace"]
500
501
  trace_id = raw_trace["id"] if raw_trace.is_a?(Hash)
501
502
  end
502
503
  return nil unless trace_id.is_a?(String)
data/lib/bitfab/replay.rb CHANGED
@@ -1144,20 +1144,9 @@ module Bitfab
1144
1144
  )
1145
1145
  end
1146
1146
 
1147
- span = http_client.get_external_span(original_span_id, replay_view: true)
1147
+ span = http_client.get_span_raw(original_span_id, replay_view: true)
1148
1148
  item_data = extract_span_data(span)
1149
1149
 
1150
- # Fetch the span tree when the base strategy needs recorded outputs
1151
- # ("all"/"marked") OR when selective mock overrides are present. Overrides
1152
- # gate on the same non-root call-counter machinery the tree drives, so
1153
- # the tree must be fetched for them to fire even under mock: "none".
1154
- #
1155
- # Only mock: "all" needs every span's recorded output inline (it mocks
1156
- # every matched recorded child), so it fetches an eager tree even when
1157
- # overrides exist.
1158
- # Non-"all" runs that need a tree ("marked", or "none" with overrides)
1159
- # fetch it payload-free (includeOutputs=false) and pull each mocked span's
1160
- # output lazily by externalSpanId, never dragging down unused outputs.
1161
1150
  overrides_present = !mock_overrides.nil? && !mock_overrides.empty?
1162
1151
  include_outputs = mock_strategy == "all"
1163
1152
  mock_tree = nil
@@ -1201,7 +1190,7 @@ module Bitfab
1201
1190
  experiment_id,
1202
1191
  span["id"],
1203
1192
  metrics,
1204
- input_source_trace_id: span["externalTraceId"],
1193
+ input_source_trace_id: span["traceId"] || span["externalTraceId"],
1205
1194
  selective_replay:,
1206
1195
  mock_strategy:,
1207
1196
  mock_tree:,
@@ -1294,14 +1283,9 @@ module Bitfab
1294
1283
  counter_key = "#{key}:#{name}"
1295
1284
  index = counters[counter_key] || 0
1296
1285
  counters[counter_key] = index + 1
1297
- # externalSpanId is what the lazy path fetches the recorded output by
1298
- # when the tree came back payload-free (includeOutputs=false). output
1299
- # / outputMeta are copied ONLY when the node actually carries them, so
1300
- # the presence of the :output key distinguishes an inline nil output
1301
- # from a payload-free entry (mirrors TS's undefined-vs-null check).
1302
1286
  entry = {
1303
1287
  source_span_id: node["sourceSpanId"],
1304
- external_span_id: node["externalSpanId"]
1288
+ span_row_id: node["spanId"] || node["externalSpanId"]
1305
1289
  }
1306
1290
  entry[:output] = node["output"] if node.key?("output")
1307
1291
  entry[:output_meta] = node["outputMeta"] if node.key?("outputMeta")
@@ -1316,25 +1300,25 @@ module Bitfab
1316
1300
  end
1317
1301
 
1318
1302
  # Build a memoized lazy fetcher for a single replay item. The returned
1319
- # lambda takes an externalSpanId and returns that span's deserialized
1320
- # recorded output, fetching it via get_external_span at most once per id
1303
+ # lambda takes a span ID and returns that span's deserialized
1304
+ # recorded output, fetching it via get_span_raw at most once per id
1321
1305
  # (a span read twice - as a base "marked" mock and via an override's
1322
1306
  # get_original_output - fetches once). The cache is per item: each item
1323
1307
  # replays synchronously on its own thread, so no lock is needed.
1324
1308
  def build_span_output_fetcher(http_client)
1325
1309
  cache = {}
1326
- lambda do |external_span_id|
1327
- return cache[external_span_id] if cache.key?(external_span_id)
1310
+ lambda do |span_row_id|
1311
+ return cache[span_row_id] if cache.key?(span_row_id)
1328
1312
 
1329
- span = http_client.get_external_span(external_span_id, replay_view: true)
1313
+ span = http_client.get_span_raw(span_row_id, replay_view: true)
1330
1314
  span_data = (span["rawData"] || {})["span_data"] || {}
1331
1315
  meta = span_data["output_serialized"]
1332
1316
  meta = span_data["output_meta"] if meta.nil? || meta == ""
1333
- cache[external_span_id] = Serialize.deserialize_output(span_data["output"], meta)
1317
+ cache[span_row_id] = Serialize.deserialize_output(span_data["output"], meta)
1334
1318
  end
1335
1319
  end
1336
1320
 
1337
- # Extract input/output data from an external span's rawData.
1321
+ # Extract input/output data from a span's rawData.
1338
1322
  def extract_span_data(span)
1339
1323
  raw_data = span["rawData"] || {}
1340
1324
  span_data = raw_data["span_data"] || {}
@@ -198,7 +198,7 @@ module Bitfab
198
198
  end
199
199
 
200
200
  def trace_id(payload)
201
- payload["traceId"] || payload["sourceTraceId"] || payload.dig("externalTrace", "id") || payload["id"]
201
+ payload["traceId"] || payload["sourceTraceId"] || payload.dig("rawTrace", "id") || payload.dig("externalTrace", "id") || payload["id"]
202
202
  end
203
203
 
204
204
  def framework?(payload)
@@ -194,12 +194,27 @@ module Bitfab
194
194
  end
195
195
  end
196
196
 
197
- # Reads one trace's verdicts, or nil when the trace has none.
197
+ # Reads one trace's verdicts, or nil when the trace has none. See {#get_all} for the shape of each
198
+ # "evidence" item.
198
199
  def get(trace_id)
199
200
  get_all([trace_id]).first
200
201
  end
201
202
 
202
- # Reads the verdicts of several traces.
203
+ # Reads the verdicts of several traces. Each trace and each entry of its "assertions" carries
204
+ # "evidence": nil, or an array of hashes, one per span cited, each with "spanId" and "text".
205
+ #
206
+ # Evidence written by Bitfab's assertion judge also carries "field", "ranges", and "spanChanged",
207
+ # which point at the exact passages the judge read. "field" is the span field the judge read
208
+ # ("summary", "input", "output", "reasoning", "content", "metadata", "errors", "contexts", or
209
+ # "llm_usage"). "ranges" are [start, end) character ranges into that field's text as the judge saw
210
+ # it: a string field as is, any other value as JSON indented by two spaces. Indexes are JavaScript
211
+ # string indexes (UTF-16 code units), not Ruby character indexes, so text outside the Basic
212
+ # Multilingual Plane counts two per character. Use them to highlight only when "spanChanged" is false.
213
+ #
214
+ # "spanChanged" is true when the span no longer matches what was judged: "text" is then only a short
215
+ # preview saved with the verdict, and "ranges" must not be used to highlight anything. When it is
216
+ # false, "text" is the full text of "ranges" read from the span, joined by a newline, an ellipsis,
217
+ # and a newline. Unknown keys are passed through as the server sent them.
203
218
  def get_all(trace_ids)
204
219
  return [] if trace_ids.empty?
205
220
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.74.2"
4
+ VERSION = "0.75.0"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -146,9 +146,6 @@ module Bitfab
146
146
  lease = ctx[:db_branch_lease]
147
147
  return unless lease
148
148
 
149
- # Surface the Bitfab trace ID (what the customer sees in the dashboard),
150
- # not the external one. Falling back to the external ID keeps replays from
151
- # external sources working until that path is fully wired.
152
149
  trace_id = ctx[:source_bitfab_trace_id] || ctx[:input_source_trace_id]
153
150
  return unless trace_id
154
151
 
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.74.2
4
+ version: 0.75.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team