bitfab 0.51.9 → 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
@@ -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,12 @@ 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)
356
383
  # Decide at CALL time, not construction. The key may be set after the
357
384
  # client is built (env loaded later), so re-checking per call lets a
358
385
  # late-resolved key take effect.
359
- return yield unless tracing_enabled?
386
+ return yield unless should_record?
360
387
 
361
388
  resolved_capture_when = capture_when.to_s
362
389
  unless ["always", "nested"].include?(resolved_capture_when)
@@ -395,7 +422,7 @@ module Bitfab
395
422
  trace_id = if independent_root && ambient_parent
396
423
  SecureRandom.uuid
397
424
  else
398
- 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
399
426
  end
400
427
  span_id = SecureRandom.uuid
401
428
  parent_span_id = parent&.dig(:span_id)
@@ -409,11 +436,16 @@ module Bitfab
409
436
  if is_root_span && !TraceState.get(trace_id)
410
437
  TraceState.create(
411
438
  trace_id,
439
+ trace_function_key:,
412
440
  test_run_id: resolved_test_run_id,
413
- 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
414
444
  )
415
445
  end
416
446
 
447
+ on_start&.call(trace_id:, span_id:)
448
+
417
449
  # Advance the per-(key, name) call counter for any non-root span under
418
450
  # an active mock tree, even when this span won't itself be mocked.
419
451
  # Unmarked spans must consume an index so subsequent marked siblings
@@ -448,8 +480,10 @@ module Bitfab
448
480
  started_at:,
449
481
  test_run_id: resolved_test_run_id,
450
482
  input_source_span_id: resolved_input_source_span_id,
451
- mock_source:
483
+ mock_source:,
484
+ instrumentation: (surface == :trace) ? "trace" : "span"
452
485
  )
486
+ on_complete&.call(result: mocked_output, error: nil, ended_at: Bitfab.now_iso_timestamp)
453
487
  return mocked_output
454
488
  end
455
489
  end
@@ -477,6 +511,7 @@ module Bitfab
477
511
  span_prompt = nil
478
512
  completed = false
479
513
  @http_client.trace_completion.start(trace_id, span_id)
514
+ recording_override = managed_root ? {} : nil
480
515
 
481
516
  complete = lambda do |final_result, final_error|
482
517
  # Never crash the host app due to span building/sending. Idempotent:
@@ -499,14 +534,18 @@ module Bitfab
499
534
  end
500
535
  end
501
536
 
537
+ on_complete&.call(result: recorded_result, error: recorded_error, ended_at:)
538
+
502
539
  send_span(
540
+ span_data:,
541
+ instrumentation: recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span"),
503
542
  trace_function_key:,
504
543
  trace_id:,
505
544
  span_id:,
506
545
  parent_span_id:,
507
- span_name:,
508
- span_type:,
509
- 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,
510
549
  contexts: span_contexts,
511
550
  prompt: span_prompt,
512
551
  args:,
@@ -567,17 +606,19 @@ module Bitfab
567
606
  trace_id:,
568
607
  span_id:,
569
608
  explicit_span_receiver:,
570
- 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:
571
611
  ) do
612
+ invoke = -> { around_body ? around_body.call { yield } : yield }
572
613
  result = if explicit_span_receiver
573
614
  Subtree.with_borrowed_span(
574
615
  trace_id:,
575
616
  span_id:,
576
617
  receiver: explicit_span_receiver,
577
618
  method_name: explicit_span_method_name
578
- ) { yield }
619
+ ) { invoke.call }
579
620
  else
580
- yield
621
+ invoke.call
581
622
  end
582
623
  ensure
583
624
  # Capture contexts before the span context is popped
@@ -610,6 +651,8 @@ module Bitfab
610
651
  trace_id:,
611
652
  span_id:,
612
653
  complete:,
654
+ surface:,
655
+ around_body:,
613
656
  explicit_span_receiver:,
614
657
  explicit_span_method_name:
615
658
  )
@@ -673,6 +716,13 @@ module Bitfab
673
716
 
674
717
  # Whether tracing should run, decided lazily at call time. An explicit
675
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
+
676
726
  def tracing_enabled?
677
727
  return false unless capture_configured?
678
728
 
@@ -685,8 +735,8 @@ module Bitfab
685
735
  # span output. The span is sent exactly once: when iteration finishes,
686
736
  # raises, or the wrapper is `.close`d.
687
737
  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:}
738
+ explicit_span_method_name: nil, surface: nil, around_body: nil)
739
+ span_entry = {trace_id:, span_id:, surface:}
690
740
  if explicit_span_receiver
691
741
  span_entry[:explicit_span_receiver] = explicit_span_receiver
692
742
  span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
@@ -699,7 +749,7 @@ module Bitfab
699
749
  # the right parent. The fiber bridge carries the same parent into
700
750
  # Enumerator.new / enum_for source fibers on this thread.
701
751
  ReplayContext.with_span(trace_id:, span_id:) do
702
- SpanContext.with_fiber_bridge(trace_id:, span_id:) do
752
+ SpanContext.with_fiber_bridge(trace_id:, span_id:, surface:) do
703
753
  SpanContext.stack.push(span_entry)
704
754
  begin
705
755
  iterate = lambda do
@@ -708,6 +758,8 @@ module Bitfab
708
758
  yielder << value
709
759
  end
710
760
  end
761
+ raw_iterate = iterate
762
+ iterate = -> { around_body.call(&raw_iterate) } if around_body
711
763
  if explicit_span_receiver
712
764
  Subtree.with_borrowed_span(
713
765
  trace_id:,
@@ -755,6 +807,7 @@ module Bitfab
755
807
  "ended_at" => ended_at
756
808
  }
757
809
 
810
+ raw_trace["ingestion_type"] = trace_state[:ingestion_type] if trace_state&.dig(:ingestion_type)
758
811
  if trace_state&.dig(:name)
759
812
  raw_trace["name"] = trace_state[:name]
760
813
  end
@@ -764,6 +817,9 @@ module Bitfab
764
817
  if trace_state&.dig(:contexts)
765
818
  raw_trace["contexts"] = trace_state[:contexts]
766
819
  end
820
+ unless trace_state&.dig(:replay_attempt).nil?
821
+ raw_trace["replay_attempt"] = trace_state[:replay_attempt]
822
+ end
767
823
  if trace_state&.dig(:input_source_trace_id)
768
824
  raw_trace["input_source_trace_id"] = trace_state[:input_source_trace_id]
769
825
  end
@@ -822,7 +878,8 @@ module Bitfab
822
878
  def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
823
879
  span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
824
880
  started_at:, ended_at:, test_run_id: nil, input_source_span_id: nil, mocked: false,
825
- mock_target: nil, mock_source: nil)
881
+ mock_target: nil, mock_source: nil, span_data: {}, instrumentation: "span")
882
+ extra_span_data = span_data
826
883
  # If drop() was called on this trace, suppress the span PAYLOAD upload for
827
884
  # every span that completes after the flag was set. The trace completion
828
885
  # still rides out with dropped: true, so the server scrubs any sibling
@@ -849,6 +906,7 @@ module Bitfab
849
906
  "output" => human_output,
850
907
  "function_name" => function_name
851
908
  }
909
+ span_data.merge!(extra_span_data)
852
910
  span_data["input_serialized"] = marshalled_input if marshalled_input
853
911
  span_data["output_serialized"] = marshalled_output if marshalled_output
854
912
  if error
@@ -863,7 +921,8 @@ module Bitfab
863
921
  "trace_id" => trace_id,
864
922
  "started_at" => started_at,
865
923
  "ended_at" => ended_at,
866
- "span_data" => span_data
924
+ "span_data" => span_data,
925
+ "span_origin" => Bitfab.make_span_origin(instrumentation)
867
926
  }
868
927
  raw_span["parent_id"] = parent_span_id if parent_span_id
869
928
  raw_span["input_source_span_id"] = input_source_span_id if input_source_span_id
@@ -875,6 +934,7 @@ module Bitfab
875
934
  "source" => "ruby-sdk-function",
876
935
  "sourceTraceId" => trace_id,
877
936
  "traceFunctionKey" => trace_function_key,
937
+ "rootTraceFunctionKey" => TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key,
878
938
  "rawSpan" => raw_span
879
939
  }
880
940
  payload["testRunId"] = test_run_id if test_run_id
@@ -1029,7 +1089,7 @@ module Bitfab
1029
1089
  # output as the result and no error.
1030
1090
  def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
1031
1091
  span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
1032
- 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")
1033
1093
  ended_at = Bitfab.now_iso_timestamp
1034
1094
  send_span(
1035
1095
  trace_function_key:,
@@ -1050,6 +1110,7 @@ module Bitfab
1050
1110
  test_run_id:,
1051
1111
  input_source_span_id:,
1052
1112
  mocked: true,
1113
+ instrumentation:,
1053
1114
  # Both paths short-circuit the call, so the target is always the
1054
1115
  # output; only where the value came from differs.
1055
1116
  mock_target: "output",
@@ -1093,7 +1154,7 @@ module Bitfab
1093
1154
  # values warn once and default to "always"
1094
1155
  # @param mock_on_replay [Boolean] mark this span for the default "marked"
1095
1156
  # 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)
1157
+ def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
1097
1158
  Bitfab::Traceable.wrap(
1098
1159
  klass,
1099
1160
  method_name,
@@ -1102,6 +1163,8 @@ module Bitfab
1102
1163
  type:,
1103
1164
  capture_when:,
1104
1165
  mock_on_replay:,
1166
+ finalize:,
1167
+ test_run_id:,
1105
1168
  client: @client
1106
1169
  )
1107
1170
  end
@@ -1109,7 +1172,7 @@ module Bitfab
1109
1172
  # Experimentally trace an existing method and its first-party call subtree.
1110
1173
  def trace(klass, method_name, name: nil, type: "custom",
1111
1174
  max_depth: Subtree::DEFAULT_MAX_DEPTH, max_spans: Subtree::DEFAULT_MAX_SPANS,
1112
- exclude: [], include_wrappers: false)
1175
+ exclude: [], include_wrappers: false, mock_on_replay_default: false)
1113
1176
  Bitfab::Traceable.trace(
1114
1177
  klass, method_name,
1115
1178
  trace_function_key: @trace_function_key,
@@ -1119,6 +1182,7 @@ module Bitfab
1119
1182
  max_spans:,
1120
1183
  exclude:,
1121
1184
  include_wrappers:,
1185
+ mock_on_replay_default:,
1122
1186
  client: @client
1123
1187
  )
1124
1188
  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