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.
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
- # Dataset operations for the authenticated organization.
35
- attr_reader :datasets
40
+ attr_reader :datasets, :assertion_categories, :traces, :labels, :graders
36
41
 
37
- # Trace lookup operations for the authenticated organization.
38
- attr_reader :traces
39
-
40
- def initialize(api_key: nil, service_url: nil, enabled: true, 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
41
46
  @api_key_config = api_key
42
47
  @service_url = service_url || DEFAULT_SERVICE_URL
43
- # The user's on/off intent; effective enabled also requires a resolved key.
44
- @explicitly_enabled = 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
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: 10,
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: false, finalize: nil)
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 tracing_enabled?
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
- ) { yield }
619
+ ) { invoke.call }
577
620
  else
578
- yield
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 @explicitly_enabled && !@api_key_warned
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 @explicitly_enabled
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Bitfab
4
4
  DEFAULT_SERVICE_URL = "https://bitfab.ai"
5
+ CAPTURE_ENABLED_ENV = "BITFAB_CAPTURE_ENABLED"
5
6
  REPLAY_CONTEXT_KEY = :__bitfab_replay_context
6
7
 
7
8
  # Wire prefix the Bitfab plugin scans for on stderr to report live progress
@@ -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
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