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 +4 -4
- data/README.md +8 -5
- data/lib/debugbundle/client.rb +9 -0
- data/lib/debugbundle/client_exception_support.rb +12 -2
- data/lib/debugbundle/exception_identity.rb +42 -0
- data/lib/debugbundle/logging.rb +2 -1
- data/lib/debugbundle/telemetry_privacy.rb +34 -4
- data/lib/debugbundle/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25d32f695274543c343f714a99ec12668bb42c23c608c07469a6a0fb6d34d2c9
|
|
4
|
+
data.tar.gz: 42bb8c82354c4b8b06a81f8b4e462fbd1028c23650aff6d36cb28b5402781d5d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
23
|
+
## Evidence in 2.1.0
|
|
20
24
|
|
|
21
|
-
|
|
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.
|
|
122
|
+
create a delivered marker. Installed/runtime qualification passed before publication.
|
|
120
123
|
|
|
121
124
|
## Automatic capture and application filtering
|
|
122
125
|
|
data/lib/debugbundle/client.rb
CHANGED
|
@@ -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
|
data/lib/debugbundle/logging.rb
CHANGED
|
@@ -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
|
-
|
|
135
|
+
policy = key_policy(name, work)
|
|
136
|
+
next if policy == :omit
|
|
119
137
|
|
|
120
|
-
result[name] =
|
|
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 =
|
|
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
|
data/lib/debugbundle/version.rb
CHANGED
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.
|
|
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-
|
|
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
|