debugbundle 1.4.0 → 1.5.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 +7 -1
- data/lib/debugbundle/client.rb +12 -4
- data/lib/debugbundle/client_event_support.rb +27 -2
- data/lib/debugbundle/logging.rb +30 -16
- data/lib/debugbundle/rack/middleware.rb +13 -3
- data/lib/debugbundle/relay/handler.rb +9 -1
- data/lib/debugbundle/telemetry_privacy.rb +194 -0
- data/lib/debugbundle/version.rb +1 -1
- data/lib/debugbundle.rb +1 -0
- metadata +3 -23
- data/Gemfile +0 -17
- data/Makefile +0 -52
- 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 -118
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f8040734d7a652566f0c06328713b3b7fa5bc559b1e30f1432c45b71f8decab
|
|
4
|
+
data.tar.gz: b53fba03cb4013cb375610e1e9acef0264f79cc87ad98c987b72ae1968e9a411
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e76d4d31fb538e7a9261b3a544e51440f9406c4ba3fc61508dd34477052e84d4036955568188f1baf988c6a30c30fa8fdbe08d4f286a26131adac3f9ef0f26aa
|
|
7
|
+
data.tar.gz: 64d7f18d44af78e8bd267fde9b967996d90b308efc0df021fa3d1cc2205f0cad69929a9b896ad5819df0ee4618ca399f9f6229dd372ce956e50397e770cd3db0
|
data/README.md
CHANGED
|
@@ -4,6 +4,12 @@ Ruby SDK for DebugBundle.
|
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
7
|
+
The source test matrix covers Ruby 3.1 through 4.0. Rails 7.x remains an installed-base lane; the current Rails 8.1 lane exercises the relay and Rack middleware on Ruby 4.0.
|
|
8
|
+
|
|
9
|
+
## Automatic capture and application filtering
|
|
10
|
+
|
|
11
|
+
`capture_logger(logger)` respects the logger level, Rails silence and disabled output. Lazy message blocks are evaluated by the native logger: zero times when suppressed and once when accepted. Native return values and application block exceptions are preserved. DebugBundle applies its own configured minimum capture level after the logger accepts a message.
|
|
12
|
+
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
9
15
|
```ruby
|
|
@@ -314,7 +320,7 @@ This repository also ships a clean-install app-driven smoke harness that validat
|
|
|
314
320
|
|
|
315
321
|
```sh
|
|
316
322
|
make smoke
|
|
317
|
-
make smoke-published VERSION=1.
|
|
323
|
+
make smoke-published VERSION=1.5.0
|
|
318
324
|
```
|
|
319
325
|
|
|
320
326
|
`make smoke` builds the gem, installs it into a fresh RubyGems home, drives a Rack request plus a browser relay batch through the public SDK surface, validates event envelope shape, and confirms the mock ingestion endpoint receives the expected service, environment, SDK metadata, and correlation fields.
|
data/lib/debugbundle/client.rb
CHANGED
|
@@ -212,7 +212,12 @@ module DebugBundle
|
|
|
212
212
|
end
|
|
213
213
|
|
|
214
214
|
def set_context(key, value)
|
|
215
|
-
|
|
215
|
+
safe = TelemetryPrivacy.protect(
|
|
216
|
+
{ key.to_s => @redactor.redact_value(value) }, additional_fields: config.redact_fields
|
|
217
|
+
)
|
|
218
|
+
@context[key.to_s] = safe[key.to_s] if safe.key?(key.to_s)
|
|
219
|
+
value
|
|
220
|
+
rescue StandardError
|
|
216
221
|
value
|
|
217
222
|
end
|
|
218
223
|
|
|
@@ -238,17 +243,20 @@ module DebugBundle
|
|
|
238
243
|
resolved, raw_value = resolve_probe_value(data, block)
|
|
239
244
|
return unless resolved
|
|
240
245
|
|
|
246
|
+
safe_label = TelemetryPrivacy.protect(label.to_s, additional_fields: config.redact_fields)
|
|
241
247
|
entry = {
|
|
242
|
-
'label' =>
|
|
248
|
+
'label' => safe_label,
|
|
243
249
|
'data' => normalize_probe_data(raw_value),
|
|
244
250
|
'timestamp' => now.iso8601
|
|
245
251
|
}
|
|
246
252
|
|
|
247
|
-
bucket = (@probe_buffers[
|
|
253
|
+
bucket = (@probe_buffers[safe_label] ||= [])
|
|
248
254
|
bucket << entry
|
|
249
255
|
bucket.shift while bucket.length > config.max_probe_entries_per_label
|
|
250
256
|
|
|
251
|
-
emit_probe_events(
|
|
257
|
+
emit_probe_events(safe_label, entry['data'], matching_directives)
|
|
258
|
+
rescue StandardError
|
|
259
|
+
nil
|
|
252
260
|
end
|
|
253
261
|
|
|
254
262
|
def capture_exceptions
|
|
@@ -74,15 +74,23 @@ module DebugBundle
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def normalize_probe_data(value)
|
|
77
|
-
redacted = @redactor.redact_value(value)
|
|
77
|
+
redacted = TelemetryPrivacy.protect(@redactor.redact_value(value), additional_fields: config.redact_fields)
|
|
78
78
|
redacted.is_a?(Hash) ? redacted : { 'value' => redacted }
|
|
79
|
+
rescue StandardError
|
|
80
|
+
{ 'value' => '[REDACTED]' }
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def apply_before_send(event)
|
|
82
|
-
|
|
84
|
+
protected = protect_event(event)
|
|
85
|
+
return nil unless protected
|
|
86
|
+
|
|
87
|
+
prepared = BeforeSend.apply(protected, config.before_send)
|
|
88
|
+
prepared && protect_event(prepared)
|
|
83
89
|
end
|
|
84
90
|
|
|
85
91
|
def enqueue_event(event)
|
|
92
|
+
event = protect_event(event)
|
|
93
|
+
return unless event
|
|
86
94
|
return unless sampled_in?
|
|
87
95
|
|
|
88
96
|
@buffer_mutex.synchronize do
|
|
@@ -91,6 +99,23 @@ module DebugBundle
|
|
|
91
99
|
end
|
|
92
100
|
end
|
|
93
101
|
|
|
102
|
+
def protect_event(event)
|
|
103
|
+
return nil unless TelemetryPrivacy.safe_event_identity?(event, additional_fields: config.redact_fields)
|
|
104
|
+
|
|
105
|
+
fields = TelemetryPrivacy.protect(
|
|
106
|
+
{ 'service' => event['service'], 'payload' => event['payload'], 'context' => event['context'] },
|
|
107
|
+
additional_fields: config.redact_fields
|
|
108
|
+
)
|
|
109
|
+
return nil unless fields['service'].is_a?(Hash) && fields['payload'].is_a?(Hash)
|
|
110
|
+
return nil unless fields['service']['name'].is_a?(String) && fields['service']['environment'].is_a?(String)
|
|
111
|
+
|
|
112
|
+
event.merge('service' => fields['service'], 'payload' => fields['payload']).tap do |protected|
|
|
113
|
+
protected['context'] = fields['context'] if event.key?('context')
|
|
114
|
+
end
|
|
115
|
+
rescue StandardError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
94
119
|
def buffered_batch
|
|
95
120
|
@buffer_mutex.synchronize { @buffer.dup }
|
|
96
121
|
end
|
data/lib/debugbundle/logging.rb
CHANGED
|
@@ -16,24 +16,23 @@ module DebugBundle
|
|
|
16
16
|
|
|
17
17
|
def self.install_stdlib_logger(logger, client:)
|
|
18
18
|
interceptor = Module.new do
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
resolved_message = progname if resolved_message.nil?
|
|
19
|
+
%i[add log].each do |method_name|
|
|
20
|
+
define_method(method_name) do |severity, message = nil, progname = nil, &block|
|
|
21
|
+
normalized_severity = severity || ::Logger::UNKNOWN
|
|
22
|
+
# Match Logger's early return, including Rails' thread-local level.
|
|
23
|
+
disabled = normalized_severity < level || (is_a?(::Logger) && instance_variable_get(:@logdev).nil?)
|
|
24
|
+
next super(severity, message, progname, &block) if disabled || Thread.current[RECURSION_GUARD_KEY]
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
resolved_message = message
|
|
27
|
+
resolved_message = progname.nil? ? self.progname : progname if message.nil?
|
|
28
|
+
result = if message.nil? && block
|
|
29
|
+
super(severity, message, progname) { resolved_message = block.call }
|
|
30
|
+
else
|
|
31
|
+
super(severity, message, progname)
|
|
32
|
+
end
|
|
33
|
+
DebugBundle::Logging.capture_stdlib(client, resolved_message, normalized_severity, self.progname)
|
|
34
|
+
result
|
|
32
35
|
end
|
|
33
|
-
|
|
34
|
-
super(severity, message, progname, &block)
|
|
35
|
-
ensure
|
|
36
|
-
Thread.current[RECURSION_GUARD_KEY] = was_capturing
|
|
37
36
|
end
|
|
38
37
|
end
|
|
39
38
|
|
|
@@ -41,6 +40,21 @@ module DebugBundle
|
|
|
41
40
|
interceptor
|
|
42
41
|
end
|
|
43
42
|
|
|
43
|
+
def self.capture_stdlib(client, message, severity, progname)
|
|
44
|
+
was_capturing = Thread.current[RECURSION_GUARD_KEY]
|
|
45
|
+
Thread.current[RECURSION_GUARD_KEY] = true
|
|
46
|
+
client.capture_log(
|
|
47
|
+
message.to_s,
|
|
48
|
+
level: LOGGER_LEVEL_NAMES.fetch(severity, :warning),
|
|
49
|
+
context: { logger_name: progname }
|
|
50
|
+
)
|
|
51
|
+
rescue StandardError
|
|
52
|
+
# SDK failures must not change the result of the application's logger.
|
|
53
|
+
nil
|
|
54
|
+
ensure
|
|
55
|
+
Thread.current[RECURSION_GUARD_KEY] = was_capturing
|
|
56
|
+
end
|
|
57
|
+
|
|
44
58
|
def self.install_semantic_logger(client:)
|
|
45
59
|
return nil unless defined?(::SemanticLogger)
|
|
46
60
|
return nil unless ::SemanticLogger.respond_to?(:add_appender)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'uri'
|
|
4
4
|
|
|
5
5
|
module DebugBundle
|
|
6
6
|
module Rack
|
|
@@ -65,9 +65,19 @@ module DebugBundle
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def parse_query(query_string)
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
raw_query = query_string.to_s
|
|
69
|
+
return {} if raw_query.bytesize > 16_384
|
|
70
|
+
|
|
71
|
+
URI.decode_www_form(raw_query.tr(';', '&')).each_with_object({}) do |(key, value), result|
|
|
72
|
+
result[key] = if result.key?(key)
|
|
73
|
+
Array(result[key]) << value
|
|
74
|
+
else
|
|
75
|
+
value
|
|
76
|
+
end
|
|
70
77
|
end
|
|
78
|
+
rescue StandardError
|
|
79
|
+
# Broken request metadata must never interrupt the host application.
|
|
80
|
+
{}
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
def route_template(env)
|
|
@@ -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,194 @@
|
|
|
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
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def protect(value, additional_fields: [])
|
|
21
|
+
raise ArgumentError, 'unsafe_input' if additional_fields.length > 128
|
|
22
|
+
unless additional_fields.all? { |field| field.is_a?(String) && field.length.between?(1, 64) }
|
|
23
|
+
raise ArgumentError, 'unsafe_input'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
work = { nodes: 0, bytes: 0, seen: {}.compare_by_identity,
|
|
27
|
+
fields: (FIELDS + additional_fields).map { |field| canonical(field) },
|
|
28
|
+
extra: additional_fields }
|
|
29
|
+
result = visit(value, work, 0, true)
|
|
30
|
+
raise ArgumentError, 'budget_exceeded' if JSON.generate(result).bytesize > MAX_BYTES
|
|
31
|
+
|
|
32
|
+
result
|
|
33
|
+
rescue JSON::GeneratorError, Encoding::InvalidByteSequenceError
|
|
34
|
+
raise ArgumentError, 'unsafe_input'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def safe_event_identity?(event, additional_fields: [])
|
|
38
|
+
correlation = event['correlation'] || {}
|
|
39
|
+
return false unless correlation.is_a?(Hash) && correlation.size <= 8
|
|
40
|
+
|
|
41
|
+
values = event.values_at('schema_version', 'sdk_name', 'sdk_version') + correlation.values
|
|
42
|
+
values.all? do |value|
|
|
43
|
+
value.nil? || (value.is_a?(String) && protect(value, additional_fields: additional_fields) == value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def canonical(value)
|
|
48
|
+
value.downcase.gsub(/[^a-z0-9]/, '')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def sensitive_key?(key, fields)
|
|
52
|
+
segments = key.gsub(/([a-z0-9])([A-Z])/, '\\1_\\2').downcase.split(/[^a-z0-9]+/)
|
|
53
|
+
segments.each_index.any? do |start|
|
|
54
|
+
combined = +''
|
|
55
|
+
segments.drop(start).any? do |segment|
|
|
56
|
+
combined << segment
|
|
57
|
+
fields.include?(combined)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def visit(value, work, depth, structured)
|
|
63
|
+
work[:nodes] += 1
|
|
64
|
+
raise ArgumentError, 'budget_exceeded' if work[:nodes] > 4096
|
|
65
|
+
return REDACTED if depth > 16
|
|
66
|
+
|
|
67
|
+
case value
|
|
68
|
+
when String then string(value, work, structured)
|
|
69
|
+
when Hash, Array then collection(value, work, depth, structured)
|
|
70
|
+
when NilClass, TrueClass, FalseClass, Integer then value
|
|
71
|
+
when Float
|
|
72
|
+
raise ArgumentError, 'unsafe_input' unless value.finite?
|
|
73
|
+
|
|
74
|
+
value
|
|
75
|
+
else
|
|
76
|
+
raise ArgumentError, 'unsafe_input'
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def collection(value, work, depth, structured)
|
|
81
|
+
marked = false
|
|
82
|
+
return '[Circular]' if work[:seen].key?(value)
|
|
83
|
+
return REDACTED if value.length > 256
|
|
84
|
+
|
|
85
|
+
work[:seen][value] = true
|
|
86
|
+
marked = true
|
|
87
|
+
if value.is_a?(Array)
|
|
88
|
+
value.map { |item| visit(item, work, depth + 1, structured) }
|
|
89
|
+
else
|
|
90
|
+
value.each_with_object({}) do |(key, nested), result|
|
|
91
|
+
raise ArgumentError, 'unsafe_input' unless key.is_a?(String) || key.is_a?(Symbol)
|
|
92
|
+
|
|
93
|
+
name = key.to_s
|
|
94
|
+
next if name.bytesize > 128
|
|
95
|
+
|
|
96
|
+
count_bytes(work, name)
|
|
97
|
+
next if scrub_text(name, work) != name
|
|
98
|
+
|
|
99
|
+
result[name] = sensitive_key?(name, work[:fields]) ? REDACTED : visit(nested, work, depth + 1, structured)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
ensure
|
|
103
|
+
work[:seen].delete(value) if marked
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def count_bytes(work, value)
|
|
107
|
+
work[:bytes] += value.bytesize
|
|
108
|
+
raise ArgumentError, 'budget_exceeded' if work[:bytes] > MAX_BYTES
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def string(value, work, structured)
|
|
112
|
+
raise ArgumentError, 'unsafe_input' unless value.valid_encoding?
|
|
113
|
+
return REDACTED if value.bytesize > 16_384
|
|
114
|
+
|
|
115
|
+
count_bytes(work, value)
|
|
116
|
+
if structured && value.start_with?('{', '[')
|
|
117
|
+
begin
|
|
118
|
+
nested = JSON.parse(value, max_nesting: 17)
|
|
119
|
+
return JSON.generate(visit(nested, work, 0, false)) if nested.is_a?(Hash) || nested.is_a?(Array)
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
# Malformed structured content with a credential label is withheld below.
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
result = scrub_text(value, work)
|
|
125
|
+
return REDACTED if result == value && value.start_with?('{', '[') &&
|
|
126
|
+
/(?:password|token|secret|authorization|cookie)["']?\s*[:=]/i.match?(value)
|
|
127
|
+
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def valid_card?(candidate)
|
|
132
|
+
digits = candidate.delete(' -')
|
|
133
|
+
return false unless digits.length.between?(13, 19) && digits.chars.uniq.length > 1
|
|
134
|
+
|
|
135
|
+
digits.reverse.chars.each_with_index.sum do |char, index|
|
|
136
|
+
number = char.to_i * (index.odd? ? 2 : 1)
|
|
137
|
+
number > 9 ? number - 9 : number
|
|
138
|
+
end.modulo(10).zero?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def scrub_text(value, work, scan_urls: true)
|
|
142
|
+
return REDACTED if /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i.match?(value) &&
|
|
143
|
+
!/-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i.match?(value)
|
|
144
|
+
|
|
145
|
+
result = value
|
|
146
|
+
if /(?:password|token|secret|authorization|cookie)%3[ad]/i.match?(result)
|
|
147
|
+
result = URI.decode_www_form_component(result)
|
|
148
|
+
end
|
|
149
|
+
pem = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i
|
|
150
|
+
result = result.gsub(pem, REDACTED)
|
|
151
|
+
result = result.gsub(/\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/i, '\\1: [REDACTED]')
|
|
152
|
+
result = result.gsub(%r{\b(Bearer|Basic)\s+[A-Za-z0-9._~+/-]{6,}}i, '\\1 [REDACTED]')
|
|
153
|
+
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
|
|
158
|
+
result = result.gsub(/(?<![A-Za-z0-9_-])(?:[0-9][ -]?){12,18}[0-9](?![A-Za-z0-9_-])/) do |candidate|
|
|
159
|
+
valid_card?(candidate) ? REDACTED : candidate
|
|
160
|
+
end
|
|
161
|
+
return result unless scan_urls
|
|
162
|
+
|
|
163
|
+
result.gsub(%r{\bhttps?://[^\s<>"']+}i) do |candidate|
|
|
164
|
+
raw = candidate.sub(/[).,;]+\z/, '')
|
|
165
|
+
scrub_url(raw, work) + candidate.delete_prefix(raw)
|
|
166
|
+
end
|
|
167
|
+
rescue URI::InvalidURIError, ArgumentError
|
|
168
|
+
REDACTED
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def scrub_url(raw, work)
|
|
172
|
+
uri = URI.parse(raw)
|
|
173
|
+
return REDACTED unless uri.host
|
|
174
|
+
|
|
175
|
+
if uri.userinfo
|
|
176
|
+
uri.user = 'REDACTED'
|
|
177
|
+
uri.password = nil
|
|
178
|
+
end
|
|
179
|
+
uri.path = '/' if uri.path.empty?
|
|
180
|
+
if uri.query
|
|
181
|
+
uri.query = URI.encode_www_form(URI.decode_www_form(uri.query).filter_map do |key, value|
|
|
182
|
+
next if key.bytesize > 128 || scrub_text(key, work, scan_urls: false) != key
|
|
183
|
+
|
|
184
|
+
unsafe = sensitive_key?(key, work[:fields]) || scrub_text(value, work, scan_urls: false) != value
|
|
185
|
+
[key, unsafe ? REDACTED : value]
|
|
186
|
+
end)
|
|
187
|
+
end
|
|
188
|
+
uri.fragment = nil
|
|
189
|
+
uri.to_s
|
|
190
|
+
rescue URI::InvalidURIError
|
|
191
|
+
REDACTED
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
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: 1.
|
|
4
|
+
version: 1.5.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-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -44,11 +44,8 @@ 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
|
|
@@ -68,27 +65,10 @@ files:
|
|
|
68
65
|
- lib/debugbundle/runtime.rb
|
|
69
66
|
- lib/debugbundle/sidekiq/server_middleware.rb
|
|
70
67
|
- lib/debugbundle/suppression.rb
|
|
68
|
+
- lib/debugbundle/telemetry_privacy.rb
|
|
71
69
|
- lib/debugbundle/transport.rb
|
|
72
70
|
- lib/debugbundle/trigger_token.rb
|
|
73
71
|
- 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
72
|
homepage: https://debugbundle.com/docs/sdks/ruby
|
|
93
73
|
licenses:
|
|
94
74
|
- 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,52 +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
|
-
|
|
19
|
-
bundle-install:
|
|
20
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install"
|
|
21
|
-
|
|
22
|
-
test:
|
|
23
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && $(BUNDLE_ENV) bundle exec rspec"
|
|
24
|
-
|
|
25
|
-
lint:
|
|
26
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && $(BUNDLE_ENV) bundle exec rubocop"
|
|
27
|
-
|
|
28
|
-
build:
|
|
29
|
-
$(DOCKER_RUN) sh -lc "$(BUNDLE_ENV) bundle config set path vendor/bundle && $(BUNDLE_ENV) bundle install && gem build debugbundle.gemspec"
|
|
30
|
-
|
|
31
|
-
smoke:
|
|
32
|
-
$(DOCKER_RUN) sh -lc "gem build debugbundle.gemspec && ruby smoke/run_app_driven_smoke.rb --source local --version $(VERSION)"
|
|
33
|
-
|
|
34
|
-
smoke-published:
|
|
35
|
-
$(DOCKER_RUN) sh -lc "ruby smoke/run_app_driven_smoke.rb --source published --version $(VERSION)"
|
|
36
|
-
|
|
37
|
-
compat-rack:
|
|
38
|
-
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'
|
|
39
|
-
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'
|
|
40
|
-
|
|
41
|
-
compat-rails:
|
|
42
|
-
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'
|
|
43
|
-
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'
|
|
44
|
-
|
|
45
|
-
compat-sidekiq:
|
|
46
|
-
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'
|
|
47
|
-
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'
|
|
48
|
-
|
|
49
|
-
compat: compat-rack compat-rails compat-sidekiq
|
|
50
|
-
|
|
51
|
-
shell:
|
|
52
|
-
$(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
|