debugbundle 1.5.0 → 2.0.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 +15 -5
- data/lib/debugbundle/acknowledgement.rb +6 -2
- data/lib/debugbundle/before_send.rb +1 -1
- data/lib/debugbundle/client.rb +193 -71
- data/lib/debugbundle/client_event_support.rb +76 -43
- data/lib/debugbundle/client_process_support.rb +64 -0
- data/lib/debugbundle/client_queue_support.rb +183 -0
- data/lib/debugbundle/config_worker.rb +60 -0
- data/lib/debugbundle/delivery_worker.rb +103 -0
- data/lib/debugbundle/redaction.rb +7 -8
- data/lib/debugbundle/safe_input.rb +40 -0
- data/lib/debugbundle/suppression.rb +62 -20
- data/lib/debugbundle/telemetry_privacy.rb +15 -5
- data/lib/debugbundle/transport.rb +6 -2
- data/lib/debugbundle/version.rb +1 -1
- metadata +7 -2
|
@@ -11,6 +11,7 @@ module DebugBundle
|
|
|
11
11
|
LOOP_RESET_AFTER_SECONDS = 60.0
|
|
12
12
|
LOOP_CHECKPOINT_SECONDS = 30.0
|
|
13
13
|
MAX_NORMAL_EVENTS_PER_WINDOW = 3
|
|
14
|
+
MAX_TRACKED_FINGERPRINTS = 2_048
|
|
14
15
|
|
|
15
16
|
State = Struct.new(
|
|
16
17
|
:window_started_at,
|
|
@@ -29,14 +30,28 @@ module DebugBundle
|
|
|
29
30
|
class Tracker
|
|
30
31
|
def initialize
|
|
31
32
|
@states = {}
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
@overflow_count = 0
|
|
35
|
+
@overflow_first_seen_at = nil
|
|
36
|
+
@overflow_last_seen_at = nil
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
def should_capture(key, now:)
|
|
35
|
-
|
|
40
|
+
locked = @mutex.try_lock
|
|
41
|
+
return true unless locked
|
|
42
|
+
|
|
43
|
+
fingerprint = Digest::SHA256.hexdigest(key)
|
|
44
|
+
state = @states.delete(fingerprint)
|
|
45
|
+
if state.nil? && @states.length >= MAX_TRACKED_FINGERPRINTS
|
|
46
|
+
_, evicted = @states.shift
|
|
47
|
+
record_overflow(evicted) if evicted.pending_suppressed_count.positive?
|
|
48
|
+
end
|
|
49
|
+
state ||= new_state(now)
|
|
50
|
+
@states[fingerprint] = state
|
|
36
51
|
|
|
37
52
|
if state.suppression_mode && (now - state.last_seen_at) >= LOOP_RESET_AFTER_SECONDS
|
|
38
|
-
@states[
|
|
39
|
-
state = @states[
|
|
53
|
+
@states[fingerprint] = new_state(now)
|
|
54
|
+
state = @states[fingerprint]
|
|
40
55
|
end
|
|
41
56
|
|
|
42
57
|
if (now - state.window_started_at) >= DUPLICATE_WINDOW_SECONDS
|
|
@@ -65,31 +80,58 @@ module DebugBundle
|
|
|
65
80
|
|
|
66
81
|
mark_suppressed(state, now)
|
|
67
82
|
false
|
|
83
|
+
ensure
|
|
84
|
+
@mutex.unlock if locked
|
|
68
85
|
end
|
|
69
86
|
|
|
70
87
|
def drain_aggregates(now:)
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
aggregates = @states.each_with_object([]) do |(fingerprint, state), results|
|
|
90
|
+
next if state.pending_suppressed_count.zero?
|
|
91
|
+
next if state.pending_first_seen_at.nil? || state.pending_last_seen_at.nil?
|
|
92
|
+
next if checkpoint_not_due?(state, now)
|
|
93
|
+
|
|
94
|
+
results << {
|
|
95
|
+
'fingerprint' => fingerprint,
|
|
96
|
+
'suppressed_count' => state.pending_suppressed_count,
|
|
97
|
+
'first_seen' => Time.at(state.pending_first_seen_at).utc.iso8601,
|
|
98
|
+
'last_seen' => Time.at(state.pending_last_seen_at).utc.iso8601,
|
|
99
|
+
'window_seconds' => DUPLICATE_WINDOW_SECONDS.to_i
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
state.pending_suppressed_count = 0
|
|
103
|
+
state.pending_first_seen_at = nil
|
|
104
|
+
state.pending_last_seen_at = nil
|
|
105
|
+
state.last_aggregate_emitted_at = now
|
|
106
|
+
end
|
|
107
|
+
append_overflow_aggregate(aggregates)
|
|
108
|
+
aggregates
|
|
88
109
|
end
|
|
89
110
|
end
|
|
90
111
|
|
|
91
112
|
private
|
|
92
113
|
|
|
114
|
+
def record_overflow(state)
|
|
115
|
+
@overflow_first_seen_at ||= state.pending_first_seen_at
|
|
116
|
+
@overflow_count += state.pending_suppressed_count
|
|
117
|
+
@overflow_last_seen_at = state.pending_last_seen_at
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def append_overflow_aggregate(aggregates)
|
|
121
|
+
return if @overflow_count.zero?
|
|
122
|
+
|
|
123
|
+
aggregates << {
|
|
124
|
+
'fingerprint' => Digest::SHA256.hexdigest('ruby:suppression-state-pressure'),
|
|
125
|
+
'suppressed_count' => @overflow_count,
|
|
126
|
+
'first_seen' => Time.at(@overflow_first_seen_at).utc.iso8601,
|
|
127
|
+
'last_seen' => Time.at(@overflow_last_seen_at).utc.iso8601,
|
|
128
|
+
'window_seconds' => DUPLICATE_WINDOW_SECONDS.to_i
|
|
129
|
+
}
|
|
130
|
+
@overflow_count = 0
|
|
131
|
+
@overflow_first_seen_at = nil
|
|
132
|
+
@overflow_last_seen_at = nil
|
|
133
|
+
end
|
|
134
|
+
|
|
93
135
|
def new_state(now)
|
|
94
136
|
State.new(
|
|
95
137
|
window_started_at: now,
|
|
@@ -14,6 +14,11 @@ module DebugBundle
|
|
|
14
14
|
verification_code authorization cookie ssn client_secret x_api_key set_cookie
|
|
15
15
|
proxy_authorization accessToken refreshToken privateKey clientSecret
|
|
16
16
|
].freeze
|
|
17
|
+
ASSIGNMENT_VALUE_PATTERN = '(?:"[^"]*"|\'[^\']*\'|[^\s&,;]+)'
|
|
18
|
+
DEFAULT_ASSIGNMENT_PATTERN = Regexp.new(
|
|
19
|
+
"\\b(#{FIELDS.map { |field| Regexp.escape(field) }.join('|')})\\b([\"']?\\s*[:=]\\s*)#{ASSIGNMENT_VALUE_PATTERN}",
|
|
20
|
+
Regexp::IGNORECASE
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
module_function
|
|
19
24
|
|
|
@@ -25,7 +30,8 @@ module DebugBundle
|
|
|
25
30
|
|
|
26
31
|
work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
|
|
27
32
|
fields: (FIELDS + additional_fields).map { |field| canonical(field) },
|
|
28
|
-
extra: additional_fields
|
|
33
|
+
extra: additional_fields,
|
|
34
|
+
assignment_pattern: assignment_pattern(additional_fields) }
|
|
29
35
|
result = visit(value, work, 0, true)
|
|
30
36
|
raise ArgumentError, 'budget_exceeded' if JSON.generate(result).bytesize > MAX_BYTES
|
|
31
37
|
|
|
@@ -48,6 +54,13 @@ module DebugBundle
|
|
|
48
54
|
value.downcase.gsub(/[^a-z0-9]/, '')
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
def assignment_pattern(additional_fields)
|
|
58
|
+
return DEFAULT_ASSIGNMENT_PATTERN if additional_fields.empty?
|
|
59
|
+
|
|
60
|
+
terms = (FIELDS + additional_fields).map { |field| Regexp.escape(field) }.join('|')
|
|
61
|
+
Regexp.new("\\b(#{terms})\\b([\"']?\\s*[:=]\\s*)#{ASSIGNMENT_VALUE_PATTERN}", Regexp::IGNORECASE)
|
|
62
|
+
end
|
|
63
|
+
|
|
51
64
|
def sensitive_key?(key, fields)
|
|
52
65
|
segments = key.gsub(/([a-z0-9])([A-Z])/, '\\1_\\2').downcase.split(/[^a-z0-9]+/)
|
|
53
66
|
segments.each_index.any? do |start|
|
|
@@ -151,10 +164,7 @@ module DebugBundle
|
|
|
151
164
|
result = result.gsub(/\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/i, '\\1: [REDACTED]')
|
|
152
165
|
result = result.gsub(%r{\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{6,}}i, '\\1 [REDACTED]')
|
|
153
166
|
result = result.gsub(/\bdbundle_(?:proj|mem|probe|agent)_[A-Za-z0-9_-]+\b/, REDACTED)
|
|
154
|
-
|
|
155
|
-
pattern = /\b(#{Regexp.escape(field)})\b(["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s&,;]+)/i
|
|
156
|
-
result = result.gsub(pattern, '\\1\\2[REDACTED]')
|
|
157
|
-
end
|
|
167
|
+
result = result.gsub(work[:assignment_pattern], '\\1\\2[REDACTED]')
|
|
158
168
|
result = result.gsub(/(?<![A-Za-z0-9_-])(?:[0-9][ -]?){12,18}[0-9](?![A-Za-z0-9_-])/) do |candidate|
|
|
159
169
|
valid_card?(candidate) ? REDACTED : candidate
|
|
160
170
|
end
|
|
@@ -4,6 +4,7 @@ require 'json'
|
|
|
4
4
|
require 'net/http'
|
|
5
5
|
require 'securerandom'
|
|
6
6
|
require 'uri'
|
|
7
|
+
require 'time'
|
|
7
8
|
require 'fileutils'
|
|
8
9
|
|
|
9
10
|
module DebugBundle
|
|
@@ -83,8 +84,11 @@ module DebugBundle
|
|
|
83
84
|
def parse_retry_after(value)
|
|
84
85
|
return nil if value.nil? || value.strip.empty?
|
|
85
86
|
|
|
86
|
-
seconds =
|
|
87
|
-
seconds.
|
|
87
|
+
seconds = Float(value, exception: false)
|
|
88
|
+
seconds = Time.httpdate(value) - Time.now if seconds.nil?
|
|
89
|
+
return nil unless seconds.finite?
|
|
90
|
+
|
|
91
|
+
seconds.clamp(0, RETRY_AFTER_CAP_SECONDS).to_i
|
|
88
92
|
rescue ArgumentError, TypeError
|
|
89
93
|
nil
|
|
90
94
|
end
|
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:
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- DebugBundle
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -51,7 +51,11 @@ files:
|
|
|
51
51
|
- lib/debugbundle/before_send.rb
|
|
52
52
|
- lib/debugbundle/client.rb
|
|
53
53
|
- lib/debugbundle/client_event_support.rb
|
|
54
|
+
- lib/debugbundle/client_process_support.rb
|
|
55
|
+
- lib/debugbundle/client_queue_support.rb
|
|
54
56
|
- lib/debugbundle/config.rb
|
|
57
|
+
- lib/debugbundle/config_worker.rb
|
|
58
|
+
- lib/debugbundle/delivery_worker.rb
|
|
55
59
|
- lib/debugbundle/logging.rb
|
|
56
60
|
- lib/debugbundle/rack/middleware.rb
|
|
57
61
|
- lib/debugbundle/rack/relay_middleware.rb
|
|
@@ -63,6 +67,7 @@ files:
|
|
|
63
67
|
- lib/debugbundle/relay/handler.rb
|
|
64
68
|
- lib/debugbundle/remote_config.rb
|
|
65
69
|
- lib/debugbundle/runtime.rb
|
|
70
|
+
- lib/debugbundle/safe_input.rb
|
|
66
71
|
- lib/debugbundle/sidekiq/server_middleware.rb
|
|
67
72
|
- lib/debugbundle/suppression.rb
|
|
68
73
|
- lib/debugbundle/telemetry_privacy.rb
|