bitfab 0.75.1 → 0.75.3

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: fad2341a7e82e22c18faf4809a1807407660eed238b04886b2687e4dc15989b2
4
- data.tar.gz: 7a46b9c4935290898decf6f835257d5b8551281eb1ddebb1680128fcd5ac8c74
3
+ metadata.gz: 0e8124c4242b339613c4f4b714a68305748f2c32e2d894fd884250bc8c1e4036
4
+ data.tar.gz: 9ed475b72f8f55b58334af9fe4feae30b07940aa9847c763fbccba227e21e41c
5
5
  SHA512:
6
- metadata.gz: 82ef99ecba72308fc1e865257f260756cab829bae0167404f50f8317dd0d921446b02f4379f5c254e98f8deb3fb68104afa73075c4081cc9d5a7b7345243611f
7
- data.tar.gz: 696fb090a73f51ab3bb9ca07277e89a0529dd3031a8c96bfa581f7c54235ba3205f804df4790e5d83163d635dfa5228f9d0be05e4e37d4c1805fb7271a920732
6
+ metadata.gz: 7b1356376e48cbaf2a8889fdf7c26945ed21050035603ae480e00d6da313acff4292c981d7f9be751b3dcfd37aa954d5ab6619d492a4f0547f8af5f99faddfdb
7
+ data.tar.gz: 15239246da620b92a67d87aa731974b22efe17fa97f2f25103491454a6ddd64327225a3b12f92f6df9ebc1770aceb82a7eb090a938ec31b95d32b0ba63ebd37e
@@ -4,11 +4,9 @@ require "uri"
4
4
 
5
5
  module Bitfab
6
6
  class Traces
7
- CODE_DRAFT_SAVE_TIMEOUT_SECONDS = 300
8
- CODED_KINDS = ["code", "hybrid", :code, :hybrid].freeze
9
7
  CATEGORY_ID_KEYS = ["category_id", :category_id].freeze
10
8
  DEPRECATED_CATEGORY_ID_KEYS = ["category_assertion_id", :category_assertion_id].freeze
11
- private_constant :CODE_DRAFT_SAVE_TIMEOUT_SECONDS, :CODED_KINDS, :CATEGORY_ID_KEYS, :DEPRECATED_CATEGORY_ID_KEYS
9
+ private_constant :CATEGORY_ID_KEYS, :DEPRECATED_CATEGORY_ID_KEYS
12
10
 
13
11
  def initialize(http_client)
14
12
  @http_client = http_client
@@ -31,31 +29,23 @@ module Bitfab
31
29
  # pass nil to clear it. The deprecated "category_assertion_id" is still
32
30
  # accepted and sent as "category_id"; "category_id" wins when both are set.
33
31
  #
34
- # "kind" says who decides the assertion: "judge" (the LLM judge), "code"
35
- # ("code" decides every replay), or "hybrid" ("code" decides what it can
36
- # and returns "defer" for the rest). Send "code" or "hybrid" together with
37
- # "code", {"source" => ..., "spanNames" => [...]}. "source" is the body of
38
- # an async TypeScript (or plain JavaScript) function called with spans
39
- # (each {name, index, hasData}, in start order, without its data), input,
40
- # and output. It reads span data with await named("span name") or await
41
- # span.data(), and returns "pass", "fail", or "defer" (hybrid only), or
42
- # {result, reason}, with one second of compute and the data of at most 50
43
- # spans loaded within ten seconds. "spanNames" lists every span a correct run is
44
- # expected to produce. Code that cannot run on the trace, or that returns
45
- # "defer" for a "code" assertion, is refused. Omit both to keep them, or
46
- # pass "kind" => "judge" to remove the code. Returned assertions carry
47
- # "kind", "code", and "codeBrokenReason".
48
- #
49
- # Send "kind" => "code" or "hybrid" without "code", on an assertion that
50
- # has no code yet, to have Bitfab write the code: the text is checked by
51
- # the guided rewrite (unless skipped), code of that kind is written and run
52
- # on the trace, and the save waits for both, so that entry's "rewrite" is
53
- # already final. Its returned "codeDraft" hash says what happened:
54
- # "requestedKind", "status" ("written", or "not_written" when no working
55
- # code could be written and the assertion was saved as "judge" without
56
- # code), and "reason" (why not, or nil). Every other entry's "codeDraft"
57
- # is nil. At most 10 assertions per save may ask for code this way, and a
58
- # save that asks for any waits up to five minutes.
32
+ # "code" is optional code that decides the assertion on a replay without
33
+ # the LLM judge: {"source" => ..., "spanNames" => [...]}. Send a "code"
34
+ # hash to set it, "code" => nil to remove it, or omit "code" to keep the
35
+ # current code. "source" is the body of an async TypeScript (or plain
36
+ # JavaScript) function called with spans (each {name, index, hasData}, in
37
+ # start order, without its data), input, and output. It reads span data
38
+ # with await named("span name") or await span.data(), with one second of
39
+ # compute and the data of at most 50 spans loaded within ten seconds. It
40
+ # returns "pass" or "fail", which decides the verdict with no judge call,
41
+ # or "defer" to hand the replay to the LLM judge, or {result, reason}. A
42
+ # defer may carry what the code found for the judge to read alongside the
43
+ # run: {result: "defer", reason, data}. A thrown error counts as "defer"
44
+ # with no data. "spanNames" lists every span a correct run is expected to
45
+ # produce. The code is run on the trace before saving and refused when it
46
+ # throws, times out, or returns anything other than a result. Returned
47
+ # assertions carry "code" (nil when the assertion has none) and
48
+ # "codeBrokenReason", which says why the code could not run on a replay.
59
49
  #
60
50
  # By default, after the save, Bitfab checks each assertion's text against its
61
51
  # trace. When the text bundles two claims, adds a condition, points at a
@@ -90,8 +80,7 @@ module Bitfab
90
80
  "updates" => updates.map { |update| with_new_category_key(update) },
91
81
  "source" => source,
92
82
  "guidedRewrite" => !skip_guided_rewrite
93
- },
94
- timeout: save_timeout(updates)
83
+ }
95
84
  )["assertions"]
96
85
  saved.is_a?(Array) ? saved.map { |assertion| with_both_category_keys(assertion) } : saved
97
86
  end
@@ -102,18 +91,6 @@ module Bitfab
102
91
 
103
92
  private
104
93
 
105
- def save_timeout(updates)
106
- asks = updates.any? do |update|
107
- (update["assertions"] || update[:assertions] || []).any? { |assertion| asks_bitfab_to_write_code?(assertion) }
108
- end
109
- asks ? CODE_DRAFT_SAVE_TIMEOUT_SECONDS : nil
110
- end
111
-
112
- def asks_bitfab_to_write_code?(assertion)
113
- kind = assertion["kind"] || assertion[:kind]
114
- CODED_KINDS.include?(kind) && !assertion.key?("code") && !assertion.key?(:code)
115
- end
116
-
117
94
  def assertions_path(trace_id, suffix = "")
118
95
  "/api/sdk/traces/#{URI.encode_www_form_component(trace_id).gsub("+", "%20")}/assertions#{suffix}"
119
96
  end
data/lib/bitfab/client.rb CHANGED
@@ -743,7 +743,7 @@ module Bitfab
743
743
  end
744
744
  end
745
745
  rescue => e
746
- error = e.message
746
+ error = ErrorChain.describe(e)
747
747
  complete.call(result, error)
748
748
  raise
749
749
  end
@@ -944,7 +944,7 @@ module Bitfab
944
944
  end
945
945
  complete.call(yielded, nil)
946
946
  rescue => e
947
- complete.call(yielded, e.message)
947
+ complete.call(yielded, ErrorChain.describe(e))
948
948
  raise
949
949
  ensure
950
950
  SpanContext.stack.pop
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ # Reads the chain of causes below an exception. A database driver error
5
+ # usually reaches a traced method wrapped one or two levels down, so
6
+ # recording only the top message makes a connection timeout read as a broken
7
+ # query. Spans and replay results carry the deepest cause too.
8
+ module ErrorChain
9
+ # How many causes below an exception are followed.
10
+ MAX_DEPTH = 10
11
+ CODE_METHODS = %i[code sqlstate errno].freeze
12
+
13
+ module_function
14
+
15
+ # The causes below `error`, nearest first, stopping at the depth limit or
16
+ # at a cause already seen.
17
+ def causes(error)
18
+ found = []
19
+ seen = {error.object_id => true}
20
+ current = error
21
+ while found.length < MAX_DEPTH
22
+ cause = current.cause
23
+ break if cause.nil? || seen[cause.object_id]
24
+
25
+ found << cause
26
+ seen[cause.object_id] = true
27
+ current = cause
28
+ end
29
+ found
30
+ end
31
+
32
+ # The exception's message, followed by the deepest cause's message when it
33
+ # wraps another exception, e.g. `Failed query: ... (caused by: connection
34
+ # timeout)`. The cause is left off when the message already contains it.
35
+ def describe(error)
36
+ message = error.message
37
+ deepest = causes(error).last
38
+ return message if deepest.nil?
39
+
40
+ deepest_message = deepest.message.to_s.strip
41
+ return message if deepest_message.empty? || message.include?(deepest_message)
42
+
43
+ "#{message} (caused by: #{deepest_message})"
44
+ end
45
+
46
+ # An exception as a JSON record: type, message, backtrace, the driver's
47
+ # code when it has one, and its causes nested under `cause` down to the
48
+ # depth limit. The block adds fields a specific exception type carries.
49
+ def serialize(error, &extra)
50
+ serialize_at(error, 1, {}, &extra)
51
+ end
52
+
53
+ # `seen` is created fresh by `serialize` for each top-level call, so one
54
+ # call never skips a cause another call already recorded.
55
+ def serialize_at(error, depth, seen, &extra)
56
+ seen[error.object_id] = true
57
+ serialized = {type: error.class.name, message: error.message, backtrace: error.backtrace}
58
+ code = error_code(error)
59
+ serialized[:code] = code unless code.nil?
60
+ serialized.merge!(extra.call(error)) if extra
61
+ cause = error.cause
62
+ if cause && !seen[cause.object_id] && depth < MAX_DEPTH
63
+ serialized[:cause] = serialize_at(cause, depth + 1, seen, &extra)
64
+ end
65
+ serialized
66
+ end
67
+ private_class_method :serialize_at
68
+
69
+ def error_code(error)
70
+ CODE_METHODS.each do |name|
71
+ next unless error.respond_to?(name)
72
+
73
+ value = begin
74
+ error.public_send(name)
75
+ rescue
76
+ nil
77
+ end
78
+ return value if value.is_a?(String) || value.is_a?(Integer)
79
+ end
80
+ nil
81
+ end
82
+ end
83
+ end
data/lib/bitfab/replay.rb CHANGED
@@ -160,13 +160,9 @@ module Bitfab
160
160
  def json_safe(value)
161
161
  case value
162
162
  when Exception
163
- serialized = {type: value.class.name, message: value.message, backtrace: value.backtrace}
164
- if value.is_a?(DbBranchReplayError)
165
- serialized[:code] = value.code
166
- serialized[:original_trace_id] = value.original_trace_id
167
- serialized[:cause] = json_safe(value.cause) if value.cause
163
+ ErrorChain.serialize(value) do |error|
164
+ error.is_a?(DbBranchReplayError) ? {original_trace_id: error.original_trace_id} : {}
168
165
  end
169
- serialized
170
166
  when Hash
171
167
  value.to_h { |key, child| [key, json_safe(child)] }
172
168
  when Array
@@ -177,7 +173,7 @@ module Bitfab
177
173
  end
178
174
 
179
175
  def replay_item_error_message(error)
180
- return error.message unless error.is_a?(DbBranchReplayError)
176
+ return ErrorChain.describe(error) unless error.is_a?(DbBranchReplayError)
181
177
 
182
178
  "Replay requested a database branch for trace #{error.original_trace_id} but it " \
183
179
  "could not be resolved (#{error.code}): #{error.message}. The method was not run, " \
@@ -1528,7 +1524,7 @@ module Bitfab
1528
1524
  input: kwargs.empty? ? args : [*args, kwargs],
1529
1525
  result: fn_result,
1530
1526
  original_output: item["output"],
1531
- error: item_error&.message,
1527
+ error: item_error && ErrorChain.describe(item_error),
1532
1528
  trace_error: fn_error,
1533
1529
  replay_error:,
1534
1530
  duration_ms: replay_duration_ms,
@@ -740,7 +740,7 @@ module Bitfab
740
740
  return unless entry.is_a?(OpenSpan)
741
741
 
742
742
  exception = event.raised_exception
743
- entry.error = "#{exception.class}: #{exception.message}"
743
+ entry.error = "#{exception.class}: #{ErrorChain.describe(exception)}"
744
744
  end
745
745
 
746
746
  def clear_rescued_error(event)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.75.1"
4
+ VERSION = "0.75.3"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
  require_relative "bitfab/version"
6
6
  require_relative "bitfab/constants"
7
7
  require_relative "bitfab/warn_once"
8
+ require_relative "bitfab/error_chain"
8
9
  require_relative "bitfab/experiment_id"
9
10
  require_relative "bitfab/payload_budget"
10
11
  require_relative "bitfab/serialize"
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.75.1
4
+ version: 0.75.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -183,6 +183,7 @@ files:
183
183
  - lib/bitfab/db_snapshot.rb
184
184
  - lib/bitfab/detached_trace.rb
185
185
  - lib/bitfab/env.rb
186
+ - lib/bitfab/error_chain.rb
186
187
  - lib/bitfab/experiment_id.rb
187
188
  - lib/bitfab/experiments.rb
188
189
  - lib/bitfab/git_command.rb