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.
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
- # Dataset operations for the authenticated organization.
36
- attr_reader :datasets
40
+ attr_reader :datasets, :assertion_categories, :traces, :labels, :graders
37
41
 
38
- # Trace lookup operations for the authenticated organization.
39
- attr_reader :traces
40
-
41
- def initialize(api_key: nil, service_url: nil, enabled: nil, strict: false)
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
- # The user's on/off intent; effective enabled also requires a resolved key.
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: 10,
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: false, finalize: nil)
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 tracing_enabled?
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
- if finalize && final_error.nil?
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
- ) { yield }
625
+ ) { invoke.call }
579
626
  else
580
- yield
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
- human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
837
- human_output, output_dropped = Serialize.serialize_value_with_report(result)
838
- dropped = input_dropped + output_dropped
839
-
840
- # Marshal + Base64 for full Ruby-to-Ruby object reconstruction
841
- raw_input = (args.length == 1 && kwargs.empty?) ? args[0] : {args:, kwargs:}
842
- marshalled_input = Serialize.marshal_value(raw_input)
843
- marshalled_output = Serialize.marshal_value(result)
844
-
845
- span_data = {
846
- "name" => span_name,
847
- "type" => span_type,
848
- "input" => human_inputs,
849
- "output" => human_output,
850
- "function_name" => function_name
851
- }
852
- span_data["input_serialized"] = marshalled_input if marshalled_input
853
- span_data["output_serialized"] = marshalled_output if marshalled_output
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
- exclude: [], include_wrappers: false)
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
@@ -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. No provider is captured (it is resolved at replay time).
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