debugbundle 1.0.0 → 1.1.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: 4b4c0b584220369f1090b505a818ba607e548173faca6018d24e26e4cb902c88
4
- data.tar.gz: 4faa4b7b09b784f5f320f997f24fb8918106b3f88785ba795dbe0ae2a7a237c5
3
+ metadata.gz: 55cf00eddb0f7fc23cdc0b574d5c8c95f1b06575dfcef315e7c8f5c31278942b
4
+ data.tar.gz: 43e388045674fc42d1cc44f11365cb392aae7f3a09f64f3784c776cd96c79b6f
5
5
  SHA512:
6
- metadata.gz: 5ceb4089417df9cc82cee9d5f0c3a9e7ea11e4bc69b517a2c9ab5dfeb637e6fb3c57f5bd01a2425cf52fdae66bda463f3b8eff638f37aa556202968478b4060f
7
- data.tar.gz: 4f64e58b96b26f1808514010ade909e6a5b6423cd71fcb83037bb9e54c6cb25b71a2f279ef065a348050c1196ab5a8d6b89a503f26a4e72144d5b6a9215dad44
6
+ metadata.gz: e0beea973c239fdbe571ffbe9e62f48e206112414b9372466e6c99252bd4ed85dc6c349bed7986875d04b77d8e8a6525e35d9fcd3291b8066ffa60fad4d0d73a
7
+ data.tar.gz: 374744696cbd5d1cf8b05791ac67e9e275e33c88fa4d1d3e363056db7b44f3e225fff2c60d223ff4508440e69ec32ab045eb7d9271daea7601d7d758ec6c0569
data/README.md CHANGED
@@ -314,7 +314,7 @@ This repository also ships a clean-install app-driven smoke harness that validat
314
314
 
315
315
  ```sh
316
316
  make smoke
317
- make smoke-published VERSION=1.0.0
317
+ make smoke-published VERSION=1.1.0
318
318
  ```
319
319
 
320
320
  `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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'digest'
4
4
  require 'time'
5
+ require 'uri'
5
6
 
6
7
  require 'debugbundle/runtime'
7
8
 
@@ -23,7 +24,6 @@ module DebugBundle
23
24
  ].freeze
24
25
  BALANCED_IMMEDIATE_REQUEST_STATUSES = [408, 423, 424, 425, 429].freeze
25
26
  INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES = (BALANCED_IMMEDIATE_REQUEST_STATUSES + [409]).freeze
26
- BALANCED_ANOMALY_REQUEST_STATUSES = [400, 401, 403, 404, 409, 410, 422].freeze
27
27
  LOCAL_ENVIRONMENTS = %w[development local test].freeze
28
28
  REQUEST_TRIGGER_DIRECTIVES_KEY = :__debugbundle_request_trigger_directives__
29
29
  THREAD_HOOK_MUTEX = Mutex.new
@@ -142,9 +142,7 @@ module DebugBundle
142
142
  enqueue_event(base_event('backend_exception', payload, merged_context))
143
143
  end
144
144
 
145
- def capture_error(error, context: nil, handled: true)
146
- capture_exception(error, context: context, handled: handled)
147
- end
145
+ def capture_error(error, context: nil, handled: true) = capture_exception(error, context: context, handled: handled)
148
146
 
149
147
  def capture_log(message, level: :warning, context: nil)
150
148
  return unless capture_enabled?
@@ -172,7 +170,7 @@ module DebugBundle
172
170
  sanitized_request = request_payload(request)
173
171
  sanitized_response = response_payload(response)
174
172
  response_status = (sanitized_response['status_code'] || 0).to_i
175
- return unless capture_request_event?(response_status)
173
+ return unless capture_request_event?(response_status, sanitized_request)
176
174
 
177
175
  payload = {
178
176
  'method' => sanitized_request['method'],
@@ -371,9 +369,7 @@ module DebugBundle
371
369
  :healthy
372
370
  end
373
371
 
374
- def buffered_event_count
375
- @buffer_mutex.synchronize { @buffer.length }
376
- end
372
+ def buffered_event_count = @buffer_mutex.synchronize { @buffer.length }
377
373
 
378
374
  private
379
375
 
@@ -400,9 +396,7 @@ module DebugBundle
400
396
  )
401
397
  end
402
398
 
403
- def capture_enabled?
404
- config.enabled? && config.configured?
405
- end
399
+ def capture_enabled? = config.enabled? && config.configured?
406
400
 
407
401
  def merge_context(context)
408
402
  merged = @context.merge(stringify_hash(context || {}))
@@ -437,9 +431,7 @@ module DebugBundle
437
431
  }
438
432
  end
439
433
 
440
- def runtime_payload
441
- Runtime.payload
442
- end
434
+ def runtime_payload = Runtime.payload
443
435
 
444
436
  def exception_causes(error)
445
437
  causes = []
@@ -521,13 +513,9 @@ module DebugBundle
521
513
  }
522
514
  end
523
515
 
524
- def service_name
525
- config.service || DEFAULT_SERVICE_NAME
526
- end
516
+ def service_name = config.service || DEFAULT_SERVICE_NAME
527
517
 
528
- def environment_name
529
- config.environment || DEFAULT_ENVIRONMENT
530
- end
518
+ def environment_name = config.environment || DEFAULT_ENVIRONMENT
531
519
 
532
520
  def correlation_payload(context)
533
521
  request = object_to_hash(context['request'])
@@ -591,14 +579,20 @@ module DebugBundle
591
579
  end
592
580
  end
593
581
 
594
- def capture_request_event?(status_code)
582
+ def capture_request_event?(status_code, request)
595
583
  mode = @capture_policy.capture_request_events
596
- immediate_statuses = immediate_request_statuses
597
- anomaly_statuses = anomaly_request_statuses
598
584
 
599
585
  return true if mode == 'all'
600
- return true if immediate_statuses.include?(status_code)
601
- return true if mode == 'failures_only' && anomaly_statuses.include?(status_code)
586
+ return true if immediate_request_event?(status_code, request)
587
+ return true if mode == 'failures_only' && status_code >= 500
588
+
589
+ false
590
+ end
591
+
592
+ def immediate_request_event?(status_code, request)
593
+ return true if status_code >= 500
594
+ return true if immediate_request_statuses.include?(status_code)
595
+ return true if matching_immediate_client_error_path_rule?(status_code, request)
602
596
 
603
597
  false
604
598
  end
@@ -616,10 +610,34 @@ module DebugBundle
616
610
  statuses + Array(@capture_policy.immediate_client_error_statuses)
617
611
  end
618
612
 
619
- def anomaly_request_statuses
620
- return [] if @capture_policy.preset == 'minimal'
613
+ def matching_immediate_client_error_path_rule?(status_code, request)
614
+ return false unless (400..499).cover?(status_code)
615
+
616
+ path = normalize_request_path(request['path'] || request['url'])
617
+ method = request['method'].to_s.upcase
618
+ Array(@capture_policy.immediate_client_error_path_rules).any? do |rule|
619
+ next false unless rule.status_code == status_code
620
+ next false if !rule.http_methods.empty? && !rule.http_methods.include?(method)
621
621
 
622
- BALANCED_ANOMALY_REQUEST_STATUSES
622
+ if rule.path_pattern.end_with?('*')
623
+ path.start_with?(rule.path_pattern.delete_suffix('*'))
624
+ else
625
+ path == rule.path_pattern
626
+ end
627
+ end
628
+ end
629
+
630
+ def normalize_request_path(value)
631
+ begin
632
+ uri = URI.parse(value.to_s)
633
+ return uri.path if uri.path && !uri.path.empty?
634
+ rescue URI::InvalidURIError
635
+ # Fall through to the lightweight path-only fallback.
636
+ end
637
+ fallback = value.to_s.split('?', 2).first.to_s.split('#', 2).first
638
+ return fallback if fallback.start_with?('/') && !fallback.empty?
639
+
640
+ '/'
623
641
  end
624
642
 
625
643
  def matching_probe_directives(label)
@@ -630,9 +648,7 @@ module DebugBundle
630
648
  end
631
649
  end
632
650
 
633
- def current_request_trigger_directives
634
- Array(Thread.current[REQUEST_TRIGGER_DIRECTIVES_KEY])
635
- end
651
+ def current_request_trigger_directives = Array(Thread.current[REQUEST_TRIGGER_DIRECTIVES_KEY])
636
652
 
637
653
  def matching_request_trigger_directives(label)
638
654
  current_request_trigger_directives.select do |directive|
@@ -671,13 +687,9 @@ module DebugBundle
671
687
  0
672
688
  end
673
689
 
674
- def rate_limited?
675
- @retry_at && @retry_at > now
676
- end
690
+ def rate_limited? = @retry_at && @retry_at > now
677
691
 
678
- def local_environment?
679
- LOCAL_ENVIRONMENTS.include?(environment_name.to_s)
680
- end
692
+ def local_environment? = LOCAL_ENVIRONMENTS.include?(environment_name.to_s)
681
693
 
682
694
  def poll_remote_config_if_due!
683
695
  return unless @config_fetcher
@@ -713,12 +725,8 @@ module DebugBundle
713
725
  nil
714
726
  end
715
727
 
716
- def now
717
- @time_provider.call
718
- end
728
+ def now = @time_provider.call
719
729
 
720
- def monotonic_now
721
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
722
- end
730
+ def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
723
731
  end
724
732
  end
@@ -11,9 +11,12 @@ module DebugBundle
11
11
  :capture_breadcrumbs,
12
12
  :capture_probe_events,
13
13
  :immediate_client_error_statuses,
14
+ :immediate_client_error_path_rules,
14
15
  keyword_init: true
15
16
  )
16
17
 
18
+ ImmediateClientErrorPathRule = Struct.new(:status_code, :path_pattern, :http_methods, keyword_init: true)
19
+
17
20
  Directive = Struct.new(:id, :label_pattern, :service, :environment, :expires_at, keyword_init: true) do
18
21
  def active?(label:, service:, environment:, now:)
19
22
  return false if expires_at <= now
@@ -69,7 +72,8 @@ module DebugBundle
69
72
  capture_request_events: 'failures_only',
70
73
  capture_breadcrumbs: 'local_only',
71
74
  capture_probe_events: 'buffer_only',
72
- immediate_client_error_statuses: []
75
+ immediate_client_error_statuses: [],
76
+ immediate_client_error_path_rules: []
73
77
  )
74
78
  end
75
79
 
@@ -80,7 +84,8 @@ module DebugBundle
80
84
  capture_request_events: 'failures_only',
81
85
  capture_breadcrumbs: 'exception_only',
82
86
  capture_probe_events: 'buffer_only',
83
- immediate_client_error_statuses: []
87
+ immediate_client_error_statuses: [],
88
+ immediate_client_error_path_rules: []
84
89
  )
85
90
  end
86
91
 
@@ -122,10 +127,48 @@ module DebugBundle
122
127
  capture_probe_events: payload['capture_probe_events'] || payload[:capture_probe_events] || 'buffer_only',
123
128
  immediate_client_error_statuses: Array(
124
129
  payload['immediate_client_error_statuses'] || payload[:immediate_client_error_statuses]
125
- ).grep(Integer)
130
+ ).grep(Integer),
131
+ immediate_client_error_path_rules: parse_immediate_client_error_path_rules(
132
+ payload['immediate_client_error_path_rules'] || payload[:immediate_client_error_path_rules]
133
+ )
126
134
  )
127
135
  end
128
136
 
137
+ def self.parse_immediate_client_error_path_rules(value)
138
+ entries = Array(value)
139
+ return [] if entries.empty? || entries.length > 25
140
+
141
+ entries.filter_map do |entry|
142
+ next unless entry.is_a?(Hash)
143
+
144
+ status_code = entry['status_code'] || entry[:status_code]
145
+ path_pattern = entry['path_pattern'] || entry[:path_pattern]
146
+ raw_methods = Array(entry['methods'] || entry[:methods])
147
+ next unless status_code.is_a?(Integer) && (400..499).cover?(status_code)
148
+ next unless valid_path_pattern?(path_pattern)
149
+ next if raw_methods.length > 7
150
+
151
+ http_methods = raw_methods.map { |method| method.to_s.upcase }.uniq
152
+ next unless http_methods.all? { |method| %w[GET POST PUT PATCH DELETE HEAD OPTIONS].include?(method) }
153
+
154
+ ImmediateClientErrorPathRule.new(
155
+ status_code: status_code,
156
+ path_pattern: path_pattern,
157
+ http_methods: http_methods
158
+ )
159
+ end
160
+ end
161
+
162
+ def self.valid_path_pattern?(value)
163
+ return false unless value.is_a?(String)
164
+ return false if value.empty? || value.length > 256
165
+ return false unless value.start_with?('/')
166
+ return false if value.include?('?') || value.include?('#')
167
+
168
+ wildcard_index = value.index('*')
169
+ wildcard_index.nil? || wildcard_index == value.length - 1
170
+ end
171
+
129
172
  def self.parse_directive(payload)
130
173
  return nil unless payload.is_a?(Hash)
131
174
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DebugBundle
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -23,7 +23,7 @@ RSpec.describe 'Rack integration' do
23
23
  client = DebugBundle::Client.new(project_token: 'dbundle_proj_test', service: 'rack-checkout', transport: transport)
24
24
  app = Rack::Builder.new do
25
25
  use DebugBundle::Rack::Middleware, client: client
26
- run ->(_env) { [422, { 'Content-Type' => 'text/plain' }, ['ok']] }
26
+ run ->(_env) { [503, { 'Content-Type' => 'text/plain' }, ['ok']] }
27
27
  end.to_app
28
28
 
29
29
  response = Rack::MockRequest.new(app).get(
@@ -35,10 +35,10 @@ RSpec.describe 'Rack integration' do
35
35
  client.flush
36
36
  event = transport_events.fetch(0).fetch(:events).fetch(0)
37
37
 
38
- expect(response.status).to eq(422)
38
+ expect(response.status).to eq(503)
39
39
  expect(response.body).to eq('ok')
40
40
  expect(event.fetch('event_type')).to eq('request_event')
41
41
  expect(event.fetch('correlation')).to include('request_id' => 'req-rack', 'trace_id' => 'trace-rack')
42
- expect(event.fetch('payload')).to include('path' => '/checkout', 'method' => 'GET', 'response_status' => 422)
42
+ expect(event.fetch('payload')).to include('path' => '/checkout', 'method' => 'GET', 'response_status' => 503)
43
43
  end
44
44
  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.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DebugBundle
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-05-31 00:00:00.000000000 Z
10
+ date: 2026-06-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: base64