debugbundle 1.4.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cae31875b7ffe9d2005b62f2107fb4810061ec4362e7bf48c4dd521e43d415f6
4
- data.tar.gz: 2e005a0112e3197aa9b19fed791b71cea7dbf1eefcfc6edebc5a2ce7c764f3d0
3
+ metadata.gz: 8f8040734d7a652566f0c06328713b3b7fa5bc559b1e30f1432c45b71f8decab
4
+ data.tar.gz: b53fba03cb4013cb375610e1e9acef0264f79cc87ad98c987b72ae1968e9a411
5
5
  SHA512:
6
- metadata.gz: e2f99cad665ebbcd10ea0965ed97f16c1930567344848ead03aa2ff9d1ac297bf9260ba2915a72454d6843e2805e41ebc648525ab0250077c29ea3f8897a291a
7
- data.tar.gz: 25a171fc0e6cd96c74a8cc7a85fde1dbfe52837d4c46cef69926e2b4c736df49912ffce294613b3c7738d4f6f7b8cdc2f4121f6898a1563c4e7056f237d52a55
6
+ metadata.gz: e76d4d31fb538e7a9261b3a544e51440f9406c4ba3fc61508dd34477052e84d4036955568188f1baf988c6a30c30fa8fdbe08d4f286a26131adac3f9ef0f26aa
7
+ data.tar.gz: 64d7f18d44af78e8bd267fde9b967996d90b308efc0df021fa3d1cc2205f0cad69929a9b896ad5819df0ee4618ca399f9f6229dd372ce956e50397e770cd3db0
data/README.md CHANGED
@@ -4,6 +4,8 @@ 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
+
7
9
  ## Automatic capture and application filtering
8
10
 
9
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.
@@ -318,7 +320,7 @@ This repository also ships a clean-install app-driven smoke harness that validat
318
320
 
319
321
  ```sh
320
322
  make smoke
321
- make smoke-published VERSION=1.4.1
323
+ make smoke-published VERSION=1.5.0
322
324
  ```
323
325
 
324
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.
@@ -212,7 +212,12 @@ module DebugBundle
212
212
  end
213
213
 
214
214
  def set_context(key, value)
215
- @context[key.to_s] = @redactor.redact_value(value)
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' => label.to_s,
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[label.to_s] ||= [])
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(label.to_s, entry['data'], matching_directives)
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
- BeforeSend.apply(event, config.before_send)
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
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
- CGI.parse(query_string.to_s).transform_values do |values|
69
- values.length == 1 ? values.first : values
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugBundle
4
- VERSION = '1.4.1'
4
+ VERSION = '1.5.0'
5
5
  end
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.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-15 00:00:00.000000000 Z
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,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
@@ -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