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 +4 -4
- data/README.md +1 -1
- data/lib/debugbundle/client.rb +51 -43
- data/lib/debugbundle/remote_config.rb +46 -3
- data/lib/debugbundle/version.rb +1 -1
- data/spec/rack_integration_spec.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55cf00eddb0f7fc23cdc0b574d5c8c95f1b06575dfcef315e7c8f5c31278942b
|
|
4
|
+
data.tar.gz: 43e388045674fc42d1cc44f11365cb392aae7f3a09f64f3784c776cd96c79b6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
data/lib/debugbundle/client.rb
CHANGED
|
@@ -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
|
|
601
|
-
return true if mode == 'failures_only' &&
|
|
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
|
|
620
|
-
return
|
|
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
|
-
|
|
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
|
|
data/lib/debugbundle/version.rb
CHANGED
|
@@ -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) { [
|
|
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(
|
|
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' =>
|
|
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.
|
|
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-
|
|
10
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|