bitfab 0.64.25 → 0.65.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6be3e712951d897d0cd849ae85155eb7276e170799228d7d84b190d40f376b3c
4
- data.tar.gz: fd8221b81baa82065582036be59042b2b0faff64e09e173b6f754ca4b3c592c5
3
+ metadata.gz: 9ed49d0b493c83442f21b1fc0ec1598cc675ce438601c357d9e3b16173c9e06d
4
+ data.tar.gz: fde85c743c635b053536c67ede3cced56eb560f92e13ed4014851399ef0f9fa1
5
5
  SHA512:
6
- metadata.gz: de63a2a365d14b3bc59d6cf3b4e427203f56d2963d760732444cee78bdc03d45e57f4da9b7ffd26fb30ad71f7f0d5d9d7df35fad9873bf5a7d8e59de12ba1f13
7
- data.tar.gz: fd21103ff0706e5e3200c35a8cd1fa65b4df2404882f9d6a35c4676d790e9d512bb2dae4aabb0aec4c2ff8820901b74bd0632d953367fc3c029bdcd26495f8fc
6
+ metadata.gz: ed71773a0f5eb1b56f7245e6b77add9c61119ed38dabb2271f322e07b7cbda014d22f9167d52631ebdb99452f64916c1713ec0808042c43db8c1a37184ad5a34
7
+ data.tar.gz: 4539a52a91412534ef12b06ac326b14280ed09b241f203783c731c9ab5b8c4879f623b977ab71124ac2d1c90b4e8e29e27dbbdf7e6ba94ef3a14afe0d9599327
@@ -4,22 +4,39 @@ require "uri"
4
4
 
5
5
  module Bitfab
6
6
  class Traces
7
+ CATEGORY_ID_KEYS = ["category_id", :category_id].freeze
8
+ DEPRECATED_CATEGORY_ID_KEYS = ["category_assertion_id", :category_assertion_id].freeze
9
+ private_constant :CATEGORY_ID_KEYS, :DEPRECATED_CATEGORY_ID_KEYS
10
+
7
11
  def initialize(http_client)
8
12
  @http_client = http_client
9
13
  end
10
14
 
15
+ # Each returned assertion carries "category_id" and, with the same value,
16
+ # the deprecated "category_assertion_id".
11
17
  def get_assertions(trace_id)
12
- @http_client.get(assertions_path(trace_id))
18
+ response = @http_client.get(assertions_path(trace_id))
19
+ return response unless response.is_a?(Hash) && response["assertions"].is_a?(Array)
20
+
21
+ response.merge("assertions" => response["assertions"].map { |assertion| with_both_category_keys(assertion) })
13
22
  end
14
23
 
24
+ # Assertion hashes take "category_id": omit it to keep the assignment, or
25
+ # pass nil to clear it. The deprecated "category_assertion_id" is still
26
+ # accepted and sent as "category_id"; "category_id" wins when both are set.
15
27
  def save_assertions(trace_id, assertions, source: "agent")
16
28
  save_assertions_all([{"traceId" => trace_id, "assertions" => assertions}], source:)
17
29
  end
18
30
 
31
+ # Same assertion hash keys as {#save_assertions}, for several traces at once.
19
32
  def save_assertions_all(updates, source: "agent")
20
33
  return [] if updates.empty?
21
34
 
22
- @http_client.request("/api/sdk/traces/assertions", {"updates" => updates, "source" => source})["assertions"]
35
+ saved = @http_client.request(
36
+ "/api/sdk/traces/assertions",
37
+ {"updates" => updates.map { |update| with_new_category_key(update) }, "source" => source}
38
+ )["assertions"]
39
+ saved.is_a?(Array) ? saved.map { |assertion| with_both_category_keys(assertion) } : saved
23
40
  end
24
41
 
25
42
  def archive_assertions(trace_id, assertion_ids)
@@ -31,5 +48,39 @@ module Bitfab
31
48
  def assertions_path(trace_id, suffix = "")
32
49
  "/api/sdk/traces/#{URI.encode_www_form_component(trace_id).gsub("+", "%20")}/assertions#{suffix}"
33
50
  end
51
+
52
+ def with_new_category_key(update)
53
+ return update unless update.is_a?(Hash)
54
+
55
+ key = ["assertions", :assertions].find { |candidate| update[candidate].is_a?(Array) }
56
+ return update if key.nil?
57
+
58
+ update.merge(key => update[key].map { |assertion| rename_category_key(assertion) })
59
+ end
60
+
61
+ def rename_category_key(assertion)
62
+ return assertion unless assertion.is_a?(Hash)
63
+
64
+ old_key = DEPRECATED_CATEGORY_ID_KEYS.find { |candidate| assertion.key?(candidate) }
65
+ return assertion if old_key.nil?
66
+
67
+ Bitfab.warn_once("category-assertion-id-deprecated",
68
+ "category_assertion_id is deprecated; use category_id instead.")
69
+ renamed = assertion.except(*DEPRECATED_CATEGORY_ID_KEYS)
70
+ renamed["category_id"] = assertion[old_key] unless CATEGORY_ID_KEYS.any? { |candidate| assertion.key?(candidate) }
71
+ renamed
72
+ end
73
+
74
+ def with_both_category_keys(assertion)
75
+ return assertion unless assertion.is_a?(Hash)
76
+
77
+ if assertion.key?("category_id")
78
+ assertion.key?("category_assertion_id") ? assertion : assertion.merge("category_assertion_id" => assertion["category_id"])
79
+ elsif assertion.key?("category_assertion_id")
80
+ assertion.merge("category_id" => assertion["category_assertion_id"])
81
+ else
82
+ assertion
83
+ end
84
+ end
34
85
  end
35
86
  end
@@ -3,8 +3,9 @@
3
3
  require "uri"
4
4
 
5
5
  module Bitfab
6
- class AssertionCategories
7
- PATH = "/api/sdk/assertionCategories"
6
+ # The organization's categories, which group assertions and scenarios.
7
+ class Categories
8
+ PATH = "/api/sdk/categories"
8
9
  private_constant :PATH
9
10
 
10
11
  def initialize(http_client)
@@ -36,4 +37,7 @@ module Bitfab
36
37
  URI.encode_www_form_component(value).gsub("+", "%20")
37
38
  end
38
39
  end
40
+
41
+ # @deprecated Use {Categories}.
42
+ AssertionCategories = Categories
39
43
  end
data/lib/bitfab/client.rb CHANGED
@@ -37,7 +37,13 @@ module Bitfab
37
37
 
38
38
  attr_reader :service_url
39
39
 
40
- attr_reader :datasets, :assertion_categories, :organization_members, :traces, :spans, :labels, :graders, :experiments
40
+ attr_reader :datasets, :categories, :organization_members, :traces, :spans, :labels, :graders, :experiments
41
+
42
+ # @deprecated Use {#categories}. Returns the same object.
43
+ def assertion_categories
44
+ Bitfab.warn_once("assertion-categories-deprecated", "assertion_categories is deprecated; use categories instead.")
45
+ @categories
46
+ end
41
47
 
42
48
  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
49
  @db_snapshot = DbSnapshot.normalize_config(db_snapshot)
@@ -54,7 +60,7 @@ module Bitfab
54
60
  @http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url, timeout:)
55
61
  @http_client.configure_simulation_plan(enabled: simulation_plan, start: !api_key_resolved_lazily?)
56
62
  @datasets = Datasets.new(@http_client)
57
- @assertion_categories = AssertionCategories.new(@http_client)
63
+ @categories = Categories.new(@http_client)
58
64
  @organization_members = OrganizationMembers.new(@http_client)
59
65
  @traces = Traces.new(@http_client)
60
66
  @spans = Spans.new(@http_client)
@@ -21,8 +21,13 @@ module Bitfab
21
21
  # {"summary" => String, "actions" => [{"name", "details", "result", "failed", "at"}], "source" => Hash or nil}
22
22
  # and replaces the saved value whole. Passing nil clears it. Leaving it out keeps it.
23
23
  #
24
+ # category_ids: files the scenario under those categories and replaces its whole set of categories.
25
+ # Leave it out to keep the current set, and pass an empty array to clear it. The returned hash lists the
26
+ # scenario's categories under "categories", each {"id", "title", "description"}, sorted by title.
27
+ #
24
28
  # Raises ArgumentError before any request when nothing is passed, or when data: and append_data: are both passed.
25
- def save_scenario(trace_id, name: nil, description: nil, trigger: nil, data: nil, append_data: nil, actual_outcome: UNSET)
29
+ def save_scenario(trace_id, name: nil, description: nil, trigger: nil, data: nil, append_data: nil, actual_outcome: UNSET,
30
+ category_ids: nil)
26
31
  if !data.nil? && !append_data.nil?
27
32
  raise ArgumentError, "Pass data or append_data, not both: data replaces the saved data whole."
28
33
  end
@@ -34,8 +39,9 @@ module Bitfab
34
39
  payload["data"] = data unless data.nil?
35
40
  payload["appendData"] = append_data unless append_data.nil?
36
41
  payload["actualOutcome"] = actual_outcome unless actual_outcome.equal?(UNSET)
42
+ payload["categoryIds"] = category_ids.to_a unless category_ids.nil?
37
43
  if payload.empty?
38
- raise ArgumentError, "Pass at least one of name, description, trigger, data, append_data, or actual_outcome: there is nothing to save otherwise."
44
+ raise ArgumentError, "Pass at least one of name, description, trigger, data, append_data, actual_outcome, or category_ids: there is nothing to save otherwise."
39
45
  end
40
46
 
41
47
  @http_client.request(scenario_path(trace_id), payload)["scenario"]
@@ -796,6 +796,8 @@ module Bitfab
796
796
  end
797
797
 
798
798
  def skip_boundary_call?(event)
799
+ return false if sdk_frame?(event.path)
800
+
799
801
  boundary = @stacks[Fiber.current.object_id]&.last
800
802
  if boundary.is_a?(Boundary) && boundary.skip_next_call &&
801
803
  boundary.receiver.equal?(event.self) && boundary.method_name == called_method_id(event)
@@ -819,6 +821,13 @@ module Bitfab
819
821
  true
820
822
  end
821
823
 
824
+ def sdk_frame?(path)
825
+ @sdk_frames ||= {}
826
+ @sdk_frames.fetch(path) do
827
+ @sdk_frames[path] = Subtree.realpath(path)&.start_with?(SDK_PREFIX) || false
828
+ end
829
+ end
830
+
822
831
  def passthrough_wrapper?(parameters)
823
832
  kinds = parameters.map(&:first)
824
833
  kinds.any? { |kind| kind == :rest || kind == :keyrest } &&
@@ -108,7 +108,7 @@ module Bitfab
108
108
  client: target_client, trace_function_key:, config: capture_config,
109
109
  max_depth:, max_captured_subtree_spans:, include_wrappers:, mock_on_replay_default:,
110
110
  root_receiver: self, root_method_name: method_name
111
- ) { super(*args, **kwargs, &block) }
111
+ ) { Traceable.send(:as_root_call, self, method_name) { super(*args, **kwargs, &block) } }
112
112
  end
113
113
  call = Subtree::ConfiguredCall.new(receiver: self, method_name:, name: span_name,
114
114
  type:, args:, kwargs:, nested_trace_key: trace_function_key)
@@ -122,12 +122,14 @@ module Bitfab
122
122
  client: target_client, trace_function_key:, config: capture_config,
123
123
  max_depth:, max_captured_subtree_spans:, include_wrappers:, mock_on_replay_default:,
124
124
  root_receiver: self, root_method_name: method_name
125
- ) { super(*args, **kwargs, &block) }
125
+ ) { Traceable.send(:as_root_call, self, method_name) { super(*args, **kwargs, &block) } }
126
126
  end
127
127
  end
128
128
  end
129
129
  wrapper.instance_variable_set(:@bitfab_span_method, method_name_str)
130
130
  wrapper.instance_variable_set(:@bitfab_span_key, trace_function_key)
131
+ wrapper.instance_variable_set(:@bitfab_trace_method, method_name_str)
132
+ wrapper.instance_variable_set(:@bitfab_trace_client, bound_client)
131
133
  preserve_visibility(klass, wrapper, method_name)
132
134
  klass.prepend(wrapper)
133
135
  end
@@ -142,9 +144,12 @@ module Bitfab
142
144
  bound_client = client
143
145
  wrapper = Module.new do
144
146
  define_method(method_name) do |*args, **kwargs, &block|
147
+ next super(*args, **kwargs, &block) if Traceable.send(:own_span_call?, self, method_name)
148
+
145
149
  Subtree.assert_surface!(:trace)
146
150
  session = Subtree.current_session
147
- if !session || (bound_client && !session.client.equal?(bound_client))
151
+ if !session || (bound_client && !session.client.equal?(bound_client)) ||
152
+ Traceable.send(:root_handles_call?, self, method_name, wrapper)
148
153
  next super(*args, **kwargs, &block)
149
154
  end
150
155
  unless capture
@@ -176,6 +181,49 @@ module Bitfab
176
181
  klass.prepend(wrapper)
177
182
  end
178
183
 
184
+ ROOT_CALL_KEY = :bitfab_root_call
185
+
186
+ def self.as_root_call(receiver, method_name)
187
+ previous = Thread.current[ROOT_CALL_KEY]
188
+ Thread.current[ROOT_CALL_KEY] = [receiver, method_name.to_sym]
189
+ yield
190
+ ensure
191
+ Thread.current[ROOT_CALL_KEY] = previous
192
+ end
193
+ private_class_method :as_root_call
194
+
195
+ def self.root_handles_call?(receiver, method_name, node_wrapper)
196
+ marker = Thread.current[ROOT_CALL_KEY]
197
+ if marker && marker[0].equal?(receiver) && marker[1] == method_name.to_sym
198
+ Thread.current[ROOT_CALL_KEY] = nil
199
+ return true
200
+ end
201
+
202
+ trace_wrapper = inner_trace_wrapper(receiver, method_name, node_wrapper)
203
+ return false unless trace_wrapper
204
+
205
+ client = trace_wrapper.instance_variable_get(:@bitfab_trace_client) || Bitfab.client_or_nil
206
+ (client&.send(:should_record?) && !Subtree.nested_root_suppressed?) ? true : false
207
+ end
208
+ private_class_method :root_handles_call?
209
+
210
+ def self.own_span_call?(receiver, method_name)
211
+ span = SpanContext.current
212
+ return false unless span && span[:surface] == :span
213
+
214
+ span[:explicit_span_receiver].equal?(receiver) && span[:explicit_span_method_name] == method_name
215
+ end
216
+ private_class_method :own_span_call?
217
+
218
+ def self.inner_trace_wrapper(receiver, method_name, node_wrapper)
219
+ ancestors = receiver.is_a?(Module) ? receiver.singleton_class.ancestors : receiver.class.ancestors
220
+ name = method_name.to_s
221
+ ancestors.drop_while { |mod| !mod.equal?(node_wrapper) }.find do |mod|
222
+ mod.instance_variable_defined?(:@bitfab_trace_method) && mod.instance_variable_get(:@bitfab_trace_method) == name
223
+ end
224
+ end
225
+ private_class_method :inner_trace_wrapper
226
+
179
227
  def self.source_of(klass, method_name)
180
228
  path, line = klass.instance_method(method_name).source_location || [nil, nil]
181
229
  [path, line, method_name]
@@ -345,15 +393,14 @@ module Bitfab
345
393
 
346
394
  def method_added(method_name)
347
395
  super
396
+ if defined?(@_bitfab_pending_nodes) && @_bitfab_pending_nodes&.key?(method_name)
397
+ _bitfab_node_method(method_name, **@_bitfab_pending_nodes.delete(method_name))
398
+ end
348
399
  if defined?(@_bitfab_pending_spans) && @_bitfab_pending_spans&.key?(method_name)
349
- config = @_bitfab_pending_spans.delete(method_name)
350
- _bitfab_wrap_method(method_name, **config)
351
- elsif defined?(@_bitfab_pending_traces) && @_bitfab_pending_traces&.key?(method_name)
352
- config = @_bitfab_pending_traces.delete(method_name)
353
- _bitfab_trace_method(method_name, **config)
354
- elsif defined?(@_bitfab_pending_nodes) && @_bitfab_pending_nodes&.key?(method_name)
355
- config = @_bitfab_pending_nodes.delete(method_name)
356
- _bitfab_node_method(method_name, **config)
400
+ _bitfab_wrap_method(method_name, **@_bitfab_pending_spans.delete(method_name))
401
+ end
402
+ if defined?(@_bitfab_pending_traces) && @_bitfab_pending_traces&.key?(method_name)
403
+ _bitfab_trace_method(method_name, **@_bitfab_pending_traces.delete(method_name))
357
404
  end
358
405
  end
359
406
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.64.25"
4
+ VERSION = "0.65.1"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -15,7 +15,7 @@ require_relative "bitfab/git_state"
15
15
  require_relative "bitfab/span_context"
16
16
  require_relative "bitfab/http_client"
17
17
  require_relative "bitfab/datasets"
18
- require_relative "bitfab/assertion_categories"
18
+ require_relative "bitfab/categories"
19
19
  require_relative "bitfab/organization_members"
20
20
  require_relative "bitfab/traces"
21
21
  require_relative "bitfab/spans"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.64.25
4
+ version: 0.65.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -155,9 +155,9 @@ files:
155
155
  - exe/bitfab-replay
156
156
  - exe/bitfab-seed
157
157
  - lib/bitfab.rb
158
- - lib/bitfab/assertion_categories.rb
159
158
  - lib/bitfab/assertions.rb
160
159
  - lib/bitfab/baml.rb
160
+ - lib/bitfab/categories.rb
161
161
  - lib/bitfab/client.rb
162
162
  - lib/bitfab/cloudReplay.py
163
163
  - lib/bitfab/cloud_replay_cli.rb