bitfab 0.51.2 → 0.51.4
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 +49 -0
- data/lib/bitfab/client.rb +117 -21
- data/lib/bitfab/datasets.rb +23 -5
- data/lib/bitfab/span_context.rb +6 -2
- data/lib/bitfab/subtree.rb +774 -0
- data/lib/bitfab/traceable.rb +253 -6
- data/lib/bitfab/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b739123699ec796d304175df24d0ff07ffa098b95c31d878370a07c7b9cb34c6
|
|
4
|
+
data.tar.gz: 6895704bbaf7f045b704a2ac71a7502a5f5b205792abd80af1b9d4fa124879d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c4b895369cabdcec196d957e92bc2cf13dfe972efa3342ffbb0bc705f7670050cf60a7f85c4132dd92b394c59c338acc41c7bbb5597cb1a8aa160f501a5d3895
|
|
7
|
+
data.tar.gz: 810b845093e98709f56162422a9e87e1de46180e91224a0a60c67abc2f04f39ef1a3add5ac8f31e374c656920dc95c2aec79f6731c8160e0af98884791cf5450
|
data/README.md
CHANGED
|
@@ -215,6 +215,51 @@ end
|
|
|
215
215
|
# └─ check_fraud (grandchild)
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
### Experimental Subtree Tracing
|
|
219
|
+
|
|
220
|
+
Use `bitfab_trace` to record a root method and every first-party Ruby method it calls without wrapping each descendant:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
class OrderPipeline
|
|
224
|
+
include Bitfab::Traceable
|
|
225
|
+
bitfab_function "order-pipeline"
|
|
226
|
+
|
|
227
|
+
bitfab_trace :process, type: "agent"
|
|
228
|
+
def process(order_id)
|
|
229
|
+
validate(order_id)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def validate(order_id)
|
|
233
|
+
{ order_id:, valid: true }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting `max_spans` stops new capture but allows already-open spans in every Fiber to finish. `max_depth:`, `max_spans:`, `exclude:`, and `include_wrappers:` control the subtree. Automatically captured calls are not replay mock targets. Declare a call with `bitfab_span` when replay must substitute it: its explicit span records once, and automatic descendants continue beneath it.
|
|
239
|
+
|
|
240
|
+
When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and `Subtree capture ended before the call returned` as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use.
|
|
241
|
+
|
|
242
|
+
Returning an `Enumerator` keeps capture pending until iteration finishes or raises, including calls already paused in child Fibers. Capture is inactive between the initial return and consumption. Automatic spans record lazy return values as placeholders without iterating them; the traced root records the yielded values.
|
|
243
|
+
|
|
244
|
+
A nested `bitfab_trace` with a different key starts an independent trace while the outer trace continues recording the overlapping subtree. Both traces have distinct trace and span IDs, including during replay, and the complete structure each root would record alone, so two active roots double span volume in their shared region. A recursive call or nested annotation using the same key stays in the active trace instead of creating another root.
|
|
245
|
+
|
|
246
|
+
Configure a discovered method with `bitfab_node`. A node never creates a span or trace by itself; the active `bitfab_trace` owns its key, limits, parent, and lifecycle:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
bitfab_node :summarize,
|
|
250
|
+
name: "Model call",
|
|
251
|
+
type: "llm",
|
|
252
|
+
mock_on_replay: true,
|
|
253
|
+
finalize: ->(response) { {text: response.text} }
|
|
254
|
+
def summarize(order)
|
|
255
|
+
model.generate(order)
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Nodes support `name:`, `type:`, `capture:`, `test_run_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
|
|
260
|
+
|
|
261
|
+
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After `max_spans` is exhausted, it stops collecting metadata and values immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
|
|
262
|
+
|
|
218
263
|
### Input/Output Capture
|
|
219
264
|
|
|
220
265
|
Positional and keyword arguments are automatically captured:
|
|
@@ -427,6 +472,10 @@ See [bitfab-ruby-example/](../bitfab-ruby-example/) for complete working example
|
|
|
427
472
|
|
|
428
473
|
- [test_span.rb](../bitfab-ruby-example/scripts/test_span.rb) - Basic span creation
|
|
429
474
|
- [test_nested_spans.rb](../bitfab-ruby-example/scripts/test_nested_spans.rb) - Nested span hierarchies
|
|
475
|
+
- [test_subtree.rb](../bitfab-ruby-example/scripts/test_subtree.rb) - Whole-subtree capture with independent overlapping roots
|
|
476
|
+
- [test_subtree_comprehensive.rb](../bitfab-ruby-example/scripts/test_subtree_comprehensive.rb) - Self-checking subtree and node behavior examples, including child-Fiber context and installed application gems
|
|
477
|
+
- [test_subtree_replay.rb](../bitfab-ruby-example/scripts/test_subtree_replay.rb) - Live subtree record and replay
|
|
478
|
+
- [subtree coverage map](../bitfab-ruby-example/subtree_examples/README.md) - Every subtree spec behavior mapped to its runnable example
|
|
430
479
|
- [test_wrap.rb](../bitfab-ruby-example/scripts/test_wrap.rb) - Wrapping external code
|
|
431
480
|
- [test_enabled.rb](../bitfab-ruby-example/scripts/test_enabled.rb) - Using the enabled flag
|
|
432
481
|
- [test_metadata.rb](../bitfab-ruby-example/scripts/test_metadata.rb) - Custom metadata
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "constants"
|
|
|
7
7
|
require_relative "http_client"
|
|
8
8
|
require_relative "replay"
|
|
9
9
|
require_relative "span_context"
|
|
10
|
+
require_relative "subtree"
|
|
10
11
|
require_relative "serialize"
|
|
11
12
|
require_relative "timestamp"
|
|
12
13
|
require_relative "warn_once"
|
|
@@ -303,6 +304,23 @@ module Bitfab
|
|
|
303
304
|
BitfabFunction.new(self, trace_function_key)
|
|
304
305
|
end
|
|
305
306
|
|
|
307
|
+
# Configure an existing method only when it is discovered beneath a
|
|
308
|
+
# bitfab_trace root owned by this client.
|
|
309
|
+
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
310
|
+
test_run_id: nil, mock_on_replay: false, finalize: nil)
|
|
311
|
+
Bitfab::Traceable.node(
|
|
312
|
+
klass,
|
|
313
|
+
method_name,
|
|
314
|
+
name:,
|
|
315
|
+
type:,
|
|
316
|
+
capture:,
|
|
317
|
+
test_run_id:,
|
|
318
|
+
mock_on_replay:,
|
|
319
|
+
finalize:,
|
|
320
|
+
client: self
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
306
324
|
# Fetch one persisted span without loading the full trace.
|
|
307
325
|
# Exactly one of id or name is required. Name lookups return the last
|
|
308
326
|
# matching span by default; occurrence also accepts "first" or a zero-based
|
|
@@ -327,7 +345,8 @@ module Bitfab
|
|
|
327
345
|
# Execute a block inside a span context, sending trace data on completion.
|
|
328
346
|
# Called by Traceable, not intended for direct use.
|
|
329
347
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
330
|
-
capture_when: "always", mock_on_replay: false
|
|
348
|
+
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
349
|
+
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil)
|
|
331
350
|
# Decide at CALL time, not construction. The key may be set after the
|
|
332
351
|
# client is built (env loaded later), so re-checking per call lets a
|
|
333
352
|
# late-resolved key take effect.
|
|
@@ -359,13 +378,24 @@ module Bitfab
|
|
|
359
378
|
resolved_input_source_span_id = nil
|
|
360
379
|
begin
|
|
361
380
|
replay_ctx = ReplayContext.current
|
|
362
|
-
|
|
363
|
-
|
|
381
|
+
# A subtree annotation declares a trace-function root, even when it is
|
|
382
|
+
# invoked inside another span. Outer subtree sessions keep observing it
|
|
383
|
+
# through TracePoint instead of owning this root span.
|
|
384
|
+
ambient_parent = SpanContext.current || ReplayContext.current_span
|
|
385
|
+
parent = independent_root ? nil : ambient_parent
|
|
386
|
+
if parent && (subtree_parent_span_id = Subtree.current_parent_span_id(parent[:trace_id]))
|
|
387
|
+
parent = parent.merge(span_id: subtree_parent_span_id)
|
|
388
|
+
end
|
|
389
|
+
trace_id = if independent_root && ambient_parent
|
|
390
|
+
SecureRandom.uuid
|
|
391
|
+
else
|
|
392
|
+
parent&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
393
|
+
end
|
|
364
394
|
span_id = SecureRandom.uuid
|
|
365
395
|
parent_span_id = parent&.dig(:span_id)
|
|
366
396
|
is_root_span = parent_span_id.nil?
|
|
367
397
|
started_at = Bitfab.now_iso_timestamp
|
|
368
|
-
resolved_test_run_id = replay_ctx
|
|
398
|
+
resolved_test_run_id = replay_ctx ? replay_ctx[:test_run_id] : test_run_id
|
|
369
399
|
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
370
400
|
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
371
401
|
|
|
@@ -439,18 +469,28 @@ module Bitfab
|
|
|
439
469
|
error = nil
|
|
440
470
|
span_contexts = nil
|
|
441
471
|
span_prompt = nil
|
|
442
|
-
|
|
472
|
+
completed = false
|
|
443
473
|
|
|
444
|
-
|
|
474
|
+
complete = lambda do |final_result, final_error|
|
|
445
475
|
# Never crash the host app due to span building/sending. Idempotent:
|
|
446
476
|
# only the first call sends the span. Subsequent calls (e.g. from the
|
|
447
477
|
# enumerator wrapper after iteration completes) are no-ops.
|
|
448
|
-
next if
|
|
478
|
+
next if completed
|
|
449
479
|
|
|
450
|
-
|
|
480
|
+
completed = true
|
|
451
481
|
|
|
452
482
|
begin
|
|
453
483
|
ended_at = Bitfab.now_iso_timestamp
|
|
484
|
+
recorded_result = final_result
|
|
485
|
+
recorded_error = final_error
|
|
486
|
+
if finalize && final_error.nil?
|
|
487
|
+
begin
|
|
488
|
+
recorded_result = finalize.call(final_result)
|
|
489
|
+
rescue => e
|
|
490
|
+
recorded_result = nil
|
|
491
|
+
recorded_error = "finalize failed: #{e.message}"
|
|
492
|
+
end
|
|
493
|
+
end
|
|
454
494
|
|
|
455
495
|
send_span(
|
|
456
496
|
trace_function_key:,
|
|
@@ -464,8 +504,8 @@ module Bitfab
|
|
|
464
504
|
prompt: span_prompt,
|
|
465
505
|
args:,
|
|
466
506
|
kwargs:,
|
|
467
|
-
result:
|
|
468
|
-
error:
|
|
507
|
+
result: recorded_result,
|
|
508
|
+
error: recorded_error,
|
|
469
509
|
started_at:,
|
|
470
510
|
ended_at:,
|
|
471
511
|
test_run_id: resolved_test_run_id,
|
|
@@ -514,8 +554,22 @@ module Bitfab
|
|
|
514
554
|
|
|
515
555
|
begin
|
|
516
556
|
ReplayContext.with_span(trace_id:, span_id:) do
|
|
517
|
-
SpanContext.with_span(
|
|
518
|
-
|
|
557
|
+
SpanContext.with_span(
|
|
558
|
+
trace_id:,
|
|
559
|
+
span_id:,
|
|
560
|
+
explicit_span_receiver:,
|
|
561
|
+
explicit_span_method_name: explicit_span_method_name&.to_sym
|
|
562
|
+
) do
|
|
563
|
+
result = if explicit_span_receiver
|
|
564
|
+
Subtree.with_borrowed_span(
|
|
565
|
+
trace_id:,
|
|
566
|
+
span_id:,
|
|
567
|
+
receiver: explicit_span_receiver,
|
|
568
|
+
method_name: explicit_span_method_name
|
|
569
|
+
) { yield }
|
|
570
|
+
else
|
|
571
|
+
yield
|
|
572
|
+
end
|
|
519
573
|
ensure
|
|
520
574
|
# Capture contexts before the span context is popped
|
|
521
575
|
span_contexts = SpanContext.current&.dig(:contexts)
|
|
@@ -524,7 +578,7 @@ module Bitfab
|
|
|
524
578
|
end
|
|
525
579
|
rescue => e
|
|
526
580
|
error = e.message
|
|
527
|
-
|
|
581
|
+
complete.call(result, error)
|
|
528
582
|
raise
|
|
529
583
|
end
|
|
530
584
|
|
|
@@ -542,10 +596,17 @@ module Bitfab
|
|
|
542
596
|
# once iteration completes (or errors).
|
|
543
597
|
#
|
|
544
598
|
if result.is_a?(Enumerator)
|
|
545
|
-
return wrap_enumerator(
|
|
599
|
+
return wrap_enumerator(
|
|
600
|
+
result,
|
|
601
|
+
trace_id:,
|
|
602
|
+
span_id:,
|
|
603
|
+
complete:,
|
|
604
|
+
explicit_span_receiver:,
|
|
605
|
+
explicit_span_method_name:
|
|
606
|
+
)
|
|
546
607
|
end
|
|
547
608
|
|
|
548
|
-
|
|
609
|
+
complete.call(result, error)
|
|
549
610
|
result
|
|
550
611
|
end
|
|
551
612
|
|
|
@@ -606,8 +667,13 @@ module Bitfab
|
|
|
606
667
|
# callbacks nest under the parent span. Yielded values are collected as the
|
|
607
668
|
# span output. The span is sent exactly once: when iteration finishes,
|
|
608
669
|
# raises, or the wrapper is `.close`d.
|
|
609
|
-
def wrap_enumerator(source, trace_id:, span_id:,
|
|
670
|
+
def wrap_enumerator(source, trace_id:, span_id:, complete:, explicit_span_receiver: nil,
|
|
671
|
+
explicit_span_method_name: nil)
|
|
610
672
|
span_entry = {trace_id:, span_id:}
|
|
673
|
+
if explicit_span_receiver
|
|
674
|
+
span_entry[:explicit_span_receiver] = explicit_span_receiver
|
|
675
|
+
span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
|
|
676
|
+
end
|
|
611
677
|
yielded = []
|
|
612
678
|
|
|
613
679
|
Enumerator.new do |yielder|
|
|
@@ -619,13 +685,26 @@ module Bitfab
|
|
|
619
685
|
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
620
686
|
SpanContext.stack.push(span_entry)
|
|
621
687
|
begin
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
688
|
+
iterate = lambda do
|
|
689
|
+
source.each do |value|
|
|
690
|
+
yielded << value
|
|
691
|
+
yielder << value
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
if explicit_span_receiver
|
|
695
|
+
Subtree.with_borrowed_span(
|
|
696
|
+
trace_id:,
|
|
697
|
+
span_id:,
|
|
698
|
+
receiver: explicit_span_receiver,
|
|
699
|
+
method_name: explicit_span_method_name,
|
|
700
|
+
&iterate
|
|
701
|
+
)
|
|
702
|
+
else
|
|
703
|
+
iterate.call
|
|
625
704
|
end
|
|
626
|
-
|
|
705
|
+
complete.call(yielded, nil)
|
|
627
706
|
rescue => e
|
|
628
|
-
|
|
707
|
+
complete.call(yielded, e.message)
|
|
629
708
|
raise
|
|
630
709
|
ensure
|
|
631
710
|
SpanContext.stack.pop
|
|
@@ -1009,5 +1088,22 @@ module Bitfab
|
|
|
1009
1088
|
client: @client
|
|
1010
1089
|
)
|
|
1011
1090
|
end
|
|
1091
|
+
|
|
1092
|
+
# Experimentally trace an existing method and its first-party call subtree.
|
|
1093
|
+
def trace(klass, method_name, name: nil, type: "custom",
|
|
1094
|
+
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
1095
|
+
exclude: [], include_wrappers: false)
|
|
1096
|
+
Bitfab::Traceable.trace(
|
|
1097
|
+
klass, method_name,
|
|
1098
|
+
trace_function_key: @trace_function_key,
|
|
1099
|
+
name:,
|
|
1100
|
+
type:,
|
|
1101
|
+
max_depth:,
|
|
1102
|
+
max_spans:,
|
|
1103
|
+
exclude:,
|
|
1104
|
+
include_wrappers:,
|
|
1105
|
+
client: @client
|
|
1106
|
+
)
|
|
1107
|
+
end
|
|
1012
1108
|
end
|
|
1013
1109
|
end
|
data/lib/bitfab/datasets.rb
CHANGED
|
@@ -25,10 +25,19 @@ module Bitfab
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# List datasets, scoped to one trace function when given and
|
|
28
|
-
# organization-wide otherwise.
|
|
28
|
+
# organization-wide otherwise. Fetches all pages automatically.
|
|
29
29
|
def list(trace_function_key: nil)
|
|
30
|
-
query =
|
|
31
|
-
|
|
30
|
+
query = {"limit" => "100"}
|
|
31
|
+
query["traceFunctionKey"] = trace_function_key unless trace_function_key.nil?
|
|
32
|
+
datasets = []
|
|
33
|
+
loop do
|
|
34
|
+
result = @http_client.get("/api/sdk/datasets?#{URI.encode_www_form(query)}")
|
|
35
|
+
datasets.concat(result["datasets"])
|
|
36
|
+
cursor = result["nextCursor"]
|
|
37
|
+
return datasets if cursor.nil?
|
|
38
|
+
|
|
39
|
+
query["cursor"] = cursor
|
|
40
|
+
end
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
# Fetch one dataset by id. A dataset outside this organization raises
|
|
@@ -38,9 +47,18 @@ module Bitfab
|
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
# The ids of every trace in the dataset, the same membership a replay with
|
|
41
|
-
# dataset_id: selects.
|
|
50
|
+
# dataset_id: selects. Fetches all pages automatically.
|
|
42
51
|
def list_traces(dataset_id)
|
|
43
|
-
|
|
52
|
+
trace_ids = []
|
|
53
|
+
query = {"limit" => "100"}
|
|
54
|
+
loop do
|
|
55
|
+
page = @http_client.get("#{dataset_path(dataset_id, "/traces")}?#{URI.encode_www_form(query)}")
|
|
56
|
+
trace_ids.concat(page["traceIds"])
|
|
57
|
+
cursor = page["nextCursor"]
|
|
58
|
+
return {"datasetId" => page["datasetId"], "traceIds" => trace_ids} if cursor.nil?
|
|
59
|
+
|
|
60
|
+
query["cursor"] = cursor
|
|
61
|
+
end
|
|
44
62
|
end
|
|
45
63
|
|
|
46
64
|
# Add traces to the dataset (1 to 100 ids per call). Traces outside the
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -133,7 +133,7 @@ module Bitfab
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def current
|
|
136
|
-
stack.last || fiber_bridge_stack.last
|
|
136
|
+
stack.last || Subtree.fiber_context&.inherited_span || fiber_bridge_stack.last
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Enumerator.new and enum_for run their source body in another Fiber on
|
|
@@ -149,8 +149,12 @@ module Bitfab
|
|
|
149
149
|
|
|
150
150
|
# Execute a block with a new span pushed onto the stack.
|
|
151
151
|
# The span is automatically popped when the block completes.
|
|
152
|
-
def with_span(trace_id:, span_id:)
|
|
152
|
+
def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil)
|
|
153
153
|
entry = {trace_id:, span_id:}
|
|
154
|
+
if explicit_span_receiver
|
|
155
|
+
entry[:explicit_span_receiver] = explicit_span_receiver
|
|
156
|
+
entry[:explicit_span_method_name] = explicit_span_method_name
|
|
157
|
+
end
|
|
154
158
|
stack.push(entry)
|
|
155
159
|
yield
|
|
156
160
|
ensure
|