debugbundle 1.5.0 → 2.0.0

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.
@@ -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
- state = (@states[key] ||= new_state(now))
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[key] = new_state(now)
39
- state = @states[key]
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
- @states.each_with_object([]) do |(key, state), aggregates|
72
- next if state.pending_suppressed_count.zero?
73
- next if state.pending_first_seen_at.nil? || state.pending_last_seen_at.nil?
74
- next if checkpoint_not_due?(state, now)
75
-
76
- aggregates << {
77
- 'fingerprint' => Digest::SHA256.hexdigest(key),
78
- 'suppressed_count' => state.pending_suppressed_count,
79
- 'first_seen' => Time.at(state.pending_first_seen_at).utc.iso8601,
80
- 'last_seen' => Time.at(state.pending_last_seen_at).utc.iso8601,
81
- 'window_seconds' => DUPLICATE_WINDOW_SECONDS.to_i
82
- }
83
-
84
- state.pending_suppressed_count = 0
85
- state.pending_first_seen_at = nil
86
- state.pending_last_seen_at = nil
87
- state.last_aggregate_emitted_at = now
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
- (FIELDS + work[:extra]).each do |field|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugBundle
4
- VERSION = '1.5.0'
4
+ VERSION = '2.0.0'
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: 1.5.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DebugBundle
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-21 00:00:00.000000000 Z
10
+ date: 2026-09-25 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