bitfab 0.36.9 → 0.36.11
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/README.md +2 -2
- data/lib/bitfab/client.rb +70 -49
- data/lib/bitfab/mock_override.rb +4 -4
- data/lib/bitfab/replay.rb +51 -33
- data/lib/bitfab/span_context.rb +22 -1
- data/lib/bitfab/version.rb +1 -1
- 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: 0d23fea40d370a085bd028fbf7b605a6b66bc0732a1580683b3f027b5b2082e9
|
|
4
|
+
data.tar.gz: a477ee5856fc6fb25a39a6088f820337cd2bcb327c3b028c9051d0b0f84edd5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb155e9ec03187b1f9f80a918b8df651bab41e181afcdd5c068c9645ab6b9c94884d7f8c41cab43bd50e878226e3d075962c02ac27e2b33ed2729e979b4e4464
|
|
7
|
+
data.tar.gz: 1da28622e46e44b40f5296ed699ee13e897e7bd0a54177e32f3c629fb8004ea5f87a86546a37d608541ff771ba648fffb128bd613d7aefea300c8898038c7e47
|
data/README.md
CHANGED
|
@@ -301,7 +301,7 @@ client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "marked")
|
|
|
301
301
|
# "none": everything runs real code
|
|
302
302
|
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "none")
|
|
303
303
|
|
|
304
|
-
# "all": every child
|
|
304
|
+
# "all": every matched recorded child returns history; missing occurrences fail closed
|
|
305
305
|
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "all")
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -331,7 +331,7 @@ class Pipeline
|
|
|
331
331
|
end
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
-
Use the default `mock: "marked"` behavior when you want to iterate on `process`'s logic without paying for the LLM call each run. Use `mock: "all"` for the cheapest possible replay (every child span returns its recorded output).
|
|
334
|
+
Use the default `mock: "marked"` behavior when you want to iterate on `process`'s logic without paying for the LLM call each run. Use `mock: "all"` for the cheapest possible replay (every matched recorded child span returns its recorded output; a missing occurrence fails the item closed).
|
|
335
335
|
|
|
336
336
|
### Error Handling
|
|
337
337
|
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Bitfab
|
|
|
16
16
|
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
|
|
17
17
|
|
|
18
18
|
# Sentinel returned by check_mock_replay when this span should run real
|
|
19
|
-
# code (no mock active
|
|
19
|
+
# code (no mock active or the base strategy did not select it).
|
|
20
20
|
# Using a sentinel rather than nil/false avoids confusing legitimate mocked
|
|
21
21
|
# outputs (which may themselves be nil or false).
|
|
22
22
|
MOCK_REPLAY_MISS = Object.new.freeze
|
|
@@ -87,9 +87,10 @@ module Bitfab
|
|
|
87
87
|
# @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
|
|
88
88
|
# @param method_name [Symbol] the method to replay
|
|
89
89
|
# @param trace_function_key [String] the trace function key for this method
|
|
90
|
-
# @param limit [Integer, nil] maximum number of traces to replay (default: 5
|
|
91
|
-
#
|
|
92
|
-
# determines how many traces replay
|
|
90
|
+
# @param limit [Integer, nil] maximum number of traces to replay (default: 5;
|
|
91
|
+
# maximum: 5,000). Ignored when trace_ids or dataset_id is passed because
|
|
92
|
+
# either source already determines how many traces replay. Supplying
|
|
93
|
+
# trace_ids also emits a warning.
|
|
93
94
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
94
95
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
95
96
|
# @param code_change_description [String, nil] optional rationale for the
|
|
@@ -111,7 +112,9 @@ module Bitfab
|
|
|
111
112
|
# same org and trace function or the server rejects the replay
|
|
112
113
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
113
114
|
# "none", or "all". "marked" only mocks spans declared with
|
|
114
|
-
# mock_on_replay: true; "all" mocks every child span.
|
|
115
|
+
# mock_on_replay: true; "all" mocks every matched recorded child span. A selected
|
|
116
|
+
# occurrence that is unavailable errors the item without executing the
|
|
117
|
+
# real child.
|
|
115
118
|
# @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
|
|
116
119
|
# onto the method's current signature when its shape changed after the
|
|
117
120
|
# traces were captured. Receives (args, kwargs, ctx) where ctx is
|
|
@@ -122,16 +125,17 @@ module Bitfab
|
|
|
122
125
|
# match.call(node) selects spans to substitute (node is
|
|
123
126
|
# { trace_function_key:, span_name:, type:, original_span_id: }). value is
|
|
124
127
|
# EITHER a flat value injected directly OR a callable invoked with
|
|
125
|
-
# { node:, inputs:, get_original_output: }; its result becomes
|
|
126
|
-
# output (full replacement), so downstream real code runs against it. The
|
|
128
|
+
# { node:, inputs:, kwargs:, get_original_output: }; its result becomes
|
|
129
|
+
# the span's output (full replacement), so downstream real code runs against it. The
|
|
127
130
|
# first matching override wins. Per-call overrides take precedence over
|
|
128
131
|
# those registered via register_mock_override, and both take precedence
|
|
129
132
|
# over the base mock strategy. The root span is never overridden.
|
|
130
|
-
# @param db_branch [Hash, nil]
|
|
131
|
-
#
|
|
132
|
-
# +:min_cu+/+:max_cu+ size the branch compute
|
|
133
|
-
# +:warmup_sql+ warms its cache. Read the
|
|
134
|
-
# replayed method with
|
|
133
|
+
# @param db_branch [Boolean, Hash, nil] +true+ or a Hash enables database
|
|
134
|
+
# branching; +false+/+nil+ leave it off. An empty Hash uses the mirror
|
|
135
|
+
# project's own sizing. Keys +:min_cu+/+:max_cu+ size the branch compute
|
|
136
|
+
# in Neon Compute Units and +:warmup_sql+ warms its cache. Read the
|
|
137
|
+
# resolved branch inside the replayed method with
|
|
138
|
+
# +Bitfab.current_replay_branch+.
|
|
135
139
|
# @param on_item_finish [#call, nil] optional callback invoked exactly once
|
|
136
140
|
# per item as it finishes, always with a running-totals hash containing
|
|
137
141
|
# { completed:, total:, succeeded:, errored:, item: }. It never receives a
|
|
@@ -281,7 +285,7 @@ module Bitfab
|
|
|
281
285
|
)
|
|
282
286
|
resolved_capture_when = "always"
|
|
283
287
|
end
|
|
284
|
-
return yield if resolved_capture_when == "nested" && SpanContext.current.nil?
|
|
288
|
+
return yield if resolved_capture_when == "nested" && SpanContext.current.nil? && ReplayContext.current_span.nil?
|
|
285
289
|
|
|
286
290
|
# Span setup runs before the user's block. Tracing is a side-channel, so
|
|
287
291
|
# if anything here raises (id generation, trace-state bookkeeping, a
|
|
@@ -297,8 +301,8 @@ module Bitfab
|
|
|
297
301
|
resolved_test_run_id = nil
|
|
298
302
|
resolved_input_source_span_id = nil
|
|
299
303
|
begin
|
|
300
|
-
parent = SpanContext.current
|
|
301
304
|
replay_ctx = ReplayContext.current
|
|
305
|
+
parent = SpanContext.current || ReplayContext.current_span
|
|
302
306
|
trace_id = parent ? parent[:trace_id] : (replay_ctx&.dig(:trace_id) || SecureRandom.uuid)
|
|
303
307
|
span_id = SecureRandom.uuid
|
|
304
308
|
parent_span_id = parent&.dig(:span_id)
|
|
@@ -325,11 +329,12 @@ module Bitfab
|
|
|
325
329
|
# counters: they cannot shift each other.
|
|
326
330
|
call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
327
331
|
if call_index
|
|
328
|
-
|
|
332
|
+
mock_result = check_mock_replay(
|
|
329
333
|
replay_ctx, trace_function_key, span_name, call_index,
|
|
330
334
|
span_type:, args:, kwargs:, mock_on_replay:
|
|
331
335
|
)
|
|
332
|
-
if
|
|
336
|
+
if mock_result != MOCK_REPLAY_MISS
|
|
337
|
+
mocked_output, mock_source = mock_result
|
|
333
338
|
send_mocked_span(
|
|
334
339
|
trace_function_key:,
|
|
335
340
|
trace_id:,
|
|
@@ -343,7 +348,8 @@ module Bitfab
|
|
|
343
348
|
mocked_output:,
|
|
344
349
|
started_at:,
|
|
345
350
|
test_run_id: resolved_test_run_id,
|
|
346
|
-
input_source_span_id: resolved_input_source_span_id
|
|
351
|
+
input_source_span_id: resolved_input_source_span_id,
|
|
352
|
+
mock_source:
|
|
347
353
|
)
|
|
348
354
|
return mocked_output
|
|
349
355
|
end
|
|
@@ -438,12 +444,14 @@ module Bitfab
|
|
|
438
444
|
end
|
|
439
445
|
|
|
440
446
|
begin
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
+
ReplayContext.with_span(trace_id:, span_id:) do
|
|
448
|
+
SpanContext.with_span(trace_id:, span_id:) do
|
|
449
|
+
result = yield
|
|
450
|
+
ensure
|
|
451
|
+
# Capture contexts before the span context is popped
|
|
452
|
+
span_contexts = SpanContext.current&.dig(:contexts)
|
|
453
|
+
span_prompt = SpanContext.current&.dig(:prompt)
|
|
454
|
+
end
|
|
447
455
|
end
|
|
448
456
|
rescue => e
|
|
449
457
|
error = e.message
|
|
@@ -464,12 +472,6 @@ module Bitfab
|
|
|
464
472
|
# collects yielded values as the span output, and finalizes the span
|
|
465
473
|
# once iteration completes (or errors).
|
|
466
474
|
#
|
|
467
|
-
# Limitation: when the source enumerator itself runs its body in a
|
|
468
|
-
# separate fiber (e.g. `Enumerator.new { |y| ... }` or `enum_for(...)`
|
|
469
|
-
# without a block), nested `bitfab_span` calls inside that body fiber
|
|
470
|
-
# still see an empty stack because `Thread.current[STACK_KEY]` is
|
|
471
|
-
# fiber-local. Lazy chains over collections (`.lazy.map`) and ordinary
|
|
472
|
-
# `each` callbacks DO run in the iterating fiber and nest correctly.
|
|
473
475
|
if result.is_a?(Enumerator)
|
|
474
476
|
return wrap_enumerator(result, trace_id:, span_id:, finalize:)
|
|
475
477
|
end
|
|
@@ -542,19 +544,24 @@ module Bitfab
|
|
|
542
544
|
Enumerator.new do |yielder|
|
|
543
545
|
# Push our span onto this fiber's stack so anything that runs in this
|
|
544
546
|
# fiber (including the user's lazy block and `each` callbacks) sees
|
|
545
|
-
# the right parent.
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
547
|
+
# the right parent. The fiber bridge carries the same parent into
|
|
548
|
+
# Enumerator.new / enum_for source fibers on this thread.
|
|
549
|
+
ReplayContext.with_span(trace_id:, span_id:) do
|
|
550
|
+
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
551
|
+
SpanContext.stack.push(span_entry)
|
|
552
|
+
begin
|
|
553
|
+
source.each do |value|
|
|
554
|
+
yielded << value
|
|
555
|
+
yielder << value
|
|
556
|
+
end
|
|
557
|
+
finalize.call(yielded, nil)
|
|
558
|
+
rescue => e
|
|
559
|
+
finalize.call(yielded, e.message)
|
|
560
|
+
raise
|
|
561
|
+
ensure
|
|
562
|
+
SpanContext.stack.pop
|
|
563
|
+
end
|
|
551
564
|
end
|
|
552
|
-
finalize.call(yielded, nil)
|
|
553
|
-
rescue => e
|
|
554
|
-
finalize.call(yielded, e.message)
|
|
555
|
-
raise
|
|
556
|
-
ensure
|
|
557
|
-
SpanContext.stack.pop
|
|
558
565
|
end
|
|
559
566
|
end
|
|
560
567
|
end
|
|
@@ -641,7 +648,8 @@ module Bitfab
|
|
|
641
648
|
|
|
642
649
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
643
650
|
span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
|
|
644
|
-
started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false
|
|
651
|
+
started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false,
|
|
652
|
+
mock_target: nil, mock_source: nil)
|
|
645
653
|
# If drop() was called on this trace, suppress the span PAYLOAD upload for
|
|
646
654
|
# every span that completes after the flag was set. The trace completion
|
|
647
655
|
# still rides out with dropped: true, so the server scrubs any sibling
|
|
@@ -701,6 +709,10 @@ module Bitfab
|
|
|
701
709
|
# replay so the trace view can mark them (matches the Python and
|
|
702
710
|
# TypeScript SDKs).
|
|
703
711
|
payload["mocked"] = true if mocked
|
|
712
|
+
# How the mock was produced, when known: what it replaced, and whether the
|
|
713
|
+
# value was the original trace's own or one the caller supplied.
|
|
714
|
+
payload["mockTarget"] = mock_target if mock_target
|
|
715
|
+
payload["mockSource"] = mock_source if mock_source
|
|
704
716
|
|
|
705
717
|
# A value that could only be captured as a placeholder makes the span
|
|
706
718
|
# non-replayable; record it on the payload's errors (matching the Python
|
|
@@ -769,7 +781,7 @@ module Bitfab
|
|
|
769
781
|
# override always injects, execute_span short-circuits even when this
|
|
770
782
|
# is nil (nil != MOCK_REPLAY_MISS). To inject a proc as the literal
|
|
771
783
|
# output, wrap it in a callable value.
|
|
772
|
-
return value unless value.respond_to?(:call)
|
|
784
|
+
return [value, "override"] unless value.respond_to?(:call)
|
|
773
785
|
|
|
774
786
|
get_original_output = lambda do
|
|
775
787
|
unless mock_entry
|
|
@@ -777,7 +789,7 @@ module Bitfab
|
|
|
777
789
|
end
|
|
778
790
|
resolve_recorded_output(mock_entry, replay_ctx)
|
|
779
791
|
end
|
|
780
|
-
return value.call({node:, inputs: args, kwargs:, get_original_output:})
|
|
792
|
+
return [value.call({node:, inputs: args, kwargs:, get_original_output:}), "override"]
|
|
781
793
|
end
|
|
782
794
|
end
|
|
783
795
|
|
|
@@ -792,9 +804,13 @@ module Bitfab
|
|
|
792
804
|
return MOCK_REPLAY_MISS
|
|
793
805
|
end
|
|
794
806
|
|
|
795
|
-
|
|
807
|
+
unless mock_entry
|
|
808
|
+
raise StandardError,
|
|
809
|
+
"Replay selected span '#{trace_function_key}:#{span_name}' for mocking, " \
|
|
810
|
+
"but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
|
|
811
|
+
end
|
|
796
812
|
|
|
797
|
-
resolve_recorded_output(mock_entry, replay_ctx)
|
|
813
|
+
[resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
|
|
798
814
|
end
|
|
799
815
|
|
|
800
816
|
# Resolve a mock-tree entry's recorded output. Prefers an inline output when
|
|
@@ -833,7 +849,7 @@ module Bitfab
|
|
|
833
849
|
# output as the result and no error.
|
|
834
850
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
835
851
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
836
|
-
started_at:, test_run_id:, input_source_span_id:)
|
|
852
|
+
started_at:, test_run_id:, input_source_span_id:, mock_source:)
|
|
837
853
|
ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
838
854
|
send_span(
|
|
839
855
|
trace_function_key:,
|
|
@@ -853,7 +869,11 @@ module Bitfab
|
|
|
853
869
|
ended_at:,
|
|
854
870
|
test_run_id:,
|
|
855
871
|
input_source_span_id:,
|
|
856
|
-
mocked: true
|
|
872
|
+
mocked: true,
|
|
873
|
+
# Both paths short-circuit the call, so the target is always the
|
|
874
|
+
# output; only where the value came from differs.
|
|
875
|
+
mock_target: "output",
|
|
876
|
+
mock_source:
|
|
857
877
|
)
|
|
858
878
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
859
879
|
# Never crash the host app: mocked span recording is best-effort
|
|
@@ -891,7 +911,8 @@ module Bitfab
|
|
|
891
911
|
# @param capture_when [String, Symbol] "always" to allow a root trace, or
|
|
892
912
|
# "nested" to capture only when another Bitfab span is active. Unknown
|
|
893
913
|
# values warn once and default to "always"
|
|
894
|
-
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
914
|
+
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
915
|
+
# mock strategy. A missing selected occurrence fails closed.
|
|
895
916
|
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
|
|
896
917
|
Bitfab::Traceable.wrap(
|
|
897
918
|
klass, method_name,
|
data/lib/bitfab/mock_override.rb
CHANGED
|
@@ -31,10 +31,10 @@ module Bitfab
|
|
|
31
31
|
# nil is a legitimate injected value. To inject a proc as the literal
|
|
32
32
|
# output, wrap it in a callable value: value: ->(ctx) { the_proc }.
|
|
33
33
|
#
|
|
34
|
-
# Unlike the TypeScript SDK,
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
34
|
+
# Unlike the TypeScript SDK, get_original_output is synchronous. Under
|
|
35
|
+
# non-"all" replay the first access may fetch the recorded output lazily;
|
|
36
|
+
# that fetch is memoized for the replay item. Under mock: "all" the output is
|
|
37
|
+
# inline even when overrides are present. Ruby has no async-span limitation.
|
|
38
38
|
module MockOverride
|
|
39
39
|
module_function
|
|
40
40
|
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -41,28 +41,47 @@ module Bitfab
|
|
|
41
41
|
# - "marked" : only spans declared with mock_on_replay: true return historical
|
|
42
42
|
# output; everything else runs real code (default)
|
|
43
43
|
# - "none" : every child span runs real code
|
|
44
|
-
# - "all" : every child span returns its historical
|
|
44
|
+
# - "all" : every matched recorded child span returns its historical
|
|
45
|
+
# output; a missing occurrence fails the item closed
|
|
45
46
|
MOCK_STRATEGIES = %w[none all marked].freeze
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
+
# Replay context shared by fibers on the current thread. Ruby's
|
|
49
|
+
# Thread#[] storage is fiber-local, so use true thread variables here: lazy
|
|
50
|
+
# Enumerator bodies run in another fiber but must retain replay mocking.
|
|
48
51
|
module ReplayContext
|
|
49
52
|
module_function
|
|
50
53
|
|
|
51
54
|
def current
|
|
52
|
-
Thread.current
|
|
55
|
+
Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
def current_span
|
|
59
|
+
current&.dig(:span_stack)&.last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_span(trace_id:, span_id:)
|
|
63
|
+
ctx = current
|
|
64
|
+
return yield unless ctx
|
|
65
|
+
|
|
66
|
+
entry = {trace_id:, span_id:}
|
|
67
|
+
ctx[:span_stack].push(entry)
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
ctx[:span_stack].pop if ctx
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Execute a block with replay context set for every fiber on the current
|
|
74
|
+
# thread. Child threads and processes intentionally do not inherit it.
|
|
57
75
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
58
76
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
59
77
|
db_branch_lease: nil, source_bitfab_trace_id: nil)
|
|
60
|
-
previous = Thread.current
|
|
78
|
+
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
61
79
|
ctx = {
|
|
62
80
|
test_run_id:,
|
|
63
81
|
input_source_span_id:,
|
|
64
82
|
input_source_trace_id:,
|
|
65
|
-
trace_id
|
|
83
|
+
trace_id:,
|
|
84
|
+
span_stack: []
|
|
66
85
|
}
|
|
67
86
|
if mock_tree
|
|
68
87
|
ctx[:mock_tree] = mock_tree
|
|
@@ -73,8 +92,9 @@ module Bitfab
|
|
|
73
92
|
# is present. process_single_item fetches the tree whenever overrides
|
|
74
93
|
# exist, even under mock: "none".
|
|
75
94
|
ctx[:mock_overrides] = mock_overrides if mock_overrides && !mock_overrides.empty?
|
|
76
|
-
# Lazy per-span output fetcher for
|
|
77
|
-
#
|
|
95
|
+
# Lazy per-span output fetcher for a payload-free non-"all" tree path.
|
|
96
|
+
# Absent on the eager "all" path, whose outputs are inline even when
|
|
97
|
+
# overrides are present.
|
|
78
98
|
ctx[:fetch_span_output] = fetch_span_output if fetch_span_output
|
|
79
99
|
end
|
|
80
100
|
# The per-trace DB branch (resolved server-side) and the Bitfab trace ID
|
|
@@ -94,10 +114,10 @@ module Bitfab
|
|
|
94
114
|
# used" from "branch was offered".
|
|
95
115
|
ctx[:db_branch_lease] = db_branch_lease if db_branch_lease
|
|
96
116
|
ctx[:source_bitfab_trace_id] = source_bitfab_trace_id if source_bitfab_trace_id
|
|
97
|
-
Thread.current
|
|
117
|
+
Thread.current.thread_variable_set(REPLAY_CONTEXT_KEY, ctx)
|
|
98
118
|
yield
|
|
99
119
|
ensure
|
|
100
|
-
Thread.current
|
|
120
|
+
Thread.current.thread_variable_set(REPLAY_CONTEXT_KEY, previous)
|
|
101
121
|
end
|
|
102
122
|
end
|
|
103
123
|
|
|
@@ -163,8 +183,9 @@ module Bitfab
|
|
|
163
183
|
# @param method_name [Symbol] the method to replay
|
|
164
184
|
# @param trace_function_key [String] the trace function key for this method
|
|
165
185
|
# @param limit [Integer, nil] maximum number of traces to replay (default: 5).
|
|
166
|
-
# Ignored when trace_ids is passed
|
|
167
|
-
# already determines how many traces replay.
|
|
186
|
+
# Ignored when trace_ids or dataset_id is passed because either source
|
|
187
|
+
# already determines how many traces replay. Supplying trace_ids also
|
|
188
|
+
# emits a warning.
|
|
168
189
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
169
190
|
# @param name [String, nil] optional display name for the resulting experiment/test run
|
|
170
191
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
@@ -187,7 +208,7 @@ module Bitfab
|
|
|
187
208
|
# same org and trace function or the server rejects the replay
|
|
188
209
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
189
210
|
# "none", or "all". "marked" only mocks spans declared with
|
|
190
|
-
# mock_on_replay: true; "all" mocks every child span.
|
|
211
|
+
# mock_on_replay: true; "all" mocks every matched recorded child span.
|
|
191
212
|
# @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
|
|
192
213
|
# onto the method's current signature when its shape changed after the
|
|
193
214
|
# traces were captured. Receives (args, kwargs, ctx) where ctx is
|
|
@@ -206,11 +227,11 @@ module Bitfab
|
|
|
206
227
|
# errored:, item: } where item is { trace_id:, original_trace_id:,
|
|
207
228
|
# original_span_id:, error:, duration_ms: } for the single item that just
|
|
208
229
|
# finished (source_trace_id/source_span_id remain as deprecated aliases).
|
|
209
|
-
# trace_id is the new server replay trace id,
|
|
210
|
-
#
|
|
211
|
-
# ORIGINAL (historical) trace that was replayed,
|
|
212
|
-
# error or nil, and
|
|
213
|
-
#
|
|
230
|
+
# trace_id is the new server replay trace id, available after that item's
|
|
231
|
+
# trace is flushed (nil only when delivery could not be confirmed);
|
|
232
|
+
# original_trace_id is the ORIGINAL (historical) trace that was replayed,
|
|
233
|
+
# error is that item's replay error or nil, and duration_ms is the source
|
|
234
|
+
# trace's recorded duration. Use it to
|
|
214
235
|
# render replay progress (e.g. a per-trace log). A raising callback never
|
|
215
236
|
# crashes the run.
|
|
216
237
|
# @param on_progress [#call, nil] deprecated compatibility callback. It
|
|
@@ -807,8 +828,8 @@ module Bitfab
|
|
|
807
828
|
# (only when include_db_branch_lease was sent). Release it in the +ensure+
|
|
808
829
|
# below so any raise (span fetch, mock-tree build, or the replayed
|
|
809
830
|
# method) frees the Neon resource. Items whose source trace had no
|
|
810
|
-
# snapshot ref arrive without a lease
|
|
811
|
-
#
|
|
831
|
+
# snapshot ref arrive without a lease, so the app uses its normal DB
|
|
832
|
+
# path. Unsafe calls on that path still require replay mocking.
|
|
812
833
|
lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
|
|
813
834
|
# A resolve that was ATTEMPTED and failed is different: the caller asked
|
|
814
835
|
# for a branch, so running their method against live data would produce a
|
|
@@ -847,10 +868,11 @@ module Bitfab
|
|
|
847
868
|
# the tree must be fetched for them to fire even under mock: "none".
|
|
848
869
|
#
|
|
849
870
|
# Only mock: "all" needs every span's recorded output inline (it mocks
|
|
850
|
-
# every child), so it fetches an eager tree
|
|
851
|
-
#
|
|
852
|
-
#
|
|
853
|
-
#
|
|
871
|
+
# every matched recorded child), so it fetches an eager tree even when
|
|
872
|
+
# overrides exist.
|
|
873
|
+
# Non-"all" runs that need a tree ("marked", or "none" with overrides)
|
|
874
|
+
# fetch it payload-free (includeOutputs=false) and pull each mocked span's
|
|
875
|
+
# output lazily by externalSpanId, never dragging down unused outputs.
|
|
854
876
|
overrides_present = !mock_overrides.nil? && !mock_overrides.empty?
|
|
855
877
|
include_outputs = mock_strategy == "all"
|
|
856
878
|
mock_tree = nil
|
|
@@ -864,14 +886,10 @@ module Bitfab
|
|
|
864
886
|
mock_tree = build_mock_tree(tree["root"] || {})
|
|
865
887
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
866
888
|
raise if e.is_a?(SystemExit) || e.is_a?(SignalException)
|
|
867
|
-
|
|
868
|
-
#
|
|
869
|
-
#
|
|
870
|
-
|
|
871
|
-
# "marked" can fall back to real execution (its marked spans just
|
|
872
|
-
# re-run). Mirrors the Python SDK's has_overrides re-raise.
|
|
873
|
-
raise if mock_strategy == "all" || overrides_present
|
|
874
|
-
mock_tree = nil
|
|
889
|
+
raise unless mock_strategy == "marked" && !overrides_present
|
|
890
|
+
# Keep an active empty tree so the root can still run, while any
|
|
891
|
+
# marked child fails closed at its call site.
|
|
892
|
+
mock_tree = {}
|
|
875
893
|
end
|
|
876
894
|
end
|
|
877
895
|
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -114,6 +114,7 @@ module Bitfab
|
|
|
114
114
|
# Each entry is a Hash with :trace_id and :span_id keys.
|
|
115
115
|
module SpanContext
|
|
116
116
|
STACK_KEY = :__bitfab_span_stack
|
|
117
|
+
FIBER_BRIDGE_STACK_KEY = :__bitfab_span_fiber_bridge_stack
|
|
117
118
|
|
|
118
119
|
module_function
|
|
119
120
|
|
|
@@ -122,7 +123,18 @@ module Bitfab
|
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
def current
|
|
125
|
-
stack.last
|
|
126
|
+
stack.last || fiber_bridge_stack.last
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Enumerator.new and enum_for run their source body in another Fiber on
|
|
130
|
+
# the same thread. Bridge only the active Enumerator parent across that
|
|
131
|
+
# boundary; ordinary fiber-local span stacks remain isolated.
|
|
132
|
+
def with_fiber_bridge(trace_id:, span_id:)
|
|
133
|
+
entry = {trace_id:, span_id:}
|
|
134
|
+
fiber_bridge_stack.push(entry)
|
|
135
|
+
yield
|
|
136
|
+
ensure
|
|
137
|
+
fiber_bridge_stack.pop
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
# Execute a block with a new span pushed onto the stack.
|
|
@@ -134,6 +146,15 @@ module Bitfab
|
|
|
134
146
|
ensure
|
|
135
147
|
stack.pop
|
|
136
148
|
end
|
|
149
|
+
|
|
150
|
+
def fiber_bridge_stack
|
|
151
|
+
current_thread = Thread.current
|
|
152
|
+
current_thread.thread_variable_get(FIBER_BRIDGE_STACK_KEY) || begin
|
|
153
|
+
value = []
|
|
154
|
+
current_thread.thread_variable_set(FIBER_BRIDGE_STACK_KEY, value)
|
|
155
|
+
value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
# Global storage for trace states (trace_id -> state hash)
|
data/lib/bitfab/version.rb
CHANGED