debugbundle 1.4.1 → 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.
- checksums.yaml +4 -4
- data/README.md +11 -5
- data/lib/debugbundle/before_send.rb +1 -1
- data/lib/debugbundle/client.rb +189 -65
- data/lib/debugbundle/client_event_support.rb +96 -38
- 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/rack/middleware.rb +13 -3
- data/lib/debugbundle/redaction.rb +7 -8
- data/lib/debugbundle/relay/handler.rb +9 -1
- data/lib/debugbundle/safe_input.rb +40 -0
- data/lib/debugbundle/suppression.rb +62 -20
- data/lib/debugbundle/telemetry_privacy.rb +204 -0
- data/lib/debugbundle/version.rb +1 -1
- data/lib/debugbundle.rb +1 -0
- metadata +8 -23
- data/Gemfile +0 -17
- data/Makefile +0 -58
- data/debugbundle.gemspec +0 -30
- data/spec/before_send_spec.rb +0 -74
- data/spec/client_spec.rb +0 -432
- data/spec/debugbundle_spec.rb +0 -54
- data/spec/file_transport_spec.rb +0 -54
- data/spec/fixtures/event-envelope.schema.json +0 -142
- data/spec/logger_integration_spec.rb +0 -202
- data/spec/rack_integration_spec.rb +0 -44
- data/spec/rack_middleware_spec.rb +0 -210
- data/spec/rails_railtie_spec.rb +0 -96
- data/spec/rails_relay_spec.rb +0 -121
- data/spec/redaction_spec.rb +0 -42
- data/spec/relay_spec.rb +0 -274
- data/spec/remote_config_spec.rb +0 -80
- data/spec/repository_metadata_spec.rb +0 -65
- data/spec/sidekiq_integration_spec.rb +0 -66
- data/spec/sidekiq_middleware_spec.rb +0 -50
- data/spec/spec_helper.rb +0 -23
- data/spec/suppression_spec.rb +0 -16
|
@@ -182,7 +182,7 @@ module DebugBundle
|
|
|
182
182
|
payload = candidate['payload']
|
|
183
183
|
return nil unless service.is_a?(Hash) && payload.is_a?(Hash)
|
|
184
184
|
|
|
185
|
-
{
|
|
185
|
+
sanitized = {
|
|
186
186
|
'schema_version' => candidate['schema_version'].to_s,
|
|
187
187
|
'event_id' => candidate['event_id'].to_s,
|
|
188
188
|
'event_type' => event_type,
|
|
@@ -197,6 +197,14 @@ module DebugBundle
|
|
|
197
197
|
'payload' => payload,
|
|
198
198
|
'project_token' => @project_token
|
|
199
199
|
}
|
|
200
|
+
return nil unless TelemetryPrivacy.safe_event_identity?(sanitized)
|
|
201
|
+
|
|
202
|
+
fields = TelemetryPrivacy.protect({ 'service' => sanitized['service'], 'payload' => sanitized['payload'] })
|
|
203
|
+
return nil unless fields['service'].is_a?(Hash) && fields['payload'].is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
sanitized.merge('service' => fields['service'], 'payload' => fields['payload'])
|
|
206
|
+
rescue StandardError
|
|
207
|
+
nil
|
|
200
208
|
end
|
|
201
209
|
|
|
202
210
|
def sanitize_correlation(value, event_type)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module DebugBundle
|
|
6
|
+
# Built-in scalar extraction for capture paths that must not run application renderers.
|
|
7
|
+
module SafeInput
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def key(value)
|
|
11
|
+
is_a = Object.instance_method(:is_a?)
|
|
12
|
+
return String.instance_method(:[]).bind_call(value, 0, 128) if is_a.bind_call(value, String)
|
|
13
|
+
return Symbol.instance_method(:to_s).bind_call(value)[0, 128] if is_a.bind_call(value, Symbol)
|
|
14
|
+
|
|
15
|
+
if is_a.bind_call(value, Integer) && Integer.instance_method(:bit_length).bind_call(value) <= 512
|
|
16
|
+
return Integer.instance_method(:to_s).bind_call(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
nil
|
|
20
|
+
rescue StandardError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def request_path(value)
|
|
25
|
+
return '/' unless Object.instance_method(:is_a?).bind_call(value, String)
|
|
26
|
+
|
|
27
|
+
raw = String.instance_method(:[]).bind_call(value, 0, 2_048)
|
|
28
|
+
begin
|
|
29
|
+
uri = URI.parse(raw)
|
|
30
|
+
return uri.path if uri.path && !uri.path.empty?
|
|
31
|
+
rescue URI::InvalidURIError
|
|
32
|
+
# Fall through to the bounded path-only fallback.
|
|
33
|
+
end
|
|
34
|
+
fallback = raw.split('?', 2).first.to_s.split('#', 2).first
|
|
35
|
+
fallback.start_with?('/') && !fallback.empty? ? fallback : '/'
|
|
36
|
+
rescue StandardError
|
|
37
|
+
'/'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -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,
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module DebugBundle
|
|
7
|
+
# Mandatory bounded policy for application data; protocol-owned identifiers stay outside this projection.
|
|
8
|
+
module TelemetryPrivacy
|
|
9
|
+
REDACTED = '[REDACTED]'
|
|
10
|
+
MAX_BYTES = 262_144
|
|
11
|
+
FIELDS = %w[
|
|
12
|
+
password secret token api_key apikey access_token refresh_token private_key passwd
|
|
13
|
+
card_number credit_card cvv cvc pin expiry phone bearer session_id otp
|
|
14
|
+
verification_code authorization cookie ssn client_secret x_api_key set_cookie
|
|
15
|
+
proxy_authorization accessToken refreshToken privateKey clientSecret
|
|
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
|
+
)
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def protect(value, additional_fields: [])
|
|
26
|
+
raise ArgumentError, 'unsafe_input' if additional_fields.length > 128
|
|
27
|
+
unless additional_fields.all? { |field| field.is_a?(String) && field.length.between?(1, 64) }
|
|
28
|
+
raise ArgumentError, 'unsafe_input'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
|
|
32
|
+
fields: (FIELDS + additional_fields).map { |field| canonical(field) },
|
|
33
|
+
extra: additional_fields,
|
|
34
|
+
assignment_pattern: assignment_pattern(additional_fields) }
|
|
35
|
+
result = visit(value, work, 0, true)
|
|
36
|
+
raise ArgumentError, 'budget_exceeded' if JSON.generate(result).bytesize > MAX_BYTES
|
|
37
|
+
|
|
38
|
+
result
|
|
39
|
+
rescue JSON::GeneratorError, Encoding::InvalidByteSequenceError
|
|
40
|
+
raise ArgumentError, 'unsafe_input'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def safe_event_identity?(event, additional_fields: [])
|
|
44
|
+
correlation = event['correlation'] || {}
|
|
45
|
+
return false unless correlation.is_a?(Hash) && correlation.size <= 8
|
|
46
|
+
|
|
47
|
+
values = event.values_at('schema_version', 'sdk_name', 'sdk_version') + correlation.values
|
|
48
|
+
values.all? do |value|
|
|
49
|
+
value.nil? || (value.is_a?(String) && protect(value, additional_fields: additional_fields) == value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def canonical(value)
|
|
54
|
+
value.downcase.gsub(/[^a-z0-9]/, '')
|
|
55
|
+
end
|
|
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
|
+
|
|
64
|
+
def sensitive_key?(key, fields)
|
|
65
|
+
segments = key.gsub(/([a-z0-9])([A-Z])/, '\\1_\\2').downcase.split(/[^a-z0-9]+/)
|
|
66
|
+
segments.each_index.any? do |start|
|
|
67
|
+
combined = +''
|
|
68
|
+
segments.drop(start).any? do |segment|
|
|
69
|
+
combined << segment
|
|
70
|
+
fields.include?(combined)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def visit(value, work, depth, structured)
|
|
76
|
+
work[:nodes] += 1
|
|
77
|
+
raise ArgumentError, 'budget_exceeded' if work[:nodes] > 4096
|
|
78
|
+
return REDACTED if depth > 16
|
|
79
|
+
|
|
80
|
+
case value
|
|
81
|
+
when String then string(value, work, structured)
|
|
82
|
+
when Hash, Array then collection(value, work, depth, structured)
|
|
83
|
+
when NilClass, TrueClass, FalseClass, Integer then value
|
|
84
|
+
when Float
|
|
85
|
+
raise ArgumentError, 'unsafe_input' unless value.finite?
|
|
86
|
+
|
|
87
|
+
value
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError, 'unsafe_input'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def collection(value, work, depth, structured)
|
|
94
|
+
marked = false
|
|
95
|
+
return '[Circular]' if work[:seen].key?(value)
|
|
96
|
+
return REDACTED if value.length > 256
|
|
97
|
+
|
|
98
|
+
work[:seen][value] = true
|
|
99
|
+
marked = true
|
|
100
|
+
if value.is_a?(Array)
|
|
101
|
+
value.map { |item| visit(item, work, depth + 1, structured) }
|
|
102
|
+
else
|
|
103
|
+
value.each_with_object({}) do |(key, nested), result|
|
|
104
|
+
raise ArgumentError, 'unsafe_input' unless key.is_a?(String) || key.is_a?(Symbol)
|
|
105
|
+
|
|
106
|
+
name = key.to_s
|
|
107
|
+
next if name.bytesize > 128
|
|
108
|
+
|
|
109
|
+
count_bytes(work, name)
|
|
110
|
+
next if scrub_text(name, work) != name
|
|
111
|
+
|
|
112
|
+
result[name] = sensitive_key?(name, work[:fields]) ? REDACTED : visit(nested, work, depth + 1, structured)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
ensure
|
|
116
|
+
work[:seen].delete(value) if marked
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def count_bytes(work, value)
|
|
120
|
+
work[:bytes] += value.bytesize
|
|
121
|
+
raise ArgumentError, 'budget_exceeded' if work[:bytes] > MAX_BYTES
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def string(value, work, structured)
|
|
125
|
+
raise ArgumentError, 'unsafe_input' unless value.valid_encoding?
|
|
126
|
+
return REDACTED if value.bytesize > 16_384
|
|
127
|
+
|
|
128
|
+
count_bytes(work, value)
|
|
129
|
+
if structured && value.start_with?('{', '[')
|
|
130
|
+
begin
|
|
131
|
+
nested = JSON.parse(value, max_nesting: 17)
|
|
132
|
+
return JSON.generate(visit(nested, work, 0, false)) if nested.is_a?(Hash) || nested.is_a?(Array)
|
|
133
|
+
rescue JSON::ParserError
|
|
134
|
+
# Malformed structured content with a credential label is withheld below.
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
result = scrub_text(value, work)
|
|
138
|
+
return REDACTED if result == value && value.start_with?('{', '[') &&
|
|
139
|
+
/(?:password|token|secret|authorization|cookie)["']?\s*[:=]/i.match?(value)
|
|
140
|
+
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def valid_card?(candidate)
|
|
145
|
+
digits = candidate.delete(' -')
|
|
146
|
+
return false unless digits.length.between?(13, 19) && digits.chars.uniq.length > 1
|
|
147
|
+
|
|
148
|
+
digits.reverse.chars.each_with_index.sum do |char, index|
|
|
149
|
+
number = char.to_i * (index.odd? ? 2 : 1)
|
|
150
|
+
number > 9 ? number - 9 : number
|
|
151
|
+
end.modulo(10).zero?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def scrub_text(value, work, scan_urls: true)
|
|
155
|
+
return REDACTED if /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i.match?(value) &&
|
|
156
|
+
!/-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i.match?(value)
|
|
157
|
+
|
|
158
|
+
result = value
|
|
159
|
+
if /(?:password|token|secret|authorization|cookie)%3[ad]/i.match?(result)
|
|
160
|
+
result = URI.decode_www_form_component(result)
|
|
161
|
+
end
|
|
162
|
+
pem = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i
|
|
163
|
+
result = result.gsub(pem, REDACTED)
|
|
164
|
+
result = result.gsub(/\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/i, '\\1: [REDACTED]')
|
|
165
|
+
result = result.gsub(%r{\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{6,}}i, '\\1 [REDACTED]')
|
|
166
|
+
result = result.gsub(/\bdbundle_(?:proj|mem|probe|agent)_[A-Za-z0-9_-]+\b/, REDACTED)
|
|
167
|
+
result = result.gsub(work[:assignment_pattern], '\\1\\2[REDACTED]')
|
|
168
|
+
result = result.gsub(/(?<![A-Za-z0-9_-])(?:[0-9][ -]?){12,18}[0-9](?![A-Za-z0-9_-])/) do |candidate|
|
|
169
|
+
valid_card?(candidate) ? REDACTED : candidate
|
|
170
|
+
end
|
|
171
|
+
return result unless scan_urls
|
|
172
|
+
|
|
173
|
+
result.gsub(%r{\bhttps?://[^\s<>"']+}i) do |candidate|
|
|
174
|
+
raw = candidate.sub(/[).,;]+\z/, '')
|
|
175
|
+
scrub_url(raw, work) + candidate.delete_prefix(raw)
|
|
176
|
+
end
|
|
177
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
178
|
+
REDACTED
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def scrub_url(raw, work)
|
|
182
|
+
uri = URI.parse(raw)
|
|
183
|
+
return REDACTED unless uri.host
|
|
184
|
+
|
|
185
|
+
if uri.userinfo
|
|
186
|
+
uri.user = 'REDACTED'
|
|
187
|
+
uri.password = nil
|
|
188
|
+
end
|
|
189
|
+
uri.path = '/' if uri.path.empty?
|
|
190
|
+
if uri.query
|
|
191
|
+
uri.query = URI.encode_www_form(URI.decode_www_form(uri.query).filter_map do |key, value|
|
|
192
|
+
next if key.bytesize > 128 || scrub_text(key, work, scan_urls: false) != key
|
|
193
|
+
|
|
194
|
+
unsafe = sensitive_key?(key, work[:fields]) || scrub_text(value, work, scan_urls: false) != value
|
|
195
|
+
[key, unsafe ? REDACTED : value]
|
|
196
|
+
end)
|
|
197
|
+
end
|
|
198
|
+
uri.fragment = nil
|
|
199
|
+
uri.to_s
|
|
200
|
+
rescue URI::InvalidURIError
|
|
201
|
+
REDACTED
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
data/lib/debugbundle/version.rb
CHANGED
data/lib/debugbundle.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'debugbundle/redaction'
|
|
4
|
+
require_relative 'debugbundle/telemetry_privacy'
|
|
4
5
|
require_relative 'debugbundle/acknowledgement'
|
|
5
6
|
require_relative 'debugbundle/before_send'
|
|
6
7
|
require_relative 'debugbundle/rack/middleware'
|
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.0
|
|
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-25 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -44,17 +44,18 @@ executables: []
|
|
|
44
44
|
extensions: []
|
|
45
45
|
extra_rdoc_files: []
|
|
46
46
|
files:
|
|
47
|
-
- Gemfile
|
|
48
47
|
- LICENSE
|
|
49
|
-
- Makefile
|
|
50
48
|
- README.md
|
|
51
|
-
- debugbundle.gemspec
|
|
52
49
|
- lib/debugbundle.rb
|
|
53
50
|
- lib/debugbundle/acknowledgement.rb
|
|
54
51
|
- lib/debugbundle/before_send.rb
|
|
55
52
|
- lib/debugbundle/client.rb
|
|
56
53
|
- lib/debugbundle/client_event_support.rb
|
|
54
|
+
- lib/debugbundle/client_process_support.rb
|
|
55
|
+
- lib/debugbundle/client_queue_support.rb
|
|
57
56
|
- lib/debugbundle/config.rb
|
|
57
|
+
- lib/debugbundle/config_worker.rb
|
|
58
|
+
- lib/debugbundle/delivery_worker.rb
|
|
58
59
|
- lib/debugbundle/logging.rb
|
|
59
60
|
- lib/debugbundle/rack/middleware.rb
|
|
60
61
|
- lib/debugbundle/rack/relay_middleware.rb
|
|
@@ -66,29 +67,13 @@ files:
|
|
|
66
67
|
- lib/debugbundle/relay/handler.rb
|
|
67
68
|
- lib/debugbundle/remote_config.rb
|
|
68
69
|
- lib/debugbundle/runtime.rb
|
|
70
|
+
- lib/debugbundle/safe_input.rb
|
|
69
71
|
- lib/debugbundle/sidekiq/server_middleware.rb
|
|
70
72
|
- lib/debugbundle/suppression.rb
|
|
73
|
+
- lib/debugbundle/telemetry_privacy.rb
|
|
71
74
|
- lib/debugbundle/transport.rb
|
|
72
75
|
- lib/debugbundle/trigger_token.rb
|
|
73
76
|
- lib/debugbundle/version.rb
|
|
74
|
-
- spec/before_send_spec.rb
|
|
75
|
-
- spec/client_spec.rb
|
|
76
|
-
- spec/debugbundle_spec.rb
|
|
77
|
-
- spec/file_transport_spec.rb
|
|
78
|
-
- spec/fixtures/event-envelope.schema.json
|
|
79
|
-
- spec/logger_integration_spec.rb
|
|
80
|
-
- spec/rack_integration_spec.rb
|
|
81
|
-
- spec/rack_middleware_spec.rb
|
|
82
|
-
- spec/rails_railtie_spec.rb
|
|
83
|
-
- spec/rails_relay_spec.rb
|
|
84
|
-
- spec/redaction_spec.rb
|
|
85
|
-
- spec/relay_spec.rb
|
|
86
|
-
- spec/remote_config_spec.rb
|
|
87
|
-
- spec/repository_metadata_spec.rb
|
|
88
|
-
- spec/sidekiq_integration_spec.rb
|
|
89
|
-
- spec/sidekiq_middleware_spec.rb
|
|
90
|
-
- spec/spec_helper.rb
|
|
91
|
-
- spec/suppression_spec.rb
|
|
92
77
|
homepage: https://debugbundle.com/docs/sdks/ruby
|
|
93
78
|
licenses:
|
|
94
79
|
- Apache-2.0
|
data/Gemfile
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
source 'https://rubygems.org'
|
|
4
|
-
|
|
5
|
-
gemspec
|
|
6
|
-
|
|
7
|
-
group :development, :test do
|
|
8
|
-
gem 'json_schemer', '~> 2.4'
|
|
9
|
-
gem 'rack-test', '~> 2.1'
|
|
10
|
-
gem 'rails', '~> 7.1'
|
|
11
|
-
gem 'rake', '~> 13.2'
|
|
12
|
-
gem 'rspec', '~> 3.13'
|
|
13
|
-
gem 'rubocop', '~> 1.76', require: false
|
|
14
|
-
gem 'sidekiq', '~> 7.0', require: false
|
|
15
|
-
gem 'simplecov', '~> 0.22', require: false
|
|
16
|
-
gem 'webmock', '~> 3.25'
|
|
17
|
-
end
|
data/Makefile
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
SHELL := /bin/sh
|
|
2
|
-
|
|
3
|
-
RUBY_IMAGE ?= ruby:3.4.2
|
|
4
|
-
WORKDIR := /workspace
|
|
5
|
-
BUNDLE_GEMFILE ?= Gemfile
|
|
6
|
-
VERSION ?= $(shell ruby -e 'require "./lib/debugbundle/version"; print DebugBundle::VERSION')
|
|
7
|
-
|
|
8
|
-
DOCKER_RUN = docker run --rm -t \
|
|
9
|
-
-v "$(PWD):$(WORKDIR)" \
|
|
10
|
-
-w "$(WORKDIR)" \
|
|
11
|
-
$(RUBY_IMAGE)
|
|
12
|
-
|
|
13
|
-
BUNDLE_ENV = BUNDLE_GEMFILE="$(BUNDLE_GEMFILE)"
|
|
14
|
-
|
|
15
|
-
.PHONY: bundle-install test lint build shell compat-rack compat-rails compat-sidekiq compat
|
|
16
|
-
.PHONY: smoke
|
|
17
|
-
.PHONY: smoke-published
|
|
18
|
-
.PHONY: test-focused check-docker
|
|
19
|
-
check-docker:
|
|
20
|
-
docker run --rm -v "$(CURDIR):$(WORKDIR)" -w "$(WORKDIR)" $(RUBY_IMAGE) sh -lc 'BUNDLE_PATH=vendor/bundle bundle exec rubocop && BUNDLE_PATH=vendor/bundle bundle exec rspec && gem build debugbundle.gemspec'
|
|
21
|
-
|
|
22
|
-
test-focused:
|
|
23
|
-
docker run --rm -v "$(CURDIR):$(WORKDIR)" -w "$(WORKDIR)" $(RUBY_IMAGE) sh -lc 'BUNDLE_PATH=vendor/bundle SIMPLECOV_MINIMUM_COVERAGE=0 bundle exec rspec $(TEST_FILES)'
|
|
24
|
-
|
|
25
|
-
bundle-install:
|
|
26
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install"
|
|
27
|
-
|
|
28
|
-
test:
|
|
29
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && $(BUNDLE_ENV) bundle exec rspec"
|
|
30
|
-
|
|
31
|
-
lint:
|
|
32
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && $(BUNDLE_ENV) bundle exec rubocop"
|
|
33
|
-
|
|
34
|
-
build:
|
|
35
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && gem build debugbundle.gemspec"
|
|
36
|
-
|
|
37
|
-
smoke:
|
|
38
|
-
$(DOCKER_RUN) sh -lc "gem build debugbundle.gemspec && ruby smoke/run_app_driven_smoke.rb --source local --version $(VERSION)"
|
|
39
|
-
|
|
40
|
-
smoke-published:
|
|
41
|
-
$(DOCKER_RUN) sh -lc "ruby smoke/run_app_driven_smoke.rb --source published --version $(VERSION)"
|
|
42
|
-
|
|
43
|
-
compat-rack:
|
|
44
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.1.6 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_2_2.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_2_2.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_2_2.gemfile" bundle exec rspec spec/rack_integration_spec.rb spec/rack_middleware_spec.rb spec/relay_spec.rb'
|
|
45
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.4.2 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_3.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_3.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rack_3.gemfile" bundle exec rspec spec/rack_integration_spec.rb spec/rack_middleware_spec.rb spec/relay_spec.rb'
|
|
46
|
-
|
|
47
|
-
compat-rails:
|
|
48
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.1.6 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_0.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_0.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_0.gemfile" bundle exec rspec spec/rails_relay_spec.rb spec/rails_railtie_spec.rb'
|
|
49
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.4.2 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_1.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_1.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/rails_7_1.gemfile" bundle exec rspec spec/rails_relay_spec.rb spec/rails_railtie_spec.rb'
|
|
50
|
-
|
|
51
|
-
compat-sidekiq:
|
|
52
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.2 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_7.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_7.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_7.gemfile" bundle exec rspec spec/sidekiq_middleware_spec.rb spec/sidekiq_integration_spec.rb'
|
|
53
|
-
docker run --rm -t -v "$(PWD):$(WORKDIR)" -w "$(WORKDIR)" ruby:3.4.2 sh -lc 'SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_8.gemfile" bundle config set path vendor/bundle && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_8.gemfile" bundle install && SIMPLECOV_MINIMUM_COVERAGE=0 BUNDLE_GEMFILE="gemfiles/sidekiq_8.gemfile" bundle exec rspec spec/sidekiq_middleware_spec.rb spec/sidekiq_integration_spec.rb'
|
|
54
|
-
|
|
55
|
-
compat: compat-rack compat-rails compat-sidekiq
|
|
56
|
-
|
|
57
|
-
shell:
|
|
58
|
-
$(DOCKER_RUN) sh
|
data/debugbundle.gemspec
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'lib/debugbundle/version'
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = 'debugbundle'
|
|
7
|
-
spec.version = DebugBundle::VERSION
|
|
8
|
-
spec.authors = ['DebugBundle']
|
|
9
|
-
spec.email = ['support@debugbundle.com']
|
|
10
|
-
|
|
11
|
-
spec.summary = 'DebugBundle SDK for Ruby'
|
|
12
|
-
spec.description = 'Production-ready error, request, log, and probe capture for Ruby services.'
|
|
13
|
-
spec.homepage = 'https://debugbundle.com/docs/sdks/ruby'
|
|
14
|
-
spec.license = 'Apache-2.0'
|
|
15
|
-
spec.required_ruby_version = '>= 3.1'
|
|
16
|
-
|
|
17
|
-
spec.metadata = {
|
|
18
|
-
'homepage_uri' => spec.homepage,
|
|
19
|
-
'source_code_uri' => 'https://github.com/debugbundle/debugbundle-ruby',
|
|
20
|
-
'changelog_uri' => 'https://github.com/debugbundle/debugbundle-ruby/blob/main/CHANGELOG.md',
|
|
21
|
-
'rubygems_mfa_required' => 'true'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
spec.files = Dir.glob('{lib,spec}/**/*') + %w[Gemfile Makefile README.md LICENSE debugbundle.gemspec]
|
|
25
|
-
spec.bindir = 'exe'
|
|
26
|
-
spec.require_paths = ['lib']
|
|
27
|
-
|
|
28
|
-
spec.add_dependency 'base64', '~> 0.2'
|
|
29
|
-
spec.add_dependency 'logger', '~> 1.6'
|
|
30
|
-
end
|
data/spec/before_send_spec.rb
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'spec_helper'
|
|
4
|
-
|
|
5
|
-
RSpec.describe DebugBundle::BeforeSend do
|
|
6
|
-
let(:base_event) do
|
|
7
|
-
{
|
|
8
|
-
'schema_version' => '2026-03-01',
|
|
9
|
-
'event_id' => '11111111-1111-4111-8111-111111111111',
|
|
10
|
-
'event_type' => 'log_event',
|
|
11
|
-
'occurred_at' => '2026-07-27T08:00:00Z',
|
|
12
|
-
'sdk_name' => '@debugbundle/sdk-ruby',
|
|
13
|
-
'sdk_version' => '1.3.0',
|
|
14
|
-
'service' => { 'name' => 'ruby-test', 'environment' => 'test' },
|
|
15
|
-
'payload' => { 'level' => 'error', 'message' => 'failure', 'attributes' => {} }
|
|
16
|
-
}
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it 'validates every closed canonical payload variant' do
|
|
20
|
-
payloads = {
|
|
21
|
-
'backend_exception' => {
|
|
22
|
-
'name' => 'RuntimeError', 'message' => 'failure', 'stack' => 'stack', 'handled' => true,
|
|
23
|
-
'request' => {}, 'response' => {}, 'runtime' => {}, 'probe_data' => {}
|
|
24
|
-
},
|
|
25
|
-
'request_event' => {
|
|
26
|
-
'method' => 'GET', 'path' => '/', 'query' => {}, 'headers' => {},
|
|
27
|
-
'response_status' => 503, 'duration_ms' => 10, 'response_headers' => {}
|
|
28
|
-
},
|
|
29
|
-
'frontend_breadcrumb' => { 'breadcrumb_type' => 'navigation', 'data' => {} },
|
|
30
|
-
'frontend_exception' => {
|
|
31
|
-
'name' => 'Error', 'message' => 'failure', 'stack' => 'stack',
|
|
32
|
-
'breadcrumbs' => [], 'probe_data' => {}
|
|
33
|
-
},
|
|
34
|
-
'deploy_metadata' => {
|
|
35
|
-
'commit_sha' => 'abc', 'version' => '1', 'branch' => 'main',
|
|
36
|
-
'environment' => 'test', 'deployed_at' => '2026-07-27T08:00:00Z'
|
|
37
|
-
},
|
|
38
|
-
'error_suppressed' => {
|
|
39
|
-
'fingerprint' => 'fp', 'suppressed_count' => 2, 'window_seconds' => 60,
|
|
40
|
-
'first_seen' => '2026-07-27T08:00:00Z', 'last_seen' => '2026-07-27T08:01:00Z'
|
|
41
|
-
},
|
|
42
|
-
'probe_event' => {
|
|
43
|
-
'label' => 'cart.total', 'data' => { 'value' => 2 },
|
|
44
|
-
'activation_id' => nil, 'probe_label_pattern' => 'cart.*'
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
payloads.each do |event_type, payload|
|
|
49
|
-
event = base_event.merge('event_type' => event_type, 'payload' => payload)
|
|
50
|
-
expect(described_class.valid?(event)).to be_truthy, event_type
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it 'rejects malformed roots, timestamps, payload fields, and typed values' do
|
|
55
|
-
expect(described_class.valid?([])).to be(false)
|
|
56
|
-
expect(described_class.valid?(base_event.merge('unexpected' => true))).to be(false)
|
|
57
|
-
expect(described_class.valid?(base_event.merge('event_id' => 'not-a-uuid'))).to be(false)
|
|
58
|
-
expect(described_class.valid?(base_event.merge('occurred_at' => 'not-a-time'))).to be(false)
|
|
59
|
-
expect(
|
|
60
|
-
described_class.valid?(
|
|
61
|
-
base_event.merge('payload' => base_event['payload'].merge('unexpected' => true))
|
|
62
|
-
)
|
|
63
|
-
).to be(false)
|
|
64
|
-
expect(described_class.valid?(base_event.merge('event_type' => 'unknown'))).to be_falsey
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
it 'covers strict scalar helpers used by request, suppression, and probe validation' do
|
|
68
|
-
expect(described_class.non_negative_number?(Float::INFINITY)).to be(false)
|
|
69
|
-
expect(described_class.non_negative_integer?(-1)).to be(false)
|
|
70
|
-
expect(described_class.positive_integer?(0)).to be(false)
|
|
71
|
-
expect(described_class.timestamp?('invalid')).to be(false)
|
|
72
|
-
expect(described_class.nullable_uuid?('invalid')).to be(false)
|
|
73
|
-
end
|
|
74
|
-
end
|