bitfab 0.51.9 → 0.57.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/README.md +4 -4
- data/exe/bitfab-seed +13 -0
- data/lib/bitfab/assertion_categories.rb +42 -0
- data/lib/bitfab/assertions.rb +35 -0
- data/lib/bitfab/baml.rb +208 -0
- data/lib/bitfab/client.rb +157 -50
- data/lib/bitfab/db_snapshot.rb +18 -5
- data/lib/bitfab/detached_trace.rb +46 -0
- data/lib/bitfab/graders.rb +23 -0
- data/lib/bitfab/http_client.rb +124 -35
- data/lib/bitfab/labels.rb +80 -0
- data/lib/bitfab/otel.rb +1 -1
- data/lib/bitfab/replay.rb +92 -23
- data/lib/bitfab/replay_cli.rb +7 -1
- data/lib/bitfab/replay_concurrency.rb +44 -0
- data/lib/bitfab/replay_memory.rb +111 -0
- data/lib/bitfab/replay_processes.rb +218 -0
- data/lib/bitfab/replay_registry.rb +100 -15
- data/lib/bitfab/seed.rb +128 -0
- data/lib/bitfab/seed_cli.rb +67 -0
- data/lib/bitfab/seed_context.rb +21 -0
- data/lib/bitfab/simulation_plan.rb +307 -0
- data/lib/bitfab/span_context.rb +9 -4
- data/lib/bitfab/span_origin.rb +13 -0
- data/lib/bitfab/subtree.rb +214 -26
- data/lib/bitfab/thread_propagation.rb +111 -0
- data/lib/bitfab/traceable.rb +85 -155
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +9 -2
- metadata +32 -1
data/lib/bitfab/client.rb
CHANGED
|
@@ -5,13 +5,18 @@ require "time"
|
|
|
5
5
|
|
|
6
6
|
require_relative "constants"
|
|
7
7
|
require_relative "env"
|
|
8
|
+
require_relative "seed_context"
|
|
9
|
+
require_relative "detached_trace"
|
|
8
10
|
require_relative "http_client"
|
|
11
|
+
require_relative "replay_concurrency"
|
|
12
|
+
require_relative "replay_processes"
|
|
9
13
|
require_relative "replay"
|
|
10
14
|
require_relative "span_context"
|
|
11
15
|
require_relative "subtree"
|
|
12
16
|
require_relative "serialize"
|
|
13
17
|
require_relative "timestamp"
|
|
14
18
|
require_relative "warn_once"
|
|
19
|
+
require_relative "thread_propagation"
|
|
15
20
|
|
|
16
21
|
module Bitfab
|
|
17
22
|
class Client
|
|
@@ -32,17 +37,16 @@ module Bitfab
|
|
|
32
37
|
|
|
33
38
|
attr_reader :service_url
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
attr_reader :datasets
|
|
40
|
+
attr_reader :datasets, :assertion_categories, :traces, :labels, :graders
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
def initialize(api_key: nil, service_url: nil, capture_enabled: nil, enabled: nil, strict: false, simulation_plan: true, timeout: 120, trace_across_threads: nil, env_vars: nil, baml_client: nil, db_snapshot: nil)
|
|
43
|
+
@db_snapshot = DbSnapshot.normalize_config(db_snapshot)
|
|
44
|
+
@env_vars = env_vars || ENV.to_h
|
|
45
|
+
@baml_client = baml_client
|
|
42
46
|
@api_key_config = api_key
|
|
43
47
|
@service_url = service_url || DEFAULT_SERVICE_URL
|
|
44
|
-
|
|
45
|
-
@enabled_in_code = enabled
|
|
48
|
+
Bitfab.warn_once("enabled-deprecated", "Bitfab(enabled:) is deprecated; use capture_enabled: instead.") unless enabled.nil?
|
|
49
|
+
@enabled_in_code = capture_enabled.nil? ? enabled : capture_enabled
|
|
46
50
|
@capture_configured_from_env = nil
|
|
47
51
|
@strict = strict
|
|
48
52
|
# Cached only once a non-empty key is found, so an early resolve (before
|
|
@@ -51,13 +55,19 @@ module Bitfab
|
|
|
51
55
|
@api_key_warned = false
|
|
52
56
|
# The key is NOT read here. HttpClient gets a proc so the key is resolved
|
|
53
57
|
# at send time, after any env loading has run.
|
|
54
|
-
@http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
|
|
58
|
+
@http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url, timeout:)
|
|
59
|
+
@http_client.configure_simulation_plan(enabled: simulation_plan)
|
|
55
60
|
@datasets = Datasets.new(@http_client)
|
|
61
|
+
@assertion_categories = AssertionCategories.new(@http_client)
|
|
56
62
|
@traces = Traces.new(@http_client)
|
|
63
|
+
@labels = Labels.new(@http_client)
|
|
64
|
+
@graders = Graders.new(@http_client)
|
|
57
65
|
# Mock overrides registered via register_mock_override, applied to every
|
|
58
66
|
# replay on this client (after any per-call mock_override). Instance
|
|
59
67
|
# state, no global; clear_mock_overrides resets it.
|
|
60
68
|
@mock_overrides = []
|
|
69
|
+
resolved_thread_propagation = trace_across_threads.nil? ? %w[1 true yes].include?(ENV.fetch("BITFAB_TRACE_ACROSS_THREADS", "").strip.downcase) : trace_across_threads
|
|
70
|
+
ThreadPropagation.install if resolved_thread_propagation
|
|
61
71
|
CommitRef.start_resolution
|
|
62
72
|
end
|
|
63
73
|
|
|
@@ -95,6 +105,10 @@ module Bitfab
|
|
|
95
105
|
tracing_enabled?
|
|
96
106
|
end
|
|
97
107
|
|
|
108
|
+
def capture_enabled
|
|
109
|
+
tracing_enabled?
|
|
110
|
+
end
|
|
111
|
+
|
|
98
112
|
# Replay historical traces through a method and create a test run.
|
|
99
113
|
#
|
|
100
114
|
# @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
|
|
@@ -173,8 +187,8 @@ module Bitfab
|
|
|
173
187
|
# begins processing each item. Pair it with on_item_finish to distinguish
|
|
174
188
|
# queued work from in-flight work. A raising callback never crashes the run.
|
|
175
189
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
176
|
-
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency:
|
|
177
|
-
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked",
|
|
190
|
+
def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
|
|
191
|
+
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, dry_run: false,
|
|
178
192
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
179
193
|
Replay.run(
|
|
180
194
|
self,
|
|
@@ -184,7 +198,11 @@ module Bitfab
|
|
|
184
198
|
limit:,
|
|
185
199
|
trace_ids:,
|
|
186
200
|
name:,
|
|
201
|
+
concurrency:,
|
|
187
202
|
max_concurrency:,
|
|
203
|
+
attempts:,
|
|
204
|
+
only_with_assertions:,
|
|
205
|
+
dry_run:,
|
|
188
206
|
code_change_description:,
|
|
189
207
|
code_change_files:,
|
|
190
208
|
experiment_group_id:,
|
|
@@ -307,13 +325,14 @@ module Bitfab
|
|
|
307
325
|
# @param trace_function_key [String]
|
|
308
326
|
# @return [BitfabFunction]
|
|
309
327
|
def get_function(trace_function_key)
|
|
328
|
+
@http_client.refresh_simulation_plan
|
|
310
329
|
BitfabFunction.new(self, trace_function_key)
|
|
311
330
|
end
|
|
312
331
|
|
|
313
332
|
# Configure an existing method only when it is discovered beneath a
|
|
314
333
|
# bitfab_trace root owned by this client.
|
|
315
334
|
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
316
|
-
test_run_id: nil, mock_on_replay:
|
|
335
|
+
test_run_id: nil, mock_on_replay: nil, finalize: nil)
|
|
317
336
|
Bitfab::Traceable.node(
|
|
318
337
|
klass,
|
|
319
338
|
method_name,
|
|
@@ -327,6 +346,13 @@ module Bitfab
|
|
|
327
346
|
)
|
|
328
347
|
end
|
|
329
348
|
|
|
349
|
+
# Return a handle that updates a persisted trace without an active span.
|
|
350
|
+
# Each handle operation waits for the server and raises on rejection.
|
|
351
|
+
def get_trace(trace_id)
|
|
352
|
+
validate_trace_id(trace_id)
|
|
353
|
+
DetachedTrace.new(self, @http_client, trace_id)
|
|
354
|
+
end
|
|
355
|
+
|
|
330
356
|
# Fetch one persisted span without loading the full trace.
|
|
331
357
|
# Exactly one of id or name is required. Name lookups return the last
|
|
332
358
|
# matching span by default; occurrence also accepts "first" or a zero-based
|
|
@@ -352,11 +378,13 @@ module Bitfab
|
|
|
352
378
|
# Called by Traceable, not intended for direct use.
|
|
353
379
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
354
380
|
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
355
|
-
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil
|
|
381
|
+
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil,
|
|
382
|
+
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
|
|
383
|
+
result_recorded_elsewhere: false)
|
|
356
384
|
# Decide at CALL time, not construction. The key may be set after the
|
|
357
385
|
# client is built (env loaded later), so re-checking per call lets a
|
|
358
386
|
# late-resolved key take effect.
|
|
359
|
-
return yield unless
|
|
387
|
+
return yield unless should_record?
|
|
360
388
|
|
|
361
389
|
resolved_capture_when = capture_when.to_s
|
|
362
390
|
unless ["always", "nested"].include?(resolved_capture_when)
|
|
@@ -395,7 +423,7 @@ module Bitfab
|
|
|
395
423
|
trace_id = if independent_root && ambient_parent
|
|
396
424
|
SecureRandom.uuid
|
|
397
425
|
else
|
|
398
|
-
parent&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
426
|
+
parent&.dig(:trace_id) || SeedContext.current&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
399
427
|
end
|
|
400
428
|
span_id = SecureRandom.uuid
|
|
401
429
|
parent_span_id = parent&.dig(:span_id)
|
|
@@ -409,11 +437,16 @@ module Bitfab
|
|
|
409
437
|
if is_root_span && !TraceState.get(trace_id)
|
|
410
438
|
TraceState.create(
|
|
411
439
|
trace_id,
|
|
440
|
+
trace_function_key:,
|
|
412
441
|
test_run_id: resolved_test_run_id,
|
|
413
|
-
input_source_trace_id: resolved_input_source_trace_id
|
|
442
|
+
input_source_trace_id: resolved_input_source_trace_id,
|
|
443
|
+
replay_attempt: replay_ctx&.dig(:replay_attempt),
|
|
444
|
+
db_snapshot: @db_snapshot
|
|
414
445
|
)
|
|
415
446
|
end
|
|
416
447
|
|
|
448
|
+
on_start&.call(trace_id:, span_id:)
|
|
449
|
+
|
|
417
450
|
# Advance the per-(key, name) call counter for any non-root span under
|
|
418
451
|
# an active mock tree, even when this span won't itself be mocked.
|
|
419
452
|
# Unmarked spans must consume an index so subsequent marked siblings
|
|
@@ -448,8 +481,10 @@ module Bitfab
|
|
|
448
481
|
started_at:,
|
|
449
482
|
test_run_id: resolved_test_run_id,
|
|
450
483
|
input_source_span_id: resolved_input_source_span_id,
|
|
451
|
-
mock_source
|
|
484
|
+
mock_source:,
|
|
485
|
+
instrumentation: (surface == :trace) ? "trace" : "span"
|
|
452
486
|
)
|
|
487
|
+
on_complete&.call(result: mocked_output, error: nil, ended_at: Bitfab.now_iso_timestamp)
|
|
453
488
|
return mocked_output
|
|
454
489
|
end
|
|
455
490
|
end
|
|
@@ -477,6 +512,7 @@ module Bitfab
|
|
|
477
512
|
span_prompt = nil
|
|
478
513
|
completed = false
|
|
479
514
|
@http_client.trace_completion.start(trace_id, span_id)
|
|
515
|
+
recording_override = managed_root ? {} : nil
|
|
480
516
|
|
|
481
517
|
complete = lambda do |final_result, final_error|
|
|
482
518
|
# Never crash the host app due to span building/sending. Idempotent:
|
|
@@ -490,7 +526,12 @@ module Bitfab
|
|
|
490
526
|
ended_at = Bitfab.now_iso_timestamp
|
|
491
527
|
recorded_result = final_result
|
|
492
528
|
recorded_error = final_error
|
|
493
|
-
|
|
529
|
+
recorded_name = recording_override&.dig(:name) || span_name
|
|
530
|
+
recorded_instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
|
|
531
|
+
content_off = content_off_by_simulation_plan?(
|
|
532
|
+
trace_id:, trace_function_key:, span_name: recorded_name, instrumentation: recorded_instrumentation
|
|
533
|
+
) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation: recorded_instrumentation))
|
|
534
|
+
if finalize && final_error.nil? && !(content_off && !result_recorded_elsewhere)
|
|
494
535
|
begin
|
|
495
536
|
recorded_result = finalize.call(final_result)
|
|
496
537
|
rescue => e
|
|
@@ -499,14 +540,18 @@ module Bitfab
|
|
|
499
540
|
end
|
|
500
541
|
end
|
|
501
542
|
|
|
543
|
+
on_complete&.call(result: recorded_result, error: recorded_error, ended_at:)
|
|
544
|
+
|
|
502
545
|
send_span(
|
|
546
|
+
span_data: content_off ? span_data.merge("content_off_by_simulation_plan" => true) : span_data,
|
|
547
|
+
instrumentation: recorded_instrumentation,
|
|
503
548
|
trace_function_key:,
|
|
504
549
|
trace_id:,
|
|
505
550
|
span_id:,
|
|
506
551
|
parent_span_id:,
|
|
507
|
-
span_name
|
|
508
|
-
span_type
|
|
509
|
-
function_name
|
|
552
|
+
span_name: recorded_name,
|
|
553
|
+
span_type: recording_override&.dig(:type) || span_type,
|
|
554
|
+
function_name: recording_override&.dig(:function_name) || function_name,
|
|
510
555
|
contexts: span_contexts,
|
|
511
556
|
prompt: span_prompt,
|
|
512
557
|
args:,
|
|
@@ -567,17 +612,19 @@ module Bitfab
|
|
|
567
612
|
trace_id:,
|
|
568
613
|
span_id:,
|
|
569
614
|
explicit_span_receiver:,
|
|
570
|
-
explicit_span_method_name: explicit_span_method_name&.to_sym
|
|
615
|
+
explicit_span_method_name: explicit_span_method_name&.to_sym,
|
|
616
|
+
surface:, recording_override:
|
|
571
617
|
) do
|
|
618
|
+
invoke = -> { around_body ? around_body.call { yield } : yield }
|
|
572
619
|
result = if explicit_span_receiver
|
|
573
620
|
Subtree.with_borrowed_span(
|
|
574
621
|
trace_id:,
|
|
575
622
|
span_id:,
|
|
576
623
|
receiver: explicit_span_receiver,
|
|
577
624
|
method_name: explicit_span_method_name
|
|
578
|
-
) {
|
|
625
|
+
) { invoke.call }
|
|
579
626
|
else
|
|
580
|
-
|
|
627
|
+
invoke.call
|
|
581
628
|
end
|
|
582
629
|
ensure
|
|
583
630
|
# Capture contexts before the span context is popped
|
|
@@ -610,6 +657,8 @@ module Bitfab
|
|
|
610
657
|
trace_id:,
|
|
611
658
|
span_id:,
|
|
612
659
|
complete:,
|
|
660
|
+
surface:,
|
|
661
|
+
around_body:,
|
|
613
662
|
explicit_span_receiver:,
|
|
614
663
|
explicit_span_method_name:
|
|
615
664
|
)
|
|
@@ -629,6 +678,32 @@ module Bitfab
|
|
|
629
678
|
raise ArgumentError, "id must be a valid Bitfab span ID" unless id.is_a?(String) && UUID_PATTERN.match?(id)
|
|
630
679
|
end
|
|
631
680
|
|
|
681
|
+
def content_off_by_simulation_plan?(trace_id:, trace_function_key:, span_name:, instrumentation: "span")
|
|
682
|
+
return false if SimulationPlan::FRAMEWORKS.include?(instrumentation)
|
|
683
|
+
|
|
684
|
+
@http_client.simulation_plan_content_off?(root_trace_function_key(trace_id, trace_function_key), span_name)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def simulation_plan_unavailable?(instrumentation: "span")
|
|
688
|
+
return false if SimulationPlan::FRAMEWORKS.include?(instrumentation)
|
|
689
|
+
|
|
690
|
+
@http_client.simulation_plan_unavailable?
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def simulation_plan_content_off_names?(trace_id:, trace_function_key:)
|
|
694
|
+
@http_client.simulation_plan_content_off_names?(root_trace_function_key(trace_id, trace_function_key))
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def root_trace_function_key(trace_id, trace_function_key)
|
|
698
|
+
TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def wait_for_simulation_plan
|
|
702
|
+
@http_client.wait_for_simulation_plan
|
|
703
|
+
rescue
|
|
704
|
+
nil
|
|
705
|
+
end
|
|
706
|
+
|
|
632
707
|
# Resolve the API key lazily, the first time a span actually needs it.
|
|
633
708
|
#
|
|
634
709
|
# The key is intentionally NOT read at construction: a client built when a
|
|
@@ -673,6 +748,13 @@ module Bitfab
|
|
|
673
748
|
|
|
674
749
|
# Whether tracing should run, decided lazily at call time. An explicit
|
|
675
750
|
# `enabled: false` short-circuits without ever touching the key.
|
|
751
|
+
def should_record?
|
|
752
|
+
active_run = ReplayContext.current || SeedContext.current
|
|
753
|
+
return false unless capture_configured? || active_run
|
|
754
|
+
|
|
755
|
+
!resolve_api_key.nil?
|
|
756
|
+
end
|
|
757
|
+
|
|
676
758
|
def tracing_enabled?
|
|
677
759
|
return false unless capture_configured?
|
|
678
760
|
|
|
@@ -685,8 +767,8 @@ module Bitfab
|
|
|
685
767
|
# span output. The span is sent exactly once: when iteration finishes,
|
|
686
768
|
# raises, or the wrapper is `.close`d.
|
|
687
769
|
def wrap_enumerator(source, trace_id:, span_id:, complete:, explicit_span_receiver: nil,
|
|
688
|
-
explicit_span_method_name: nil)
|
|
689
|
-
span_entry = {trace_id:, span_id:}
|
|
770
|
+
explicit_span_method_name: nil, surface: nil, around_body: nil)
|
|
771
|
+
span_entry = {trace_id:, span_id:, surface:}
|
|
690
772
|
if explicit_span_receiver
|
|
691
773
|
span_entry[:explicit_span_receiver] = explicit_span_receiver
|
|
692
774
|
span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
|
|
@@ -699,7 +781,7 @@ module Bitfab
|
|
|
699
781
|
# the right parent. The fiber bridge carries the same parent into
|
|
700
782
|
# Enumerator.new / enum_for source fibers on this thread.
|
|
701
783
|
ReplayContext.with_span(trace_id:, span_id:) do
|
|
702
|
-
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
784
|
+
SpanContext.with_fiber_bridge(trace_id:, span_id:, surface:) do
|
|
703
785
|
SpanContext.stack.push(span_entry)
|
|
704
786
|
begin
|
|
705
787
|
iterate = lambda do
|
|
@@ -708,6 +790,8 @@ module Bitfab
|
|
|
708
790
|
yielder << value
|
|
709
791
|
end
|
|
710
792
|
end
|
|
793
|
+
raw_iterate = iterate
|
|
794
|
+
iterate = -> { around_body.call(&raw_iterate) } if around_body
|
|
711
795
|
if explicit_span_receiver
|
|
712
796
|
Subtree.with_borrowed_span(
|
|
713
797
|
trace_id:,
|
|
@@ -755,6 +839,7 @@ module Bitfab
|
|
|
755
839
|
"ended_at" => ended_at
|
|
756
840
|
}
|
|
757
841
|
|
|
842
|
+
raw_trace["ingestion_type"] = trace_state[:ingestion_type] if trace_state&.dig(:ingestion_type)
|
|
758
843
|
if trace_state&.dig(:name)
|
|
759
844
|
raw_trace["name"] = trace_state[:name]
|
|
760
845
|
end
|
|
@@ -764,6 +849,9 @@ module Bitfab
|
|
|
764
849
|
if trace_state&.dig(:contexts)
|
|
765
850
|
raw_trace["contexts"] = trace_state[:contexts]
|
|
766
851
|
end
|
|
852
|
+
unless trace_state&.dig(:replay_attempt).nil?
|
|
853
|
+
raw_trace["replay_attempt"] = trace_state[:replay_attempt]
|
|
854
|
+
end
|
|
767
855
|
if trace_state&.dig(:input_source_trace_id)
|
|
768
856
|
raw_trace["input_source_trace_id"] = trace_state[:input_source_trace_id]
|
|
769
857
|
end
|
|
@@ -822,7 +910,8 @@ module Bitfab
|
|
|
822
910
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
823
911
|
span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
|
|
824
912
|
started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false,
|
|
825
|
-
mock_target: nil, mock_source: nil)
|
|
913
|
+
mock_target: nil, mock_source: nil, span_data: {}, instrumentation: "span")
|
|
914
|
+
extra_span_data = span_data
|
|
826
915
|
# If drop() was called on this trace, suppress the span PAYLOAD upload for
|
|
827
916
|
# every span that completes after the flag was set. The trace completion
|
|
828
917
|
# still rides out with dropped: true, so the server scrubs any sibling
|
|
@@ -833,24 +922,34 @@ module Bitfab
|
|
|
833
922
|
# Human-readable JSON (input/output fields). The *_with_report variants
|
|
834
923
|
# also report what could not be faithfully captured so a lossy span is
|
|
835
924
|
# marked non-replayable below instead of shipping as if it round-trips.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
925
|
+
content_off = extra_span_data["content_off_by_simulation_plan"] == true ||
|
|
926
|
+
content_off_by_simulation_plan?(trace_id:, trace_function_key:, span_name:, instrumentation:) ||
|
|
927
|
+
(!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation:))
|
|
928
|
+
if content_off
|
|
929
|
+
dropped = []
|
|
930
|
+
span_data = {"name" => span_name, "type" => span_type, "function_name" => function_name}
|
|
931
|
+
span_data.merge!(extra_span_data.except(*SimulationPlan::CONTENT_KEYS))
|
|
932
|
+
span_data["content_off_by_simulation_plan"] = true
|
|
933
|
+
else
|
|
934
|
+
human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
935
|
+
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
936
|
+
dropped = input_dropped + output_dropped
|
|
937
|
+
|
|
938
|
+
raw_input = (args.length == 1 && kwargs.empty?) ? args[0] : {args:, kwargs:}
|
|
939
|
+
marshalled_input = Serialize.marshal_value(raw_input)
|
|
940
|
+
marshalled_output = Serialize.marshal_value(result)
|
|
941
|
+
|
|
942
|
+
span_data = {
|
|
943
|
+
"name" => span_name,
|
|
944
|
+
"type" => span_type,
|
|
945
|
+
"input" => human_inputs,
|
|
946
|
+
"output" => human_output,
|
|
947
|
+
"function_name" => function_name
|
|
948
|
+
}
|
|
949
|
+
span_data.merge!(extra_span_data)
|
|
950
|
+
span_data["input_serialized"] = marshalled_input if marshalled_input
|
|
951
|
+
span_data["output_serialized"] = marshalled_output if marshalled_output
|
|
952
|
+
end
|
|
854
953
|
if error
|
|
855
954
|
span_data["error"] = error
|
|
856
955
|
span_data["error_source"] = "code"
|
|
@@ -863,7 +962,8 @@ module Bitfab
|
|
|
863
962
|
"trace_id" => trace_id,
|
|
864
963
|
"started_at" => started_at,
|
|
865
964
|
"ended_at" => ended_at,
|
|
866
|
-
"span_data" => span_data
|
|
965
|
+
"span_data" => span_data,
|
|
966
|
+
"span_origin" => Bitfab.make_span_origin(instrumentation)
|
|
867
967
|
}
|
|
868
968
|
raw_span["parent_id"] = parent_span_id if parent_span_id
|
|
869
969
|
raw_span["input_source_span_id"] = input_source_span_id if input_source_span_id
|
|
@@ -875,6 +975,7 @@ module Bitfab
|
|
|
875
975
|
"source" => "ruby-sdk-function",
|
|
876
976
|
"sourceTraceId" => trace_id,
|
|
877
977
|
"traceFunctionKey" => trace_function_key,
|
|
978
|
+
"rootTraceFunctionKey" => TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key,
|
|
878
979
|
"rawSpan" => raw_span
|
|
879
980
|
}
|
|
880
981
|
payload["testRunId"] = test_run_id if test_run_id
|
|
@@ -1029,7 +1130,7 @@ module Bitfab
|
|
|
1029
1130
|
# output as the result and no error.
|
|
1030
1131
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
1031
1132
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
1032
|
-
started_at:, test_run_id:, input_source_span_id:, mock_source:)
|
|
1133
|
+
started_at:, test_run_id:, input_source_span_id:, mock_source:, instrumentation: "span")
|
|
1033
1134
|
ended_at = Bitfab.now_iso_timestamp
|
|
1034
1135
|
send_span(
|
|
1035
1136
|
trace_function_key:,
|
|
@@ -1050,6 +1151,7 @@ module Bitfab
|
|
|
1050
1151
|
test_run_id:,
|
|
1051
1152
|
input_source_span_id:,
|
|
1052
1153
|
mocked: true,
|
|
1154
|
+
instrumentation:,
|
|
1053
1155
|
# Both paths short-circuit the call, so the target is always the
|
|
1054
1156
|
# output; only where the value came from differs.
|
|
1055
1157
|
mock_target: "output",
|
|
@@ -1093,7 +1195,7 @@ module Bitfab
|
|
|
1093
1195
|
# values warn once and default to "always"
|
|
1094
1196
|
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
1095
1197
|
# mock strategy. A missing selected occurrence fails closed.
|
|
1096
|
-
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
|
|
1198
|
+
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
|
|
1097
1199
|
Bitfab::Traceable.wrap(
|
|
1098
1200
|
klass,
|
|
1099
1201
|
method_name,
|
|
@@ -1102,6 +1204,8 @@ module Bitfab
|
|
|
1102
1204
|
type:,
|
|
1103
1205
|
capture_when:,
|
|
1104
1206
|
mock_on_replay:,
|
|
1207
|
+
finalize:,
|
|
1208
|
+
test_run_id:,
|
|
1105
1209
|
client: @client
|
|
1106
1210
|
)
|
|
1107
1211
|
end
|
|
@@ -1109,7 +1213,8 @@ module Bitfab
|
|
|
1109
1213
|
# Experimentally trace an existing method and its first-party call subtree.
|
|
1110
1214
|
def trace(klass, method_name, name: nil, type: "custom",
|
|
1111
1215
|
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
1112
|
-
|
|
1216
|
+
max_captured_subtree_spans: Subtree::DEFAULT_MAX_CAPTURED_SUBTREE_SPANS,
|
|
1217
|
+
exclude: [], include_wrappers: false, mock_on_replay_default: false)
|
|
1113
1218
|
Bitfab::Traceable.trace(
|
|
1114
1219
|
klass, method_name,
|
|
1115
1220
|
trace_function_key: @trace_function_key,
|
|
@@ -1117,8 +1222,10 @@ module Bitfab
|
|
|
1117
1222
|
type:,
|
|
1118
1223
|
max_depth:,
|
|
1119
1224
|
max_spans:,
|
|
1225
|
+
max_captured_subtree_spans:,
|
|
1120
1226
|
exclude:,
|
|
1121
1227
|
include_wrappers:,
|
|
1228
|
+
mock_on_replay_default:,
|
|
1122
1229
|
client: @client
|
|
1123
1230
|
)
|
|
1124
1231
|
end
|
data/lib/bitfab/db_snapshot.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Bitfab
|
|
|
6
6
|
# Every root trace carries a snapshot ref that pins the DB state at trace
|
|
7
7
|
# open by wall-clock timestamp. Capturing the timestamp is free (no IO) and
|
|
8
8
|
# harmless, so it happens on every trace regardless of configuration: that
|
|
9
|
-
# lets any trace be replayed against a historical branch later. The provider
|
|
10
|
-
# is resolved at replay time. Mirrors the TypeScript and Python SDKs'
|
|
9
|
+
# lets any trace be replayed against a historical branch later. The optional configured provider
|
|
10
|
+
# is captured too; otherwise it is resolved at replay time. Mirrors the TypeScript and Python SDKs'
|
|
11
11
|
# +DbSnapshotRef+ wire shape (camelCase key, since the server speaks the same
|
|
12
12
|
# protocol for every SDK).
|
|
13
13
|
module DbSnapshot
|
|
@@ -17,13 +17,26 @@ module Bitfab
|
|
|
17
17
|
#
|
|
18
18
|
# Stores only the wall clock the SDK observed immediately before invoking
|
|
19
19
|
# the wrapped function; the server-side resolver uses that as the snapshot
|
|
20
|
-
# timestamp.
|
|
20
|
+
# timestamp. A configured provider is included; otherwise it is resolved at replay time.
|
|
21
21
|
#
|
|
22
22
|
# @param sdk_wall_clock_before_fn [String] ISO wall-clock timestamp the SDK
|
|
23
23
|
# observed immediately before invoking the wrapped function.
|
|
24
24
|
# @return [Hash] snapshot ref with a single camelCase +sdkWallClockBeforeFn+ key.
|
|
25
|
-
def build_snapshot_ref(sdk_wall_clock_before_fn)
|
|
26
|
-
{"sdkWallClockBeforeFn" => sdk_wall_clock_before_fn}
|
|
25
|
+
def build_snapshot_ref(sdk_wall_clock_before_fn, config: nil)
|
|
26
|
+
ref = {"sdkWallClockBeforeFn" => sdk_wall_clock_before_fn}
|
|
27
|
+
ref["provider"] = config.fetch("provider") if config
|
|
28
|
+
ref
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_config(config)
|
|
32
|
+
return nil if config.nil?
|
|
33
|
+
raise ArgumentError, "db_snapshot must be a Hash with provider: neon." unless config.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
provider = config[:provider] || config["provider"]
|
|
36
|
+
unless provider.to_s == "neon"
|
|
37
|
+
raise ArgumentError, "db_snapshot.provider #{provider.inspect} is not supported. Supported providers: neon."
|
|
38
|
+
end
|
|
39
|
+
{"provider" => "neon"}.freeze
|
|
27
40
|
end
|
|
28
41
|
end
|
|
29
42
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bitfab
|
|
4
|
+
class DetachedTrace
|
|
5
|
+
attr_reader :trace_id
|
|
6
|
+
|
|
7
|
+
def initialize(client, http_client, trace_id)
|
|
8
|
+
@client = client
|
|
9
|
+
@http_client = http_client
|
|
10
|
+
@trace_id = trace_id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_context(context)
|
|
14
|
+
return unless context.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
patch("appendContexts" => [context])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_metadata(metadata)
|
|
20
|
+
return unless metadata.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
patch("mergeMetadata" => metadata)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def set_session_id(session_id)
|
|
26
|
+
return unless session_id.is_a?(String) && !session_id.empty?
|
|
27
|
+
|
|
28
|
+
patch("setSessionId" => session_id)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set_name(name)
|
|
32
|
+
return unless name.is_a?(String) && !name.empty?
|
|
33
|
+
|
|
34
|
+
patch("setName" => name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def patch(payload)
|
|
40
|
+
return unless @client.enabled
|
|
41
|
+
|
|
42
|
+
@http_client.request("/api/sdk/traces/#{@trace_id}", payload, method: "PATCH", timeout: 10)
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class Graders
|
|
7
|
+
def initialize(http_client)
|
|
8
|
+
@http_client = http_client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get_labels(trace_ids: nil, grader_id: nil, limit: nil)
|
|
12
|
+
if (trace_ids.nil? || trace_ids.empty?) && grader_id.nil?
|
|
13
|
+
raise ArgumentError, "Pass trace_ids, grader_id, or both: there is nothing to read otherwise."
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
params = {}
|
|
17
|
+
params["traceIds"] = trace_ids.join(",") unless trace_ids.nil? || trace_ids.empty?
|
|
18
|
+
params["graderId"] = grader_id unless grader_id.nil?
|
|
19
|
+
params["limit"] = limit unless limit.nil?
|
|
20
|
+
@http_client.get("/api/sdk/graderLabels?#{URI.encode_www_form(params)}")["labels"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|