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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 154069d58e3649c8b90ab0b5567749fef0b0ae27756354467d2f54e347c20bdb
|
|
4
|
+
data.tar.gz: 10364db06a596a352b0b96cdfda8b54fe6ec228ab2a4682b89740d38ca0f2a09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 529aa156d0361a75d06a6b9c1f1dd9950269c1c0c82a97367436a6987a8546c0f9274631c1027e0e5585147b18e34ce5a441eca453891a442e105617370f373f
|
|
7
|
+
data.tar.gz: 3d768b54c0ca11c5ed227bbe02ece3ad61a40a77597028366e900c1cc42a4e4777bb93fc837dfc2eeee030e610c0756993fb363f0d2eab5e17457b34b0c66e02
|
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. `make compat` runs the Rack, Rails, and Sidekiq Docker lanes, including Rails 8.1.
|
|
8
|
+
|
|
9
|
+
Version 2.0 changes capture and hook timing. Existing installed 1.5.x applications continue to use their pinned gem until upgraded. Review [the migration guide](MIGRATION-2.0.md) before upgrading.
|
|
10
|
+
|
|
11
|
+
`make sdk-safety-perf` runs a Docker-backed host-safety budget with 10,000 filtered INFO calls, 1,000 accepted ERROR calls with and without custom privacy fields, and 10,000 concurrent ERROR calls against a full queue while transport is held. CI and the release workflow run this gate before publication. These bounded synthetic checks supplement, but do not replace, installed runtime and workload qualification.
|
|
12
|
+
|
|
7
13
|
## Automatic capture and application filtering
|
|
8
14
|
|
|
9
15
|
`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.
|
|
@@ -38,7 +44,7 @@ DebugBundle.capture_message("worker started", level: :info)
|
|
|
38
44
|
DebugBundle.flush
|
|
39
45
|
```
|
|
40
46
|
|
|
41
|
-
`DebugBundle.init(...)` arms best-effort process exception capture automatically through `at_exit` and thread exception hooks.
|
|
47
|
+
`DebugBundle.init(...)` arms best-effort process exception capture automatically through `at_exit` and thread exception hooks. These hooks wake the sender without waiting for delivery; shutdown exceptions can be lost if the process exits before the sender finishes. Automatic delivery runs on one background sender thread when the configured batch size or flush interval is reached. Remote configuration uses a separate bounded poller so a slow fetch cannot hold delivery. An explicit `flush` waits for the sender for at most five seconds and reports whether delivery completed. Call it during a controlled shutdown window when delivery is required. A forked child starts fresh workers on its first SDK use and drops inherited parent events, context, and probes while retaining the parent's capture restrictions until remote configuration refreshes.
|
|
42
48
|
|
|
43
49
|
## Framework Integrations
|
|
44
50
|
|
|
@@ -73,7 +79,7 @@ Capture-policy fields are server-owned and must not be supplied in local SDK con
|
|
|
73
79
|
| `local_events_dir` | `.debugbundle/local/events` | Local event file destination. |
|
|
74
80
|
| `spool_dir` | `.debugbundle/local/browser-relay-spool` | Relay durable spool destination. |
|
|
75
81
|
| `redact_fields` | `[]` | Additional sensitive field names merged with built-in redaction defaults. Rails `filter_parameters` are added automatically. |
|
|
76
|
-
| `batch_size` | `25` |
|
|
82
|
+
| `batch_size` | `25` | Wake the background sender when this many events are buffered. |
|
|
77
83
|
| `flush_interval` | `5` | Flush interval in seconds. |
|
|
78
84
|
| `sample_rate` | `1.0` | Fraction of events kept before transport. |
|
|
79
85
|
| `log_level` | `warning` | Minimum captured log severity. |
|
|
@@ -318,10 +324,10 @@ This repository also ships a clean-install app-driven smoke harness that validat
|
|
|
318
324
|
|
|
319
325
|
```sh
|
|
320
326
|
make smoke
|
|
321
|
-
make smoke-published VERSION=1.
|
|
327
|
+
make smoke-published VERSION=1.5.0
|
|
322
328
|
```
|
|
323
329
|
|
|
324
|
-
`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.
|
|
330
|
+
`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. For version 2.0, it also forks the installed client and verifies exactly one child event without replaying the parent's buffered event.
|
|
325
331
|
|
|
326
332
|
## Examples
|
|
327
333
|
|
|
@@ -350,7 +356,7 @@ The repository ships a GitHub Actions release workflow at `.github/workflows/rel
|
|
|
350
356
|
|
|
351
357
|
- Push a `v*` tag or run the workflow manually with a `version` input.
|
|
352
358
|
- Configure the `RUBYGEMS_API_KEY` repository secret before enabling publish.
|
|
353
|
-
- The workflow runs lint, tests, gem build, `make smoke`, RubyGems publish, and `make smoke-published VERSION=<tag>` before creating the GitHub release.
|
|
359
|
+
- The workflow runs lint, tests, the full `make compat` framework/runtime matrix, gem build, `make smoke`, RubyGems publish, and `make smoke-published VERSION=<tag>` before creating the GitHub release.
|
|
354
360
|
|
|
355
361
|
## Documentation
|
|
356
362
|
|
|
@@ -27,7 +27,7 @@ module DebugBundle
|
|
|
27
27
|
rejection_reason dom_context probe_data
|
|
28
28
|
],
|
|
29
29
|
'deploy_metadata' => %w[commit_sha version branch environment deployed_at],
|
|
30
|
-
'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen device],
|
|
30
|
+
'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen reason level device],
|
|
31
31
|
'probe_event' => %w[label data activation_id probe_label_pattern device]
|
|
32
32
|
}.freeze
|
|
33
33
|
ROOT_FIELDS = %w[
|
data/lib/debugbundle/client.rb
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'set'
|
|
4
6
|
require 'time'
|
|
5
|
-
require 'uri'
|
|
6
7
|
|
|
7
8
|
require 'debugbundle/client_event_support'
|
|
9
|
+
require 'debugbundle/client_process_support'
|
|
10
|
+
require 'debugbundle/client_queue_support'
|
|
11
|
+
require 'debugbundle/config_worker'
|
|
12
|
+
require 'debugbundle/delivery_worker'
|
|
8
13
|
require 'debugbundle/runtime'
|
|
9
14
|
|
|
10
15
|
module DebugBundle
|
|
11
16
|
class Client
|
|
17
|
+
include QueueSupport
|
|
18
|
+
include ProcessSupport
|
|
19
|
+
|
|
12
20
|
SCHEMA_VERSION = '2026-03-01'
|
|
13
21
|
SDK_NAME = '@debugbundle/sdk-ruby'
|
|
14
22
|
DEFAULT_SERVICE_NAME = 'ruby-service'
|
|
15
23
|
DEFAULT_ENVIRONMENT = 'development'
|
|
16
24
|
MAX_BUFFER_SIZE = 1_000
|
|
25
|
+
MAX_BUFFER_BYTES = 8 * 1_024 * 1_024
|
|
17
26
|
RETRY_AFTER_CAP_SECONDS = 300
|
|
18
27
|
DEFAULT_HEADER_ALLOWLIST = %w[
|
|
19
28
|
user-agent
|
|
@@ -80,6 +89,7 @@ module DebugBundle
|
|
|
80
89
|
end
|
|
81
90
|
|
|
82
91
|
def initialize(transport: nil, time_provider: nil, random_provider: nil, config_fetcher: nil, **options)
|
|
92
|
+
@owner_pid = Process.pid
|
|
83
93
|
@config = Config.new(**options)
|
|
84
94
|
@time_provider = time_provider || -> { Time.now.utc }
|
|
85
95
|
@random_provider = random_provider || -> { rand }
|
|
@@ -90,6 +100,14 @@ module DebugBundle
|
|
|
90
100
|
@config_fetcher = config_fetcher || build_default_config_fetcher(custom_transport: !transport.nil?)
|
|
91
101
|
@context = {}
|
|
92
102
|
@buffer = []
|
|
103
|
+
@buffer_bytes = 0
|
|
104
|
+
@buffer_priority_counts = [0, 0, 0, 0]
|
|
105
|
+
@inflight_event_ids = Set.new
|
|
106
|
+
@inflight_priority_counts = [0, 0, 0, 0]
|
|
107
|
+
@event_sizes = {}
|
|
108
|
+
@pressure_drops = {}
|
|
109
|
+
@hook_bypass = Set.new
|
|
110
|
+
@finalized_events = {}
|
|
93
111
|
@buffer_mutex = Mutex.new
|
|
94
112
|
@flush_mutex = Mutex.new
|
|
95
113
|
@probe_buffers = {}
|
|
@@ -105,10 +123,22 @@ module DebugBundle
|
|
|
105
123
|
@next_remote_config_poll_at = nil
|
|
106
124
|
@remote_config_etag = nil
|
|
107
125
|
@remote_config = RemoteConfig::Snapshot.default
|
|
108
|
-
@capture_policy = @remote_config.capture_policy
|
|
126
|
+
@capture_policy = @config_fetcher ? RemoteConfig.minimal_capture_policy : @remote_config.capture_policy
|
|
127
|
+
@initial_remote_config_pending = !@config_fetcher.nil?
|
|
128
|
+
@config_worker = nil
|
|
129
|
+
return unless capture_enabled?
|
|
109
130
|
|
|
110
|
-
|
|
111
|
-
|
|
131
|
+
@delivery_worker = DeliveryWorker.new(
|
|
132
|
+
interval: config.flush_interval,
|
|
133
|
+
before_work: -> {},
|
|
134
|
+
&method(:flush_now)
|
|
135
|
+
)
|
|
136
|
+
return unless @config_fetcher
|
|
137
|
+
|
|
138
|
+
@config_worker = ConfigWorker.new(
|
|
139
|
+
next_wait_seconds: method(:next_remote_config_wait_seconds),
|
|
140
|
+
&method(:refresh_remote_config_on_worker)
|
|
141
|
+
)
|
|
112
142
|
end
|
|
113
143
|
|
|
114
144
|
def capture_exception(error, context: nil, handled: true)
|
|
@@ -117,14 +147,15 @@ module DebugBundle
|
|
|
117
147
|
|
|
118
148
|
def capture_exception_internal(error, context:, handled:, run_before_send:)
|
|
119
149
|
return unless capture_enabled?
|
|
150
|
+
return unless preflight_capacity?(3, 'exception')
|
|
120
151
|
|
|
121
|
-
|
|
152
|
+
request_remote_config_poll_if_due
|
|
122
153
|
|
|
123
154
|
merged_context = merge_context(context)
|
|
124
155
|
payload = {
|
|
125
|
-
'name' => error
|
|
126
|
-
'message' => error
|
|
127
|
-
'stack' =>
|
|
156
|
+
'name' => safe_exception_name(error),
|
|
157
|
+
'message' => safe_exception_message(error),
|
|
158
|
+
'stack' => safe_exception_stack(error),
|
|
128
159
|
'handled' => handled,
|
|
129
160
|
'request' => request_payload(merged_context['request']),
|
|
130
161
|
'response' => response_payload(merged_context['response']),
|
|
@@ -140,8 +171,6 @@ module DebugBundle
|
|
|
140
171
|
extra_context['causes'] = causes unless causes.empty?
|
|
141
172
|
|
|
142
173
|
event = base_event('backend_exception', payload, extra_context)
|
|
143
|
-
event = apply_before_send(event) if run_before_send
|
|
144
|
-
return if event.nil?
|
|
145
174
|
|
|
146
175
|
event_payload = event.fetch('payload')
|
|
147
176
|
suppression_key = [
|
|
@@ -152,7 +181,7 @@ module DebugBundle
|
|
|
152
181
|
].join(':')
|
|
153
182
|
return unless @suppression.should_capture(suppression_key, now: monotonic_now)
|
|
154
183
|
|
|
155
|
-
enqueue_event(event)
|
|
184
|
+
enqueue_event(event, skip_before_send: !run_before_send)
|
|
156
185
|
end
|
|
157
186
|
private :capture_exception_internal
|
|
158
187
|
|
|
@@ -161,26 +190,39 @@ module DebugBundle
|
|
|
161
190
|
def capture_log(message, level: :warning, context: nil)
|
|
162
191
|
return unless capture_enabled?
|
|
163
192
|
|
|
164
|
-
poll_remote_config_if_due!
|
|
165
|
-
|
|
166
193
|
normalized_level = normalize_level(level || :warning)
|
|
194
|
+
return unless level_enabled?(normalized_level)
|
|
195
|
+
|
|
196
|
+
priority = LOG_LEVEL_RANKS.fetch(normalized_level) >= LOG_LEVEL_RANKS.fetch(:error) ? 2 : 0
|
|
197
|
+
return unless preflight_capacity?(priority, normalized_level.to_s)
|
|
198
|
+
|
|
199
|
+
request_remote_config_poll_if_due
|
|
167
200
|
|
|
168
201
|
merged_context = merge_context(context)
|
|
169
202
|
payload = {
|
|
170
203
|
'level' => normalized_level.to_s,
|
|
171
|
-
'message' => message
|
|
204
|
+
'message' => safe_log_message(message),
|
|
172
205
|
'attributes' => merged_context
|
|
173
206
|
}
|
|
174
|
-
|
|
175
|
-
return if event.nil? || !level_enabled?(normalized_level)
|
|
176
|
-
|
|
177
|
-
enqueue_event(event)
|
|
207
|
+
enqueue_event(base_event('log_event', payload, merged_context))
|
|
178
208
|
end
|
|
179
209
|
|
|
180
210
|
def capture_request(request, response, context: nil)
|
|
181
211
|
return unless capture_enabled?
|
|
182
212
|
|
|
183
|
-
|
|
213
|
+
status = if response.is_a?(Hash)
|
|
214
|
+
response[:status_code] || response['status_code'] || response[:status] || response['status']
|
|
215
|
+
end
|
|
216
|
+
priority = if status.is_a?(Integer)
|
|
217
|
+
status >= 400 ? 2 : 1
|
|
218
|
+
elsif response.nil?
|
|
219
|
+
1
|
|
220
|
+
else
|
|
221
|
+
3
|
|
222
|
+
end
|
|
223
|
+
return unless preflight_capacity?(priority, 'request')
|
|
224
|
+
|
|
225
|
+
request_remote_config_poll_if_due
|
|
184
226
|
|
|
185
227
|
merged_context = merge_context(context)
|
|
186
228
|
sanitized_request = request_payload(request)
|
|
@@ -199,12 +241,9 @@ module DebugBundle
|
|
|
199
241
|
'response_headers' => sanitized_response['headers'],
|
|
200
242
|
'response_body' => sanitized_response['body']
|
|
201
243
|
}
|
|
202
|
-
|
|
203
|
-
base_event('request_event', payload, merged_context.merge('request' => sanitized_request))
|
|
204
|
-
)
|
|
205
|
-
return if event.nil? || !capture_request_event?(response_status, sanitized_request)
|
|
244
|
+
return unless capture_request_event?(response_status, sanitized_request)
|
|
206
245
|
|
|
207
|
-
enqueue_event(
|
|
246
|
+
enqueue_event(base_event('request_event', payload, merged_context.merge('request' => sanitized_request)))
|
|
208
247
|
end
|
|
209
248
|
|
|
210
249
|
def capture_message(message, level: nil, context: nil)
|
|
@@ -212,14 +251,23 @@ module DebugBundle
|
|
|
212
251
|
end
|
|
213
252
|
|
|
214
253
|
def set_context(key, value)
|
|
215
|
-
|
|
254
|
+
ensure_current_process!
|
|
255
|
+
safe_key = SafeInput.key(key)
|
|
256
|
+
return value unless safe_key
|
|
257
|
+
|
|
258
|
+
safe = TelemetryPrivacy.protect(
|
|
259
|
+
{ safe_key => @redactor.redact_value(value) }, additional_fields: config.redact_fields
|
|
260
|
+
)
|
|
261
|
+
@context[safe_key] = safe[safe_key] if safe.key?(safe_key)
|
|
262
|
+
value
|
|
263
|
+
rescue StandardError
|
|
216
264
|
value
|
|
217
265
|
end
|
|
218
266
|
|
|
219
267
|
def probe(label, data = nil, heavy: false, &block)
|
|
220
268
|
return unless capture_enabled?
|
|
221
269
|
|
|
222
|
-
|
|
270
|
+
request_remote_config_poll_if_due
|
|
223
271
|
return unless @remote_config.probes_enabled
|
|
224
272
|
|
|
225
273
|
matching_directives = matching_probe_directives(label)
|
|
@@ -238,17 +286,20 @@ module DebugBundle
|
|
|
238
286
|
resolved, raw_value = resolve_probe_value(data, block)
|
|
239
287
|
return unless resolved
|
|
240
288
|
|
|
289
|
+
safe_label = TelemetryPrivacy.protect(label.to_s, additional_fields: config.redact_fields)
|
|
241
290
|
entry = {
|
|
242
|
-
'label' =>
|
|
291
|
+
'label' => safe_label,
|
|
243
292
|
'data' => normalize_probe_data(raw_value),
|
|
244
293
|
'timestamp' => now.iso8601
|
|
245
294
|
}
|
|
246
295
|
|
|
247
|
-
bucket = (@probe_buffers[
|
|
296
|
+
bucket = (@probe_buffers[safe_label] ||= [])
|
|
248
297
|
bucket << entry
|
|
249
298
|
bucket.shift while bucket.length > config.max_probe_entries_per_label
|
|
250
299
|
|
|
251
|
-
emit_probe_events(
|
|
300
|
+
emit_probe_events(safe_label, entry['data'], matching_directives)
|
|
301
|
+
rescue StandardError
|
|
302
|
+
nil
|
|
252
303
|
end
|
|
253
304
|
|
|
254
305
|
def capture_exceptions
|
|
@@ -274,7 +325,9 @@ module DebugBundle
|
|
|
274
325
|
handled: false,
|
|
275
326
|
run_before_send: false
|
|
276
327
|
)
|
|
277
|
-
client.
|
|
328
|
+
client.__send__(:wake_sender)
|
|
329
|
+
rescue StandardError
|
|
330
|
+
nil
|
|
278
331
|
end
|
|
279
332
|
true
|
|
280
333
|
end
|
|
@@ -292,7 +345,7 @@ module DebugBundle
|
|
|
292
345
|
end
|
|
293
346
|
|
|
294
347
|
def with_request_trigger(request)
|
|
295
|
-
|
|
348
|
+
request_remote_config_poll_if_due if capture_enabled?
|
|
296
349
|
|
|
297
350
|
directives = TriggerToken.resolve_request_directives(
|
|
298
351
|
request: request,
|
|
@@ -344,19 +397,46 @@ module DebugBundle
|
|
|
344
397
|
end
|
|
345
398
|
|
|
346
399
|
def flush
|
|
400
|
+
ensure_current_process!
|
|
401
|
+
@delivery_worker&.flush || false
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def close
|
|
405
|
+
ensure_current_process!
|
|
406
|
+
@delivery_worker&.close
|
|
407
|
+
@config_worker&.close
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def flush_now
|
|
347
411
|
# rubocop:disable Metrics/BlockLength
|
|
348
412
|
@flush_mutex.synchronize do
|
|
349
413
|
append_suppression_aggregates
|
|
350
|
-
|
|
351
|
-
|
|
414
|
+
append_pressure_aggregates
|
|
415
|
+
candidates = reserve_buffered_batch
|
|
416
|
+
return true if candidates.empty?
|
|
352
417
|
return false if @transport.nil?
|
|
353
418
|
return false if rate_limited?
|
|
354
419
|
|
|
420
|
+
# Replace consumed snapshot slots so dropped events do not remain owned
|
|
421
|
+
# by this batch while later callbacks run or transport waits.
|
|
422
|
+
prepared = candidates.map! do |event|
|
|
423
|
+
finalized = finalized_event_for(event)
|
|
424
|
+
if finalized.nil?
|
|
425
|
+
remove_buffered_events([event])
|
|
426
|
+
next
|
|
427
|
+
end
|
|
428
|
+
[event, finalized]
|
|
429
|
+
end.compact
|
|
430
|
+
return true if prepared.empty?
|
|
431
|
+
|
|
432
|
+
batch = prepared.map(&:first)
|
|
433
|
+
wire_events = prepared.map(&:last)
|
|
434
|
+
|
|
355
435
|
result = Transport.coerce_result(
|
|
356
436
|
@transport.call(
|
|
357
437
|
project_token: config.project_token,
|
|
358
438
|
service_name: service_name,
|
|
359
|
-
events:
|
|
439
|
+
events: wire_events.map(&:dup)
|
|
360
440
|
)
|
|
361
441
|
)
|
|
362
442
|
|
|
@@ -382,9 +462,43 @@ module DebugBundle
|
|
|
382
462
|
rescue StandardError
|
|
383
463
|
@consecutive_failures += 1
|
|
384
464
|
false
|
|
465
|
+
ensure
|
|
466
|
+
release_buffered_batch
|
|
385
467
|
end
|
|
468
|
+
private :flush_now
|
|
469
|
+
|
|
470
|
+
def finalized_event_for(event)
|
|
471
|
+
event_id = event['event_id']
|
|
472
|
+
cached, bypass = @buffer_mutex.synchronize do
|
|
473
|
+
[@finalized_events[event_id], @hook_bypass.include?(event_id)]
|
|
474
|
+
end
|
|
475
|
+
return cached if cached
|
|
476
|
+
|
|
477
|
+
finalized = bypass || !config.before_send ? event : apply_before_send(event)
|
|
478
|
+
return nil if finalized.nil?
|
|
479
|
+
return nil unless post_hook_event_allowed?(finalized)
|
|
480
|
+
|
|
481
|
+
cache_finalized_event(event, finalized)
|
|
482
|
+
end
|
|
483
|
+
private :finalized_event_for
|
|
484
|
+
|
|
485
|
+
def post_hook_event_allowed?(event)
|
|
486
|
+
case event['event_type']
|
|
487
|
+
when 'log_event'
|
|
488
|
+
return false if @capture_policy.capture_logs == 'off'
|
|
489
|
+
|
|
490
|
+
level_enabled?(normalize_level(event.dig('payload', 'level')))
|
|
491
|
+
when 'request_event'
|
|
492
|
+
payload = event['payload']
|
|
493
|
+
capture_request_event?(payload['response_status'].to_i, payload)
|
|
494
|
+
else
|
|
495
|
+
true
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
private :post_hook_event_allowed?
|
|
386
499
|
|
|
387
500
|
def status
|
|
501
|
+
ensure_current_process!
|
|
388
502
|
return :disconnected unless config.enabled?
|
|
389
503
|
return :degraded unless config.configured?
|
|
390
504
|
return @acknowledgement_state if @acknowledgement_state
|
|
@@ -394,7 +508,10 @@ module DebugBundle
|
|
|
394
508
|
:healthy
|
|
395
509
|
end
|
|
396
510
|
|
|
397
|
-
def buffered_event_count
|
|
511
|
+
def buffered_event_count
|
|
512
|
+
ensure_current_process!
|
|
513
|
+
@buffer_mutex.synchronize { @buffer.length }
|
|
514
|
+
end
|
|
398
515
|
|
|
399
516
|
private
|
|
400
517
|
|
|
@@ -497,8 +614,8 @@ module DebugBundle
|
|
|
497
614
|
def matching_immediate_client_error_path_rule?(status_code, request)
|
|
498
615
|
return false unless (400..499).cover?(status_code)
|
|
499
616
|
|
|
500
|
-
path =
|
|
501
|
-
method = request['method']
|
|
617
|
+
path = SafeInput.request_path(request['path'] || request['url'])
|
|
618
|
+
method = SafeInput.key(request['method'])&.upcase || ''
|
|
502
619
|
Array(@capture_policy.immediate_client_error_path_rules).any? do |rule|
|
|
503
620
|
next false unless rule.status_code == status_code
|
|
504
621
|
next false if !rule.http_methods.empty? && !rule.http_methods.include?(method)
|
|
@@ -511,19 +628,6 @@ module DebugBundle
|
|
|
511
628
|
end
|
|
512
629
|
end
|
|
513
630
|
|
|
514
|
-
def normalize_request_path(value)
|
|
515
|
-
begin
|
|
516
|
-
uri = URI.parse(value.to_s)
|
|
517
|
-
return uri.path if uri.path && !uri.path.empty?
|
|
518
|
-
rescue URI::InvalidURIError
|
|
519
|
-
# Fall through to the lightweight path-only fallback.
|
|
520
|
-
end
|
|
521
|
-
fallback = value.to_s.split('?', 2).first.to_s.split('#', 2).first
|
|
522
|
-
return fallback if fallback.start_with?('/') && !fallback.empty?
|
|
523
|
-
|
|
524
|
-
'/'
|
|
525
|
-
end
|
|
526
|
-
|
|
527
631
|
def matching_probe_directives(label)
|
|
528
632
|
active_directives = @remote_config.directives + current_request_trigger_directives
|
|
529
633
|
|
|
@@ -546,21 +650,16 @@ module DebugBundle
|
|
|
546
650
|
return if candidate_directives.empty?
|
|
547
651
|
|
|
548
652
|
candidate_directives.each do |directive|
|
|
549
|
-
event = apply_before_send(
|
|
550
|
-
base_event(
|
|
551
|
-
'probe_event',
|
|
552
|
-
{
|
|
553
|
-
'label' => label,
|
|
554
|
-
'data' => data,
|
|
555
|
-
'activation_id' => directive.id,
|
|
556
|
-
'probe_label_pattern' => directive.label_pattern
|
|
557
|
-
},
|
|
558
|
-
{}
|
|
559
|
-
)
|
|
560
|
-
)
|
|
561
653
|
allowed = request_directives.include?(directive) ||
|
|
562
654
|
@capture_policy.capture_probe_events == 'standalone_when_activated'
|
|
563
|
-
|
|
655
|
+
next unless allowed
|
|
656
|
+
|
|
657
|
+
enqueue_event(base_event('probe_event', {
|
|
658
|
+
'label' => label,
|
|
659
|
+
'data' => data,
|
|
660
|
+
'activation_id' => directive.id,
|
|
661
|
+
'probe_label_pattern' => directive.label_pattern
|
|
662
|
+
}, {}))
|
|
564
663
|
end
|
|
565
664
|
end
|
|
566
665
|
|
|
@@ -575,7 +674,22 @@ module DebugBundle
|
|
|
575
674
|
|
|
576
675
|
def local_environment? = LOCAL_ENVIRONMENTS.include?(environment_name.to_s)
|
|
577
676
|
|
|
578
|
-
def
|
|
677
|
+
def request_remote_config_poll_if_due
|
|
678
|
+
return unless @next_remote_config_poll_at && @next_remote_config_poll_at <= now
|
|
679
|
+
|
|
680
|
+
@config_worker&.wake
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def refresh_remote_config_on_worker
|
|
684
|
+
if @initial_remote_config_pending
|
|
685
|
+
@initial_remote_config_pending = false
|
|
686
|
+
refresh_remote_config!
|
|
687
|
+
else
|
|
688
|
+
refresh_remote_config_if_due!
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def refresh_remote_config_if_due!
|
|
579
693
|
return unless @config_fetcher
|
|
580
694
|
return unless @next_remote_config_poll_at && @next_remote_config_poll_at <= now
|
|
581
695
|
|
|
@@ -592,6 +706,12 @@ module DebugBundle
|
|
|
592
706
|
@next_remote_config_poll_at = interval_seconds ? now + interval_seconds : nil
|
|
593
707
|
end
|
|
594
708
|
|
|
709
|
+
def next_remote_config_wait_seconds
|
|
710
|
+
return nil unless @next_remote_config_poll_at
|
|
711
|
+
|
|
712
|
+
[@next_remote_config_poll_at - now, 0].max
|
|
713
|
+
end
|
|
714
|
+
|
|
595
715
|
def capture_thread_exceptions
|
|
596
716
|
return false if @thread_exception_registered
|
|
597
717
|
|
|
@@ -604,11 +724,15 @@ module DebugBundle
|
|
|
604
724
|
|
|
605
725
|
def capture_thread_exception(error)
|
|
606
726
|
capture_exception_internal(error, context: nil, handled: false, run_before_send: false)
|
|
607
|
-
|
|
727
|
+
wake_sender
|
|
608
728
|
rescue StandardError
|
|
609
729
|
nil
|
|
610
730
|
end
|
|
611
731
|
|
|
732
|
+
def wake_sender
|
|
733
|
+
@delivery_worker&.wake
|
|
734
|
+
end
|
|
735
|
+
|
|
612
736
|
def now = @time_provider.call
|
|
613
737
|
|
|
614
738
|
def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|