debugbundle 2.1.0 → 2.1.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: fa08dbcf220cb84131821cf546272d4d08d6f15f01ba4e8c7f41f77fbc53a1c0
4
- data.tar.gz: fe1a46ab795df53b5dd1ac5bee6684b43ea35501d031a345b52c576e9a8881f9
3
+ metadata.gz: 25d32f695274543c343f714a99ec12668bb42c23c608c07469a6a0fb6d34d2c9
4
+ data.tar.gz: 42bb8c82354c4b8b06a81f8b4e462fbd1028c23650aff6d36cb28b5402781d5d
5
5
  SHA512:
6
- metadata.gz: 69e7d24b4c4615dc8ff2da958c90427c0a19d9f781629ff31f9e2918912e97f4298d6a44bba5fd2f19e56cef5fcb426796ed26e7f71b73eefb6384eaa26e0ca7
7
- data.tar.gz: 190296bc690b90b3113a59b99c7062ed5f3f95705bb633b4998577d884743f5f64570a727ca81334fb45fe7c3d02f13be4d38cda714244cca79998ad94a2887d
6
+ metadata.gz: a9086dd9cacdc78a475f77b0bbb84b247849a360f98db7d5a3024f9e653b0c14f9b513147526a04218ad67b2917db06ba0f85a729ec17996e174c6abfe43cac1
7
+ data.tar.gz: 34aa039ea542b4c395d25ea5e51b8222d0a00699dbb89caaacc08ece8077f074786615a65bf7b918babdd385e3dba5f7e89b867a2a7dad2d03eae4958e5bc4fb
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # debugbundle
2
2
 
3
+ Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
4
+
3
5
  Ruby SDK for DebugBundle.
4
6
 
5
7
  Use this gem to capture Ruby backend exceptions, request metadata, structured logs, runtime context, probe data, and browser relay traffic. It supports a singleton facade plus instance clients for Rack, Rails, Sidekiq, and explicit Ruby instrumentation.
@@ -8,7 +10,9 @@ The source test matrix covers Ruby 3.1 through 4.0. Rails 7.x remains an install
8
10
 
9
11
  Version 2.0 changes capture and hook timing. Existing installed 1.5.x applications continue to use their pinned gem until upgraded. Review [the migration guide](MIGRATION-2.0.md) before upgrading.
10
12
 
11
- `make sdk-safety-perf` runs a Docker-backed host-safety budget with 10,000 filtered INFO calls, 1,000 accepted ERROR calls with and without custom privacy fields, and 10,000 concurrent ERROR calls against a full queue while transport is held. CI and the release workflow run this gate before publication. These bounded synthetic checks supplement, but do not replace, installed runtime and workload qualification.
13
+ `make sdk-safety-perf` runs a Docker-backed host-safety budget with 10,000 filtered INFO calls, 1,000 accepted ERROR calls with and without custom privacy fields, and 10,000 concurrent ERROR calls against a full queue while transport is held. It also bounds 200 enriched exceptions and 1,000 recovery observations. CI and the release workflow run this gate before publication. These bounded synthetic checks supplement, but do not replace, installed runtime and workload qualification.
14
+
15
+ Mandatory privacy checks reuse at most 256 field-name decisions and 256 unchanged scalar strings of at most 128 bytes within each projection. Returned values remain independently owned. Every value and repeated key still counts against the existing resource budgets; both caches are discarded before the next event or policy.
12
16
 
13
17
  `make evidence-performance` compares the same exception/recovery calls with
14
18
  evidence disabled and enabled, including a full warning queue. It reports capture
@@ -16,10 +20,9 @@ time, allocated Ruby objects and retained bytes, and verifies original-error
16
20
  priority and the existing queue/diagnostic ceilings. Setup and cleanup are excluded;
17
21
  the existing `sdk-safety-perf` timing limits remain unchanged.
18
22
 
19
- ## Unreleased evidence candidate
23
+ ## Evidence in 2.1.0
20
24
 
21
- The working candidate adds structured exception evidence and explicit recovery;
22
- it is not a published release. Enrichment requires a valid capability descriptor
25
+ Version 2.1.0 adds structured exception evidence and explicit recovery. Enrichment requires a valid capability descriptor
23
26
  from the destination on an existing config/ingestion response. No extra discovery
24
27
  request is made. Unknown paths keep ordinary exception capture. The optional
25
28
  `evidence_capabilities: { version: 1, features: [...] }` pin asserts that every
@@ -116,7 +119,7 @@ Rack and Rails browser relays preserve validated evidence and server-owned
116
119
  credentials. Downstream chunks account for server metadata expansion; a durable
117
120
  spool is marked delivered only when every chunk has a complete canonical
118
121
  acknowledgement. Legacy custom transport success remains compatible and cannot
119
- create a delivered marker. Final installed/runtime qualification is in progress.
122
+ create a delivered marker. Installed/runtime qualification passed before publication.
120
123
 
121
124
  ## Automatic capture and application filtering
122
125
 
@@ -149,6 +149,15 @@ module DebugBundle
149
149
  normalized_level = normalize_level(level || :warning)
150
150
  return unless level_enabled?(normalized_level)
151
151
 
152
+ error = message.is_a?(Exception) ? message : context.is_a?(Hash) && (context[:exception] || context['exception'])
153
+ if %i[error critical].include?(normalized_level) && error.is_a?(Exception)
154
+ fields = (context || {}).reject { |key, _value| %w[exception].include?(key.to_s) }
155
+ fields[:logger_name] ||= 'logger'
156
+ fields[:log_message] = message if message.is_a?(String)
157
+ capture_exception(error, context: fields)
158
+ return
159
+ end
160
+
152
161
  priority = LOG_LEVEL_RANKS.fetch(normalized_level) >= LOG_LEVEL_RANKS.fetch(:error) ? 2 : 0
153
162
  return unless preflight_capacity?(priority, normalized_level.to_s)
154
163
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'debugbundle/exception_identity'
4
+
3
5
  module DebugBundle
4
6
  class Client
5
7
  module ExceptionSupport
@@ -9,9 +11,15 @@ module DebugBundle
9
11
  return unless capture_enabled?
10
12
  return unless preflight_capacity?(3, 'exception')
11
13
 
14
+ merged_context = evidence_context(merge_context(context))
15
+ @exception_identity ||= ExceptionIdentity.new
16
+ logged = merged_context.key?('logger_name')
17
+ automatic = !run_before_send || logged
18
+ scope = config.before_send ? nil : @operations.scope
19
+ return unless @exception_identity.admit?(error, scope, automatic)
20
+
12
21
  request_remote_config_poll_if_due
13
22
 
14
- merged_context = evidence_context(merge_context(context))
15
23
  payload = {
16
24
  'name' => safe_exception_name(error),
17
25
  'message' => safe_exception_message(error),
@@ -45,7 +53,9 @@ module DebugBundle
45
53
  return unless @suppression.should_capture(suppression_key, now: monotonic_now)
46
54
 
47
55
  event = enrich_exception(event, retained, causes, fatal: !run_before_send)
48
- enqueue_event(event, skip_before_send: !run_before_send)
56
+ return unless enqueue_event(event, skip_before_send: !run_before_send)
57
+
58
+ @exception_identity.admit?(error, scope, automatic, accepted: true)
49
59
  end
50
60
  end
51
61
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weakref'
4
+
5
+ module DebugBundle
6
+ class ExceptionIdentity
7
+ NATIVE_EQUAL = BasicObject.instance_method(:equal?)
8
+
9
+ def initialize
10
+ @entries = []
11
+ @lock = Mutex.new
12
+ end
13
+
14
+ def admit?(error, scope, automatic, accepted: false)
15
+ return true if scope.nil?
16
+ return true unless @lock.try_lock
17
+
18
+ begin
19
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ @entries.reject! { |entry| !entry[0].weakref_alive? || now - entry[3] >= 30 }
21
+ @entries.each do |entry|
22
+ next unless NATIVE_EQUAL.bind_call(entry[0].__getobj__, error) && entry[1] == scope
23
+
24
+ if !accepted && automatic != entry[2]
25
+ @entries.delete(entry)
26
+ return false
27
+ end
28
+ @entries.delete(entry) if accepted
29
+ end
30
+ if accepted
31
+ @entries.shift if @entries.length >= 128
32
+ @entries << [WeakRef.new(error), scope, automatic, now]
33
+ end
34
+ true
35
+ rescue WeakRef::RefError
36
+ true
37
+ ensure
38
+ @lock.unlock
39
+ end
40
+ end
41
+ end
42
+ end
@@ -44,7 +44,7 @@ module DebugBundle
44
44
  was_capturing = Thread.current[RECURSION_GUARD_KEY]
45
45
  Thread.current[RECURSION_GUARD_KEY] = true
46
46
  client.capture_log(
47
- message.to_s,
47
+ message.is_a?(Exception) ? message : message.to_s,
48
48
  level: LOGGER_LEVEL_NAMES.fetch(severity, :warning),
49
49
  context: { logger_name: progname }
50
50
  )
@@ -79,6 +79,7 @@ module DebugBundle
79
79
  level: log.level || :info,
80
80
  context: {
81
81
  logger_name: log.name,
82
+ exception: log.respond_to?(:exception) ? log.exception : nil,
82
83
  payload: log.payload,
83
84
  tags: log.tags
84
85
  }
@@ -28,7 +28,7 @@ module DebugBundle
28
28
  raise ArgumentError, 'unsafe_input'
29
29
  end
30
30
 
31
- work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
31
+ work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity, key_policy: {}, safe_text: {},
32
32
  fields: if additional_fields.empty?
33
33
  CANONICAL_FIELDS
34
34
  else
@@ -80,6 +80,23 @@ module DebugBundle
80
80
  end
81
81
  end
82
82
 
83
+ def key_policy(name, work)
84
+ cached = work[:key_policy][name]
85
+ return cached if cached
86
+
87
+ policy = if scrub_text(name, work) != name
88
+ :omit
89
+ elsif sensitive_key?(name, work[:fields])
90
+ :redact
91
+ else
92
+ :retain
93
+ end
94
+ # Stack frames repeat the same keys. Cache decisions only in this bounded
95
+ # projection; values, byte accounting and later policies remain independent.
96
+ work[:key_policy][name] = policy if work[:key_policy].size < 256
97
+ policy
98
+ end
99
+
83
100
  def visit(value, work, depth, structured)
84
101
  work[:nodes] += 1
85
102
  raise ArgumentError, 'budget_exceeded' if work[:nodes] > 4096
@@ -115,9 +132,10 @@ module DebugBundle
115
132
  next if name.bytesize > 128
116
133
 
117
134
  count_bytes(work, name)
118
- next if scrub_text(name, work) != name
135
+ policy = key_policy(name, work)
136
+ next if policy == :omit
119
137
 
120
- result[name] = sensitive_key?(name, work[:fields]) ? REDACTED : visit(nested, work, depth + 1, structured)
138
+ result[name] = policy == :redact ? REDACTED : visit(nested, work, depth + 1, structured)
121
139
  end
122
140
  end
123
141
  ensure
@@ -142,13 +160,25 @@ module DebugBundle
142
160
  # Malformed structured content with a credential label is withheld below.
143
161
  end
144
162
  end
145
- result = scrub_text(value, work)
163
+ result = scrub_value_text(value, work)
146
164
  return REDACTED if result == value && value.start_with?('{', '[') &&
147
165
  /(?:password|token|secret|authorization|cookie)["']?\s*[:=]/i.match?(value)
148
166
 
149
167
  result
150
168
  end
151
169
 
170
+ def scrub_value_text(value, work)
171
+ # Only short, unchanged scalar text is reusable. Structured containers and
172
+ # redacted values always take the full path; every return owns its string.
173
+ cacheable = value.bytesize <= 128 && !value.start_with?('{', '[')
174
+ cached = work[:safe_text][value] if cacheable
175
+ return cached.dup if cached
176
+
177
+ result = scrub_text(value, work)
178
+ work[:safe_text][value] = result.dup.freeze if cacheable && result == value && work[:safe_text].size < 256
179
+ result
180
+ end
181
+
152
182
  def valid_card?(candidate)
153
183
  digits = candidate.delete(' -')
154
184
  return false unless digits.length.between?(13, 19) && digits.chars.uniq.length > 1
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugBundle
4
- VERSION = '2.1.0'
4
+ VERSION = '2.1.1'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: debugbundle
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - DebugBundle
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-11-01 00:00:00.000000000 Z
10
+ date: 2026-11-06 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64
@@ -70,6 +70,7 @@ files:
70
70
  - lib/debugbundle/evidence_value.rb
71
71
  - lib/debugbundle/evidence_wire.rb
72
72
  - lib/debugbundle/exception_evidence.rb
73
+ - lib/debugbundle/exception_identity.rb
73
74
  - lib/debugbundle/framework_operation.rb
74
75
  - lib/debugbundle/logging.rb
75
76
  - lib/debugbundle/operation_context.rb