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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -5
  3. data/lib/debugbundle/before_send.rb +1 -1
  4. data/lib/debugbundle/client.rb +189 -65
  5. data/lib/debugbundle/client_event_support.rb +96 -38
  6. data/lib/debugbundle/client_process_support.rb +64 -0
  7. data/lib/debugbundle/client_queue_support.rb +183 -0
  8. data/lib/debugbundle/config_worker.rb +60 -0
  9. data/lib/debugbundle/delivery_worker.rb +103 -0
  10. data/lib/debugbundle/rack/middleware.rb +13 -3
  11. data/lib/debugbundle/redaction.rb +7 -8
  12. data/lib/debugbundle/relay/handler.rb +9 -1
  13. data/lib/debugbundle/safe_input.rb +40 -0
  14. data/lib/debugbundle/suppression.rb +62 -20
  15. data/lib/debugbundle/telemetry_privacy.rb +204 -0
  16. data/lib/debugbundle/version.rb +1 -1
  17. data/lib/debugbundle.rb +1 -0
  18. metadata +8 -23
  19. data/Gemfile +0 -17
  20. data/Makefile +0 -58
  21. data/debugbundle.gemspec +0 -30
  22. data/spec/before_send_spec.rb +0 -74
  23. data/spec/client_spec.rb +0 -432
  24. data/spec/debugbundle_spec.rb +0 -54
  25. data/spec/file_transport_spec.rb +0 -54
  26. data/spec/fixtures/event-envelope.schema.json +0 -142
  27. data/spec/logger_integration_spec.rb +0 -202
  28. data/spec/rack_integration_spec.rb +0 -44
  29. data/spec/rack_middleware_spec.rb +0 -210
  30. data/spec/rails_railtie_spec.rb +0 -96
  31. data/spec/rails_relay_spec.rb +0 -121
  32. data/spec/redaction_spec.rb +0 -42
  33. data/spec/relay_spec.rb +0 -274
  34. data/spec/remote_config_spec.rb +0 -80
  35. data/spec/repository_metadata_spec.rb +0 -65
  36. data/spec/sidekiq_integration_spec.rb +0 -66
  37. data/spec/sidekiq_middleware_spec.rb +0 -50
  38. data/spec/spec_helper.rb +0 -23
  39. data/spec/suppression_spec.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cae31875b7ffe9d2005b62f2107fb4810061ec4362e7bf48c4dd521e43d415f6
4
- data.tar.gz: 2e005a0112e3197aa9b19fed791b71cea7dbf1eefcfc6edebc5a2ce7c764f3d0
3
+ metadata.gz: 154069d58e3649c8b90ab0b5567749fef0b0ae27756354467d2f54e347c20bdb
4
+ data.tar.gz: 10364db06a596a352b0b96cdfda8b54fe6ec228ab2a4682b89740d38ca0f2a09
5
5
  SHA512:
6
- metadata.gz: e2f99cad665ebbcd10ea0965ed97f16c1930567344848ead03aa2ff9d1ac297bf9260ba2915a72454d6843e2805e41ebc648525ab0250077c29ea3f8897a291a
7
- data.tar.gz: 25a171fc0e6cd96c74a8cc7a85fde1dbfe52837d4c46cef69926e2b4c736df49912ffce294613b3c7738d4f6f7b8cdc2f4121f6898a1563c4e7056f237d52a55
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` | Max events per flush batch. |
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.4.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[
@@ -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
- refresh_remote_config!
111
- @capture_policy = RemoteConfig.minimal_capture_policy if @config_fetcher && @remote_config_etag.nil?
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
- poll_remote_config_if_due!
152
+ request_remote_config_poll_if_due
122
153
 
123
154
  merged_context = merge_context(context)
124
155
  payload = {
125
- 'name' => error.class.name,
126
- 'message' => error.message.to_s,
127
- 'stack' => Array(error.backtrace).join("\n"),
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.to_s,
204
+ 'message' => safe_log_message(message),
172
205
  'attributes' => merged_context
173
206
  }
174
- event = apply_before_send(base_event('log_event', payload, merged_context))
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
- poll_remote_config_if_due!
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
- event = apply_before_send(
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(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
- @context[key.to_s] = @redactor.redact_value(value)
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
- poll_remote_config_if_due!
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' => label.to_s,
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[label.to_s] ||= [])
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(label.to_s, entry['data'], matching_directives)
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.flush
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
- poll_remote_config_if_due! if capture_enabled?
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
- batch = buffered_batch
351
- return true if batch.empty?
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: batch.map(&:dup)
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 = @buffer_mutex.synchronize { @buffer.length }
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 = normalize_request_path(request['path'] || request['url'])
501
- method = request['method'].to_s.upcase
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
- enqueue_event(event) if event && allowed
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 poll_remote_config_if_due!
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
- flush
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)