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 +4 -4
- data/lib/bitfab/client.rb +7 -11
- data/lib/bitfab/http_client.rb +19 -18
- data/lib/bitfab/replay.rb +10 -26
- data/lib/bitfab/simulation_plan.rb +1 -1
- data/lib/bitfab/verdicts.rb +17 -2
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +0 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6976d07abea8e8c456185b0f5a76bbe861148e2a10baa1c3a65ac1a694b6993d
|
|
4
|
+
data.tar.gz: 072af5081f4c15e748478dbd058a8cc4c249804b345dcc70ed47b3e47f3c5830
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
1262
|
-
if fetcher &&
|
|
1263
|
-
return fetcher.call(
|
|
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
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -192,8 +192,8 @@ module Bitfab
|
|
|
192
192
|
raise last_error
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
# Queue
|
|
196
|
-
def
|
|
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
|
|
338
|
-
# only the input/output serialization fields used
|
|
339
|
-
|
|
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/
|
|
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
|
|
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,
|
|
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
|
-
#
|
|
372
|
+
# spanId), avoiding dragging down every span's output when only a
|
|
372
373
|
# few are mocked.
|
|
373
|
-
def get_span_tree(
|
|
374
|
-
endpoint = "/api/sdk/replay/spanTree/#{
|
|
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
|
|
406
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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["
|
|
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["
|
|
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.
|
|
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
|
-
|
|
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
|
|
1320
|
-
# recorded output, fetching it via
|
|
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 |
|
|
1327
|
-
return cache[
|
|
1310
|
+
lambda do |span_row_id|
|
|
1311
|
+
return cache[span_row_id] if cache.key?(span_row_id)
|
|
1328
1312
|
|
|
1329
|
-
span = http_client.
|
|
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[
|
|
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
|
|
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)
|
data/lib/bitfab/verdicts.rb
CHANGED
|
@@ -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
|
|
data/lib/bitfab/version.rb
CHANGED
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
|
|