bitfab 0.74.1 → 0.74.3
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 +12 -15
- data/lib/bitfab/http_client.rb +19 -18
- data/lib/bitfab/replay.rb +10 -26
- data/lib/bitfab/serialize.rb +135 -27
- data/lib/bitfab/simulation_plan.rb +1 -1
- 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: e04db08d34f7d195036216aa6f41ee940f81af773924508c2a12da97fbbd1e4a
|
|
4
|
+
data.tar.gz: 8fd59297c7eeb7e2d053ca2e4722d5b6aea43ae5c96f691a2addf81f0f9452ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa1099e23b6af4619238c7b6b94f13e9ad15ead7aa6dcaa7fb5a993b634dd00ab6468c5924bcc717da520fa9f0e96d3d5e82a7ab32656e96885626b07e38c3e2
|
|
7
|
+
data.tar.gz: a99c779521142a681697cdadd6b031b1f2f181e851c919ec2f8e571dd3eb447eeccb6063380c5ee8ccfc706e420ef1e2fe23bc0fdd2eb5802e5930838372a075
|
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)
|
|
@@ -1069,8 +1069,9 @@ module Bitfab
|
|
|
1069
1069
|
span_data.merge!(extra_span_data.except(*SimulationPlan::CONTENT_KEYS))
|
|
1070
1070
|
span_data["content_off_by_simulation_plan"] = true
|
|
1071
1071
|
else
|
|
1072
|
-
human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
1072
|
+
human_inputs, input_dropped, (kept_args, kept_kwargs) = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
1073
1073
|
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
1074
|
+
kept_result = Serialize.too_large?(output_dropped) ? human_output : result
|
|
1074
1075
|
dropped = input_dropped + output_dropped
|
|
1075
1076
|
|
|
1076
1077
|
span_data = {
|
|
@@ -1081,10 +1082,10 @@ module Bitfab
|
|
|
1081
1082
|
"function_name" => function_name
|
|
1082
1083
|
}
|
|
1083
1084
|
span_data.merge!(extra_span_data)
|
|
1084
|
-
snapshot = SpanMeta::Snapshot.capture(
|
|
1085
|
+
snapshot = SpanMeta::Snapshot.capture(kept_args, kept_kwargs, kept_result)
|
|
1085
1086
|
unless snapshot
|
|
1086
|
-
input_meta = SpanMeta.build_input_meta(
|
|
1087
|
-
output_meta = SpanMeta.build_output_meta(
|
|
1087
|
+
input_meta = SpanMeta.build_input_meta(kept_args, kept_kwargs, human_inputs)
|
|
1088
|
+
output_meta = SpanMeta.build_output_meta(kept_result, human_output)
|
|
1088
1089
|
span_data["input_meta"] = input_meta if input_meta
|
|
1089
1090
|
span_data["output_meta"] = output_meta if output_meta
|
|
1090
1091
|
end
|
|
@@ -1145,7 +1146,7 @@ module Bitfab
|
|
|
1145
1146
|
}]
|
|
1146
1147
|
end
|
|
1147
1148
|
|
|
1148
|
-
@http_client.
|
|
1149
|
+
@http_client.send_span_payload(payload, declared_node, snapshot)
|
|
1149
1150
|
end
|
|
1150
1151
|
|
|
1151
1152
|
# Advance the per-(key, name) call counter when this invocation is a
|
|
@@ -1179,10 +1180,6 @@ module Bitfab
|
|
|
1179
1180
|
def check_mock_replay(replay_ctx, trace_function_key, span_name, call_index, span_type:, args:, kwargs:, mock_on_replay:)
|
|
1180
1181
|
mock_entry = replay_ctx[:mock_tree]["#{trace_function_key}:#{span_name}:#{call_index}"]
|
|
1181
1182
|
|
|
1182
|
-
# 1) Selective overrides. The matcher runs on structural metadata only;
|
|
1183
|
-
# the recorded output is fetched (lazily, by externalSpanId) only if the
|
|
1184
|
-
# processor actually asks for it via get_original_output, so a pure
|
|
1185
|
-
# synthetic override triggers zero output fetches.
|
|
1186
1183
|
overrides = replay_ctx[:mock_overrides]
|
|
1187
1184
|
if overrides&.any?
|
|
1188
1185
|
node = {
|
|
@@ -1242,7 +1239,7 @@ module Bitfab
|
|
|
1242
1239
|
# Resolve a mock-tree entry's recorded output. Prefers an inline output when
|
|
1243
1240
|
# the tree carried one (eager "all", or an older server that ignores
|
|
1244
1241
|
# includeOutputs and returns outputs); otherwise lazily fetches it by
|
|
1245
|
-
#
|
|
1242
|
+
# span ID through the per-item memoized fetcher on the replay
|
|
1246
1243
|
# context, so only the spans actually mocked pull their output.
|
|
1247
1244
|
#
|
|
1248
1245
|
# Deserialization is type-preserving when Ruby-side Marshal+Base64 metadata
|
|
@@ -1257,9 +1254,9 @@ module Bitfab
|
|
|
1257
1254
|
end
|
|
1258
1255
|
|
|
1259
1256
|
fetcher = replay_ctx[:fetch_span_output]
|
|
1260
|
-
|
|
1261
|
-
if fetcher &&
|
|
1262
|
-
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)
|
|
1263
1260
|
end
|
|
1264
1261
|
|
|
1265
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"] || {}
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -20,6 +20,26 @@ module Bitfab
|
|
|
20
20
|
# Recursion guard for cyclic graphs and pathologically nested structures.
|
|
21
21
|
MAX_SERIALIZE_DEPTH = 16
|
|
22
22
|
|
|
23
|
+
# A running count of bytes the JSON encoding of the walk's output is known
|
|
24
|
+
# to need (`used`), and of every byte ever counted, including counts later
|
|
25
|
+
# taken back when a part that raised was stubbed (`charged`). It is also
|
|
26
|
+
# the catch tag the walk throws to once `used` passes MAX_SERIALIZED_BYTES.
|
|
27
|
+
Budget = Struct.new(:used, :charged)
|
|
28
|
+
|
|
29
|
+
# Once this many bytes have been counted in total, counting stops and the
|
|
30
|
+
# walk finishes uncounted, so parts that raise and are stubbed over and
|
|
31
|
+
# over cannot make the walk do unbounded work before the size check.
|
|
32
|
+
MAX_COUNTED_BYTES = 4 * MAX_SERIALIZED_BYTES
|
|
33
|
+
|
|
34
|
+
# String encodings whose bytes JSON writes out one for one (or rejects). In
|
|
35
|
+
# any other encoding a character can take more bytes than its UTF-8 form,
|
|
36
|
+
# so only the character count is a safe floor there.
|
|
37
|
+
BYTE_FOR_BYTE_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::BINARY].freeze
|
|
38
|
+
|
|
39
|
+
# The dropped entry for a value replaced by a placeholder because it is
|
|
40
|
+
# past MAX_SERIALIZED_BYTES.
|
|
41
|
+
TOO_LARGE = "too_large"
|
|
42
|
+
|
|
23
43
|
module_function
|
|
24
44
|
|
|
25
45
|
# Serialize a value for JSON storage (human-readable).
|
|
@@ -43,22 +63,43 @@ module Bitfab
|
|
|
43
63
|
# non-empty dropped means the capture is lossy, so the send boundary marks
|
|
44
64
|
# the span serialization_degraded (non-replayable) instead of shipping it as
|
|
45
65
|
# if it round-trips. Mirrors the Python/TS SDKs' report serializers.
|
|
66
|
+
#
|
|
67
|
+
# The walk counts a lower bound on the JSON size of what it has built so
|
|
68
|
+
# far and gives up as soon as that passes MAX_SERIALIZED_BYTES, so a value
|
|
69
|
+
# holding huge arrays or strings is stubbed without first being copied and
|
|
70
|
+
# encoded in full. The count never exceeds the real size, so a value that
|
|
71
|
+
# fits is never stubbed; the JSON.dump check afterwards still catches values
|
|
72
|
+
# whose real size passes the cap while the count does not.
|
|
46
73
|
def serialize_value_with_report(value)
|
|
47
74
|
dropped = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
budget = Budget.new(1, 1)
|
|
76
|
+
result = catch(budget) { serialize_value_inner(value, 0, dropped, budget) }
|
|
77
|
+
reason = if budget.used > MAX_SERIALIZED_BYTES
|
|
78
|
+
"too_large_over_#{MAX_SERIALIZED_BYTES}_bytes"
|
|
79
|
+
elsif oversized?(result)
|
|
80
|
+
"too_large"
|
|
52
81
|
end
|
|
53
|
-
[result, dropped]
|
|
82
|
+
return [result, dropped] unless reason
|
|
83
|
+
|
|
84
|
+
dropped << TOO_LARGE
|
|
85
|
+
[unserializable_stub(value, reason), dropped]
|
|
54
86
|
rescue StandardError, SystemStackError
|
|
55
87
|
[unserializable_stub(value, "unexpected_error"), [class_name(value)]]
|
|
56
88
|
end
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
# `budget`, when given, counts the bytes this call's output needs beyond the
|
|
91
|
+
# one byte its caller already counted for it (every JSON value takes at
|
|
92
|
+
# least one), and throws to the budget once the total passes the cap.
|
|
93
|
+
# A hash entry costs its quoted key, a colon, a comma or brace, and the
|
|
94
|
+
# value's byte. An array or set element costs its byte plus a comma or
|
|
95
|
+
# bracket; a plain Array or Set is charged for every element before any is
|
|
96
|
+
# copied, so a huge one is turned away at once. When a part raises, its
|
|
97
|
+
# stub replaces whatever that part had counted.
|
|
98
|
+
def serialize_value_inner(value, depth, dropped = [], budget = nil)
|
|
99
|
+
mark = budget&.used
|
|
59
100
|
if depth > MAX_SERIALIZE_DEPTH
|
|
60
101
|
dropped << "max_depth"
|
|
61
|
-
return "<unserializable: max_depth>"
|
|
102
|
+
return counted_string(budget, "<unserializable: max_depth>")
|
|
62
103
|
end
|
|
63
104
|
|
|
64
105
|
case value
|
|
@@ -70,47 +111,95 @@ module Bitfab
|
|
|
70
111
|
value
|
|
71
112
|
else
|
|
72
113
|
dropped << "non_finite_float"
|
|
73
|
-
"<unserializable: non_finite_float>"
|
|
114
|
+
counted_string(budget, "<unserializable: non_finite_float>")
|
|
74
115
|
end
|
|
75
|
-
when nil, true, false, Integer
|
|
116
|
+
when nil, true, false, Integer
|
|
76
117
|
value
|
|
118
|
+
when String
|
|
119
|
+
counted_string(budget, value)
|
|
77
120
|
when Hash
|
|
78
|
-
value
|
|
79
|
-
|
|
121
|
+
serialize_hash_inner(value, depth, dropped, budget)
|
|
122
|
+
when Array, Set
|
|
123
|
+
plain = value.instance_of?(Array) || value.instance_of?(Set)
|
|
124
|
+
charge(budget, 2 * value.size) if plain
|
|
125
|
+
value.map do |v|
|
|
126
|
+
charge(budget, 2) unless plain
|
|
127
|
+
serialize_value_inner(v, depth + 1, dropped, budget)
|
|
80
128
|
end
|
|
81
|
-
when Array
|
|
82
|
-
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
83
|
-
when Set
|
|
84
|
-
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
85
129
|
when Time, DateTime
|
|
86
|
-
safely_call(value, "iso8601", 3) { safe_to_s(value) }
|
|
130
|
+
counted_string(budget, safely_call(value, "iso8601", 3) { safe_to_s(value) })
|
|
87
131
|
when Date
|
|
88
|
-
safe_to_s(value)
|
|
132
|
+
counted_string(budget, safe_to_s(value))
|
|
89
133
|
when Symbol
|
|
90
|
-
value.to_s
|
|
134
|
+
counted_string(budget, value.to_s)
|
|
91
135
|
else
|
|
92
136
|
if value.respond_to?(:to_h)
|
|
93
137
|
h = safely_call(value, "to_h") do
|
|
94
138
|
dropped << class_name(value)
|
|
95
|
-
return unserializable_stub(value, "to_h_raised")
|
|
139
|
+
return counted_string(budget, unserializable_stub(value, "to_h_raised"))
|
|
96
140
|
end
|
|
97
|
-
serialize_value_inner(h, depth + 1, dropped)
|
|
141
|
+
serialize_value_inner(h, depth + 1, dropped, budget)
|
|
98
142
|
elsif value.respond_to?(:to_a)
|
|
99
143
|
a = safely_call(value, "to_a") do
|
|
100
144
|
dropped << class_name(value)
|
|
101
|
-
return unserializable_stub(value, "to_a_raised")
|
|
145
|
+
return counted_string(budget, unserializable_stub(value, "to_a_raised"))
|
|
102
146
|
end
|
|
103
|
-
serialize_value_inner(a, depth + 1, dropped)
|
|
147
|
+
serialize_value_inner(a, depth + 1, dropped, budget)
|
|
104
148
|
else
|
|
105
149
|
# An arbitrary object with no structured conversion: stringifying it is
|
|
106
150
|
# a lossy capture (a repr, not its data), so report it as dropped.
|
|
107
151
|
dropped << class_name(value)
|
|
108
|
-
safe_to_s(value)
|
|
152
|
+
counted_string(budget, safe_to_s(value))
|
|
109
153
|
end
|
|
110
154
|
end
|
|
111
155
|
rescue StandardError, SystemStackError
|
|
112
156
|
dropped << class_name(value)
|
|
113
|
-
|
|
157
|
+
budget.used = mark if budget
|
|
158
|
+
counted_string(budget, unserializable_stub(value, "inner_error"))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Serialize a hash's entries into a plain Hash keyed by each key's string.
|
|
162
|
+
# Keys that stringify alike (such as "data" and :data) share one output
|
|
163
|
+
# key holding the last of their values, so each output key is counted
|
|
164
|
+
# once, only the value that is kept is walked, and the key keeps the place
|
|
165
|
+
# of its first occurrence.
|
|
166
|
+
def serialize_hash_inner(hash, depth, dropped, budget)
|
|
167
|
+
entries = []
|
|
168
|
+
last_index = {}
|
|
169
|
+
hash.each do |k, v|
|
|
170
|
+
key = safe_to_s(k)
|
|
171
|
+
charge(budget, string_floor(key) + 5) unless last_index.key?(key)
|
|
172
|
+
last_index[key] = entries.size
|
|
173
|
+
entries << [key, v]
|
|
174
|
+
end
|
|
175
|
+
entries.each_with_index.with_object({}) do |((key, v), index), acc|
|
|
176
|
+
acc[key] = (last_index[key] == index) ? serialize_value_inner(v, depth + 1, dropped, budget) : nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Add `bytes` to the running count and stop the walk once it passes the cap.
|
|
181
|
+
def charge(budget, bytes)
|
|
182
|
+
return unless budget && budget.charged <= MAX_COUNTED_BYTES
|
|
183
|
+
|
|
184
|
+
budget.used += bytes
|
|
185
|
+
budget.charged += bytes
|
|
186
|
+
throw budget if budget.used > MAX_SERIALIZED_BYTES
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Charge for a string the walk emits (its quotes beyond the byte the caller
|
|
190
|
+
# counted) and return it.
|
|
191
|
+
def counted_string(budget, str)
|
|
192
|
+
charge(budget, string_floor(str) + 1)
|
|
193
|
+
str
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The fewest bytes JSON can write for the characters of `str`, quotes
|
|
197
|
+
# aside. A String subclass may encode itself through its own to_json, so it
|
|
198
|
+
# counts as nothing.
|
|
199
|
+
def string_floor(str)
|
|
200
|
+
return 0 unless str.instance_of?(String)
|
|
201
|
+
|
|
202
|
+
BYTE_FOR_BYTE_ENCODINGS.include?(str.encoding) ? str.bytesize : str.length
|
|
114
203
|
end
|
|
115
204
|
|
|
116
205
|
# Call `value.<method>` and return the result, or yield to the fallback
|
|
@@ -153,23 +242,42 @@ module Bitfab
|
|
|
153
242
|
end
|
|
154
243
|
|
|
155
244
|
# Like serialize_inputs, but also returns the accumulated dropped list so the
|
|
156
|
-
# send boundary can mark a lossy capture non-replayable.
|
|
245
|
+
# send boundary can mark a lossy capture non-replayable. The third element
|
|
246
|
+
# is [args, kwargs] with every value past MAX_SERIALIZED_BYTES swapped for
|
|
247
|
+
# its placeholder (the originals when none is), so the type-preserving
|
|
248
|
+
# capture never copies a value the readable capture already gave up on.
|
|
157
249
|
def serialize_inputs_with_report(args, kwargs = {})
|
|
158
250
|
dropped = []
|
|
159
|
-
|
|
251
|
+
kept_args = args
|
|
252
|
+
serialized = args.each_with_index.map do |arg, index|
|
|
160
253
|
result, arg_dropped = serialize_value_with_report(arg)
|
|
161
254
|
dropped.concat(arg_dropped)
|
|
255
|
+
if too_large?(arg_dropped)
|
|
256
|
+
kept_args = kept_args.dup if kept_args.equal?(args)
|
|
257
|
+
kept_args[index] = result
|
|
258
|
+
end
|
|
162
259
|
result
|
|
163
260
|
end
|
|
261
|
+
kept_kwargs = kwargs
|
|
164
262
|
unless kwargs.empty?
|
|
165
263
|
kw = kwargs.each_with_object({}) do |(k, v), acc|
|
|
166
264
|
result, v_dropped = serialize_value_with_report(v)
|
|
167
265
|
dropped.concat(v_dropped)
|
|
266
|
+
if too_large?(v_dropped)
|
|
267
|
+
kept_kwargs = kept_kwargs.dup if kept_kwargs.equal?(kwargs)
|
|
268
|
+
kept_kwargs[k] = result
|
|
269
|
+
end
|
|
168
270
|
acc[safe_to_s(k)] = result
|
|
169
271
|
end
|
|
170
272
|
serialized << kw
|
|
171
273
|
end
|
|
172
|
-
[serialized, dropped]
|
|
274
|
+
[serialized, dropped, [kept_args, kept_kwargs]]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Whether a dropped list from serialize_value_with_report means the value
|
|
278
|
+
# itself was replaced by a placeholder for being past MAX_SERIALIZED_BYTES.
|
|
279
|
+
def too_large?(dropped)
|
|
280
|
+
dropped.include?(TOO_LARGE)
|
|
173
281
|
end
|
|
174
282
|
|
|
175
283
|
# Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction.
|
|
@@ -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/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
|
|