bitfab 0.51.8 → 0.51.10
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 +7 -7
- 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 +107 -33
- data/lib/bitfab/constants.rb +1 -0
- data/lib/bitfab/db_snapshot.rb +18 -5
- data/lib/bitfab/detached_trace.rb +46 -0
- data/lib/bitfab/env.rb +27 -0
- data/lib/bitfab/graders.rb +23 -0
- data/lib/bitfab/http_client.rb +106 -35
- data/lib/bitfab/labels.rb +80 -0
- 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 +248 -0
- data/lib/bitfab/span_context.rb +9 -4
- data/lib/bitfab/span_origin.rb +13 -0
- data/lib/bitfab/subtree.rb +144 -5
- data/lib/bitfab/thread_propagation.rb +111 -0
- data/lib/bitfab/traceable.rb +70 -155
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +9 -2
- metadata +33 -1
data/lib/bitfab/client.rb
CHANGED
|
@@ -4,13 +4,19 @@ require "securerandom"
|
|
|
4
4
|
require "time"
|
|
5
5
|
|
|
6
6
|
require_relative "constants"
|
|
7
|
+
require_relative "env"
|
|
8
|
+
require_relative "seed_context"
|
|
9
|
+
require_relative "detached_trace"
|
|
7
10
|
require_relative "http_client"
|
|
11
|
+
require_relative "replay_concurrency"
|
|
12
|
+
require_relative "replay_processes"
|
|
8
13
|
require_relative "replay"
|
|
9
14
|
require_relative "span_context"
|
|
10
15
|
require_relative "subtree"
|
|
11
16
|
require_relative "serialize"
|
|
12
17
|
require_relative "timestamp"
|
|
13
18
|
require_relative "warn_once"
|
|
19
|
+
require_relative "thread_propagation"
|
|
14
20
|
|
|
15
21
|
module Bitfab
|
|
16
22
|
class Client
|
|
@@ -31,17 +37,17 @@ module Bitfab
|
|
|
31
37
|
|
|
32
38
|
attr_reader :service_url
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
attr_reader :datasets
|
|
40
|
+
attr_reader :datasets, :assertion_categories, :traces, :labels, :graders
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
41
46
|
@api_key_config = api_key
|
|
42
47
|
@service_url = service_url || DEFAULT_SERVICE_URL
|
|
43
|
-
|
|
44
|
-
@
|
|
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
|
|
50
|
+
@capture_configured_from_env = nil
|
|
45
51
|
@strict = strict
|
|
46
52
|
# Cached only once a non-empty key is found, so an early resolve (before
|
|
47
53
|
# env loaded) can't poison a later one.
|
|
@@ -49,13 +55,19 @@ module Bitfab
|
|
|
49
55
|
@api_key_warned = false
|
|
50
56
|
# The key is NOT read here. HttpClient gets a proc so the key is resolved
|
|
51
57
|
# at send time, after any env loading has run.
|
|
52
|
-
@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)
|
|
53
60
|
@datasets = Datasets.new(@http_client)
|
|
61
|
+
@assertion_categories = AssertionCategories.new(@http_client)
|
|
54
62
|
@traces = Traces.new(@http_client)
|
|
63
|
+
@labels = Labels.new(@http_client)
|
|
64
|
+
@graders = Graders.new(@http_client)
|
|
55
65
|
# Mock overrides registered via register_mock_override, applied to every
|
|
56
66
|
# replay on this client (after any per-call mock_override). Instance
|
|
57
67
|
# state, no global; clear_mock_overrides resets it.
|
|
58
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
|
|
59
71
|
CommitRef.start_resolution
|
|
60
72
|
end
|
|
61
73
|
|
|
@@ -93,6 +105,10 @@ module Bitfab
|
|
|
93
105
|
tracing_enabled?
|
|
94
106
|
end
|
|
95
107
|
|
|
108
|
+
def capture_enabled
|
|
109
|
+
tracing_enabled?
|
|
110
|
+
end
|
|
111
|
+
|
|
96
112
|
# Replay historical traces through a method and create a test run.
|
|
97
113
|
#
|
|
98
114
|
# @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
|
|
@@ -171,8 +187,8 @@ module Bitfab
|
|
|
171
187
|
# begins processing each item. Pair it with on_item_finish to distinguish
|
|
172
188
|
# queued work from in-flight work. A raising callback never crashes the run.
|
|
173
189
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
174
|
-
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency:
|
|
175
|
-
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,
|
|
176
192
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
177
193
|
Replay.run(
|
|
178
194
|
self,
|
|
@@ -182,7 +198,11 @@ module Bitfab
|
|
|
182
198
|
limit:,
|
|
183
199
|
trace_ids:,
|
|
184
200
|
name:,
|
|
201
|
+
concurrency:,
|
|
185
202
|
max_concurrency:,
|
|
203
|
+
attempts:,
|
|
204
|
+
only_with_assertions:,
|
|
205
|
+
dry_run:,
|
|
186
206
|
code_change_description:,
|
|
187
207
|
code_change_files:,
|
|
188
208
|
experiment_group_id:,
|
|
@@ -305,13 +325,14 @@ module Bitfab
|
|
|
305
325
|
# @param trace_function_key [String]
|
|
306
326
|
# @return [BitfabFunction]
|
|
307
327
|
def get_function(trace_function_key)
|
|
328
|
+
@http_client.refresh_simulation_plan
|
|
308
329
|
BitfabFunction.new(self, trace_function_key)
|
|
309
330
|
end
|
|
310
331
|
|
|
311
332
|
# Configure an existing method only when it is discovered beneath a
|
|
312
333
|
# bitfab_trace root owned by this client.
|
|
313
334
|
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
314
|
-
test_run_id: nil, mock_on_replay:
|
|
335
|
+
test_run_id: nil, mock_on_replay: nil, finalize: nil)
|
|
315
336
|
Bitfab::Traceable.node(
|
|
316
337
|
klass,
|
|
317
338
|
method_name,
|
|
@@ -325,6 +346,13 @@ module Bitfab
|
|
|
325
346
|
)
|
|
326
347
|
end
|
|
327
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
|
+
|
|
328
356
|
# Fetch one persisted span without loading the full trace.
|
|
329
357
|
# Exactly one of id or name is required. Name lookups return the last
|
|
330
358
|
# matching span by default; occurrence also accepts "first" or a zero-based
|
|
@@ -350,11 +378,12 @@ module Bitfab
|
|
|
350
378
|
# Called by Traceable, not intended for direct use.
|
|
351
379
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
352
380
|
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
353
|
-
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)
|
|
354
383
|
# Decide at CALL time, not construction. The key may be set after the
|
|
355
384
|
# client is built (env loaded later), so re-checking per call lets a
|
|
356
385
|
# late-resolved key take effect.
|
|
357
|
-
return yield unless
|
|
386
|
+
return yield unless should_record?
|
|
358
387
|
|
|
359
388
|
resolved_capture_when = capture_when.to_s
|
|
360
389
|
unless ["always", "nested"].include?(resolved_capture_when)
|
|
@@ -393,7 +422,7 @@ module Bitfab
|
|
|
393
422
|
trace_id = if independent_root && ambient_parent
|
|
394
423
|
SecureRandom.uuid
|
|
395
424
|
else
|
|
396
|
-
parent&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
425
|
+
parent&.dig(:trace_id) || SeedContext.current&.dig(:trace_id) || replay_ctx&.dig(:trace_id) || SecureRandom.uuid
|
|
397
426
|
end
|
|
398
427
|
span_id = SecureRandom.uuid
|
|
399
428
|
parent_span_id = parent&.dig(:span_id)
|
|
@@ -407,11 +436,16 @@ module Bitfab
|
|
|
407
436
|
if is_root_span && !TraceState.get(trace_id)
|
|
408
437
|
TraceState.create(
|
|
409
438
|
trace_id,
|
|
439
|
+
trace_function_key:,
|
|
410
440
|
test_run_id: resolved_test_run_id,
|
|
411
|
-
input_source_trace_id: resolved_input_source_trace_id
|
|
441
|
+
input_source_trace_id: resolved_input_source_trace_id,
|
|
442
|
+
replay_attempt: replay_ctx&.dig(:replay_attempt),
|
|
443
|
+
db_snapshot: @db_snapshot
|
|
412
444
|
)
|
|
413
445
|
end
|
|
414
446
|
|
|
447
|
+
on_start&.call(trace_id:, span_id:)
|
|
448
|
+
|
|
415
449
|
# Advance the per-(key, name) call counter for any non-root span under
|
|
416
450
|
# an active mock tree, even when this span won't itself be mocked.
|
|
417
451
|
# Unmarked spans must consume an index so subsequent marked siblings
|
|
@@ -446,8 +480,10 @@ module Bitfab
|
|
|
446
480
|
started_at:,
|
|
447
481
|
test_run_id: resolved_test_run_id,
|
|
448
482
|
input_source_span_id: resolved_input_source_span_id,
|
|
449
|
-
mock_source
|
|
483
|
+
mock_source:,
|
|
484
|
+
instrumentation: (surface == :trace) ? "trace" : "span"
|
|
450
485
|
)
|
|
486
|
+
on_complete&.call(result: mocked_output, error: nil, ended_at: Bitfab.now_iso_timestamp)
|
|
451
487
|
return mocked_output
|
|
452
488
|
end
|
|
453
489
|
end
|
|
@@ -475,6 +511,7 @@ module Bitfab
|
|
|
475
511
|
span_prompt = nil
|
|
476
512
|
completed = false
|
|
477
513
|
@http_client.trace_completion.start(trace_id, span_id)
|
|
514
|
+
recording_override = managed_root ? {} : nil
|
|
478
515
|
|
|
479
516
|
complete = lambda do |final_result, final_error|
|
|
480
517
|
# Never crash the host app due to span building/sending. Idempotent:
|
|
@@ -497,14 +534,18 @@ module Bitfab
|
|
|
497
534
|
end
|
|
498
535
|
end
|
|
499
536
|
|
|
537
|
+
on_complete&.call(result: recorded_result, error: recorded_error, ended_at:)
|
|
538
|
+
|
|
500
539
|
send_span(
|
|
540
|
+
span_data:,
|
|
541
|
+
instrumentation: recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span"),
|
|
501
542
|
trace_function_key:,
|
|
502
543
|
trace_id:,
|
|
503
544
|
span_id:,
|
|
504
545
|
parent_span_id:,
|
|
505
|
-
span_name
|
|
506
|
-
span_type
|
|
507
|
-
function_name
|
|
546
|
+
span_name: recording_override&.dig(:name) || span_name,
|
|
547
|
+
span_type: recording_override&.dig(:type) || span_type,
|
|
548
|
+
function_name: recording_override&.dig(:function_name) || function_name,
|
|
508
549
|
contexts: span_contexts,
|
|
509
550
|
prompt: span_prompt,
|
|
510
551
|
args:,
|
|
@@ -565,17 +606,19 @@ module Bitfab
|
|
|
565
606
|
trace_id:,
|
|
566
607
|
span_id:,
|
|
567
608
|
explicit_span_receiver:,
|
|
568
|
-
explicit_span_method_name: explicit_span_method_name&.to_sym
|
|
609
|
+
explicit_span_method_name: explicit_span_method_name&.to_sym,
|
|
610
|
+
surface:, recording_override:
|
|
569
611
|
) do
|
|
612
|
+
invoke = -> { around_body ? around_body.call { yield } : yield }
|
|
570
613
|
result = if explicit_span_receiver
|
|
571
614
|
Subtree.with_borrowed_span(
|
|
572
615
|
trace_id:,
|
|
573
616
|
span_id:,
|
|
574
617
|
receiver: explicit_span_receiver,
|
|
575
618
|
method_name: explicit_span_method_name
|
|
576
|
-
) {
|
|
619
|
+
) { invoke.call }
|
|
577
620
|
else
|
|
578
|
-
|
|
621
|
+
invoke.call
|
|
579
622
|
end
|
|
580
623
|
ensure
|
|
581
624
|
# Capture contexts before the span context is popped
|
|
@@ -608,6 +651,8 @@ module Bitfab
|
|
|
608
651
|
trace_id:,
|
|
609
652
|
span_id:,
|
|
610
653
|
complete:,
|
|
654
|
+
surface:,
|
|
655
|
+
around_body:,
|
|
611
656
|
explicit_span_receiver:,
|
|
612
657
|
explicit_span_method_name:
|
|
613
658
|
)
|
|
@@ -654,17 +699,32 @@ module Bitfab
|
|
|
654
699
|
"Bitfab.configure. If a script loads env later, load it before the first " \
|
|
655
700
|
"traced call, or pass api_key: -> { ENV[\"BITFAB_API_KEY\"] }."
|
|
656
701
|
end
|
|
657
|
-
if
|
|
702
|
+
if capture_configured? && !@api_key_warned
|
|
658
703
|
@api_key_warned = true
|
|
659
704
|
warn("Bitfab: api_key is empty: tracing is disabled. Provide a valid API key to enable tracing.")
|
|
660
705
|
end
|
|
661
706
|
nil
|
|
662
707
|
end
|
|
663
708
|
|
|
709
|
+
def capture_configured?
|
|
710
|
+
return @enabled_in_code unless @enabled_in_code.nil?
|
|
711
|
+
return @capture_configured_from_env unless @capture_configured_from_env.nil?
|
|
712
|
+
|
|
713
|
+
from_env = Bitfab.read_boolean_env(CAPTURE_ENABLED_ENV)
|
|
714
|
+
@capture_configured_from_env = from_env.nil? || from_env
|
|
715
|
+
end
|
|
716
|
+
|
|
664
717
|
# Whether tracing should run, decided lazily at call time. An explicit
|
|
665
718
|
# `enabled: false` short-circuits without ever touching the key.
|
|
719
|
+
def should_record?
|
|
720
|
+
active_run = ReplayContext.current || SeedContext.current
|
|
721
|
+
return false unless capture_configured? || active_run
|
|
722
|
+
|
|
723
|
+
!resolve_api_key.nil?
|
|
724
|
+
end
|
|
725
|
+
|
|
666
726
|
def tracing_enabled?
|
|
667
|
-
return false unless
|
|
727
|
+
return false unless capture_configured?
|
|
668
728
|
|
|
669
729
|
!resolve_api_key.nil?
|
|
670
730
|
end
|
|
@@ -675,8 +735,8 @@ module Bitfab
|
|
|
675
735
|
# span output. The span is sent exactly once: when iteration finishes,
|
|
676
736
|
# raises, or the wrapper is `.close`d.
|
|
677
737
|
def wrap_enumerator(source, trace_id:, span_id:, complete:, explicit_span_receiver: nil,
|
|
678
|
-
explicit_span_method_name: nil)
|
|
679
|
-
span_entry = {trace_id:, span_id:}
|
|
738
|
+
explicit_span_method_name: nil, surface: nil, around_body: nil)
|
|
739
|
+
span_entry = {trace_id:, span_id:, surface:}
|
|
680
740
|
if explicit_span_receiver
|
|
681
741
|
span_entry[:explicit_span_receiver] = explicit_span_receiver
|
|
682
742
|
span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
|
|
@@ -689,7 +749,7 @@ module Bitfab
|
|
|
689
749
|
# the right parent. The fiber bridge carries the same parent into
|
|
690
750
|
# Enumerator.new / enum_for source fibers on this thread.
|
|
691
751
|
ReplayContext.with_span(trace_id:, span_id:) do
|
|
692
|
-
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
752
|
+
SpanContext.with_fiber_bridge(trace_id:, span_id:, surface:) do
|
|
693
753
|
SpanContext.stack.push(span_entry)
|
|
694
754
|
begin
|
|
695
755
|
iterate = lambda do
|
|
@@ -698,6 +758,8 @@ module Bitfab
|
|
|
698
758
|
yielder << value
|
|
699
759
|
end
|
|
700
760
|
end
|
|
761
|
+
raw_iterate = iterate
|
|
762
|
+
iterate = -> { around_body.call(&raw_iterate) } if around_body
|
|
701
763
|
if explicit_span_receiver
|
|
702
764
|
Subtree.with_borrowed_span(
|
|
703
765
|
trace_id:,
|
|
@@ -745,6 +807,7 @@ module Bitfab
|
|
|
745
807
|
"ended_at" => ended_at
|
|
746
808
|
}
|
|
747
809
|
|
|
810
|
+
raw_trace["ingestion_type"] = trace_state[:ingestion_type] if trace_state&.dig(:ingestion_type)
|
|
748
811
|
if trace_state&.dig(:name)
|
|
749
812
|
raw_trace["name"] = trace_state[:name]
|
|
750
813
|
end
|
|
@@ -754,6 +817,9 @@ module Bitfab
|
|
|
754
817
|
if trace_state&.dig(:contexts)
|
|
755
818
|
raw_trace["contexts"] = trace_state[:contexts]
|
|
756
819
|
end
|
|
820
|
+
unless trace_state&.dig(:replay_attempt).nil?
|
|
821
|
+
raw_trace["replay_attempt"] = trace_state[:replay_attempt]
|
|
822
|
+
end
|
|
757
823
|
if trace_state&.dig(:input_source_trace_id)
|
|
758
824
|
raw_trace["input_source_trace_id"] = trace_state[:input_source_trace_id]
|
|
759
825
|
end
|
|
@@ -812,7 +878,8 @@ module Bitfab
|
|
|
812
878
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
813
879
|
span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
|
|
814
880
|
started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false,
|
|
815
|
-
mock_target: nil, mock_source: nil)
|
|
881
|
+
mock_target: nil, mock_source: nil, span_data: {}, instrumentation: "span")
|
|
882
|
+
extra_span_data = span_data
|
|
816
883
|
# If drop() was called on this trace, suppress the span PAYLOAD upload for
|
|
817
884
|
# every span that completes after the flag was set. The trace completion
|
|
818
885
|
# still rides out with dropped: true, so the server scrubs any sibling
|
|
@@ -839,6 +906,7 @@ module Bitfab
|
|
|
839
906
|
"output" => human_output,
|
|
840
907
|
"function_name" => function_name
|
|
841
908
|
}
|
|
909
|
+
span_data.merge!(extra_span_data)
|
|
842
910
|
span_data["input_serialized"] = marshalled_input if marshalled_input
|
|
843
911
|
span_data["output_serialized"] = marshalled_output if marshalled_output
|
|
844
912
|
if error
|
|
@@ -853,7 +921,8 @@ module Bitfab
|
|
|
853
921
|
"trace_id" => trace_id,
|
|
854
922
|
"started_at" => started_at,
|
|
855
923
|
"ended_at" => ended_at,
|
|
856
|
-
"span_data" => span_data
|
|
924
|
+
"span_data" => span_data,
|
|
925
|
+
"span_origin" => Bitfab.make_span_origin(instrumentation)
|
|
857
926
|
}
|
|
858
927
|
raw_span["parent_id"] = parent_span_id if parent_span_id
|
|
859
928
|
raw_span["input_source_span_id"] = input_source_span_id if input_source_span_id
|
|
@@ -865,6 +934,7 @@ module Bitfab
|
|
|
865
934
|
"source" => "ruby-sdk-function",
|
|
866
935
|
"sourceTraceId" => trace_id,
|
|
867
936
|
"traceFunctionKey" => trace_function_key,
|
|
937
|
+
"rootTraceFunctionKey" => TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key,
|
|
868
938
|
"rawSpan" => raw_span
|
|
869
939
|
}
|
|
870
940
|
payload["testRunId"] = test_run_id if test_run_id
|
|
@@ -1019,7 +1089,7 @@ module Bitfab
|
|
|
1019
1089
|
# output as the result and no error.
|
|
1020
1090
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
1021
1091
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
1022
|
-
started_at:, test_run_id:, input_source_span_id:, mock_source:)
|
|
1092
|
+
started_at:, test_run_id:, input_source_span_id:, mock_source:, instrumentation: "span")
|
|
1023
1093
|
ended_at = Bitfab.now_iso_timestamp
|
|
1024
1094
|
send_span(
|
|
1025
1095
|
trace_function_key:,
|
|
@@ -1040,6 +1110,7 @@ module Bitfab
|
|
|
1040
1110
|
test_run_id:,
|
|
1041
1111
|
input_source_span_id:,
|
|
1042
1112
|
mocked: true,
|
|
1113
|
+
instrumentation:,
|
|
1043
1114
|
# Both paths short-circuit the call, so the target is always the
|
|
1044
1115
|
# output; only where the value came from differs.
|
|
1045
1116
|
mock_target: "output",
|
|
@@ -1083,7 +1154,7 @@ module Bitfab
|
|
|
1083
1154
|
# values warn once and default to "always"
|
|
1084
1155
|
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
1085
1156
|
# mock strategy. A missing selected occurrence fails closed.
|
|
1086
|
-
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
|
|
1157
|
+
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
|
|
1087
1158
|
Bitfab::Traceable.wrap(
|
|
1088
1159
|
klass,
|
|
1089
1160
|
method_name,
|
|
@@ -1092,6 +1163,8 @@ module Bitfab
|
|
|
1092
1163
|
type:,
|
|
1093
1164
|
capture_when:,
|
|
1094
1165
|
mock_on_replay:,
|
|
1166
|
+
finalize:,
|
|
1167
|
+
test_run_id:,
|
|
1095
1168
|
client: @client
|
|
1096
1169
|
)
|
|
1097
1170
|
end
|
|
@@ -1099,7 +1172,7 @@ module Bitfab
|
|
|
1099
1172
|
# Experimentally trace an existing method and its first-party call subtree.
|
|
1100
1173
|
def trace(klass, method_name, name: nil, type: "custom",
|
|
1101
1174
|
max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
|
|
1102
|
-
exclude: [], include_wrappers: false)
|
|
1175
|
+
exclude: [], include_wrappers: false, mock_on_replay_default: false)
|
|
1103
1176
|
Bitfab::Traceable.trace(
|
|
1104
1177
|
klass, method_name,
|
|
1105
1178
|
trace_function_key: @trace_function_key,
|
|
@@ -1109,6 +1182,7 @@ module Bitfab
|
|
|
1109
1182
|
max_spans:,
|
|
1110
1183
|
exclude:,
|
|
1111
1184
|
include_wrappers:,
|
|
1185
|
+
mock_on_replay_default:,
|
|
1112
1186
|
client: @client
|
|
1113
1187
|
)
|
|
1114
1188
|
end
|
data/lib/bitfab/constants.rb
CHANGED
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
|
data/lib/bitfab/env.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "warn_once"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
TRUE_ENV_VALUES = %w[1 true yes].freeze
|
|
7
|
+
FALSE_ENV_VALUES = %w[0 false no].freeze
|
|
8
|
+
private_constant :TRUE_ENV_VALUES, :FALSE_ENV_VALUES
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def read_boolean_env(name)
|
|
12
|
+
raw = ENV[name]
|
|
13
|
+
return nil if raw.nil?
|
|
14
|
+
|
|
15
|
+
value = raw.strip.downcase
|
|
16
|
+
return nil if value.empty?
|
|
17
|
+
return true if TRUE_ENV_VALUES.include?(value)
|
|
18
|
+
return false if FALSE_ENV_VALUES.include?(value)
|
|
19
|
+
|
|
20
|
+
warn_once(
|
|
21
|
+
"unrecognized-boolean-env:#{name}",
|
|
22
|
+
"#{name}=\"#{raw}\" is not 0 or 1; ignoring it."
|
|
23
|
+
)
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
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
|