dead_bro 0.2.34 → 0.2.35

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: 1e87bf1af27a0f9c5e26679757060bbef1d1a84e3a9ff597e997ee6665041f06
4
- data.tar.gz: 9872d696d014494ed15f7ac83e809530a1d4f54f5d5db23d783e8c6af99f2fd2
3
+ metadata.gz: 2544ca10e878c96153137ba46f40a543e157eff3c877b1bd3211eec15b01a9e7
4
+ data.tar.gz: 046c6f63e058e9273e8f978f42a727164c4e366c65111169c1a3d86b29ebae43
5
5
  SHA512:
6
- metadata.gz: ae026a67741f9561d7b2423cd9c35a6bd80b3c1b1b0365279b665caea0cd2d45f5e7519bae715890114458be8dbe284bab766a7437a41f8c841557fbfbb00ee2
7
- data.tar.gz: 87c6441aa1789ca958c5980edadef4d5e1d5a73e03573d8684a02dbb62c26a31f8ae13300487901b63e4ba1fcca62fc4ba0676badd059d44783613dc21fb09af
6
+ metadata.gz: badda478e44e521b88d558413370bde506ecb7693c5a77864fc9953f9a082a97722c5c942386e16c8eeb3a15488e196d96da799a494ff9e9ee87cd304ea437bc
7
+ data.tar.gz: 18f19c4e1dc982cb7e6ec422a11ff4e443f1759b73e9841757987c4d8e9b5b3c914dc6995c65cbccf0bdd48110e3214e357b62546585558a3e6cab7ee2de83a1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.35] - 2026-10-21
4
+
5
+ ### Fixed
6
+ - **In production, every exception raised in a controller action was sent to DeadBro twice.** The request subscriber reports it with the request's details; the exception then reached `ErrorMiddleware` on its way out (with `consider_all_requests_local` off, `ActionDispatch::DebugExceptions` re-raises it instead of rendering the debug page), which sent it again. The dashboard already counted each error once, so the second request was wasted work for the app. The subscriber now flags the request in the Rack env (`env["dead_bro.reported"]`) and `ErrorMiddleware` skips flagged requests, so one error is one request to DeadBro. `ErrorMiddleware` still reports exceptions that never reach a controller action. An exception raised in an excluded controller is no longer sent at all.
7
+
8
+ ### Changed
9
+ - **Requests that match no route are no longer sent unless the app's settings ask for them.** Rails raises `ActionController::RoutingError` for them in the middleware stack, not in a controller, and `ErrorMiddleware` sent each one to DeadBro, which discarded it. Turn on "Track requests that match no route" in the app's settings in DeadBro (the `report_routing_errors` setting) to have them sent and stored as `ActionController::RoutingError` errors. A `RoutingError` raised by a controller action is still reported, once, by the request subscriber.
10
+ - `ErrorMiddleware` reports now carry the response status, the time the request took, the machine's host name and the request's host, so DeadBro can store them as requests.
11
+
3
12
  ## [0.2.34] - 2026-09-29
4
13
 
5
14
  ### Fixed
data/README.md CHANGED
@@ -61,6 +61,8 @@ For **Amazon ECS**, add one of these variables in your task definition from Code
61
61
 
62
62
  Use the DeadBro UI to turn features on or off, set sample rates, define controller/job inclusions and exclusions, tune slow-query EXPLAIN, enable queue and system metrics, and adjust related limits. After you deploy the initializer above, those choices take effect when the gem receives them from the API (typically on the next successful metric or heartbeat response).
63
63
 
64
+ Requests that match none of the app's routes (`ActionController::RoutingError`) aren't sent unless "Track requests that match no route" is on in the app's settings (`report_routing_errors`, off by default). DeadBro then stores them as `ActionController::RoutingError` errors.
65
+
64
66
  ### Optional local flags
65
67
 
66
68
  You can still set `config.enabled` in Ruby if you need to force the integration off in a given environment before any remote settings arrive; otherwise the dashboard can control `enabled` like other remote settings.
@@ -23,7 +23,7 @@ module DeadBro
23
23
  attr_accessor :memory_tracking_enabled, :allocation_tracking_enabled, :allocation_sample_rate,
24
24
  :sample_rate, :slow_query_threshold_ms, :watch_enabled,
25
25
  :monitor_enabled, :enable_db_stats, :enable_process_stats, :enable_system_stats,
26
- :max_sql_queries_to_send, :max_logs_to_send
26
+ :max_sql_queries_to_send, :max_logs_to_send, :report_routing_errors
27
27
 
28
28
  # Readers for exclusion lists. Writers are defined below so we can compile
29
29
  # and cache the regex form once, instead of rebuilding it per request.
@@ -73,7 +73,7 @@ module DeadBro
73
73
  explain_enabled slow_query_threshold_ms max_sql_queries_to_send max_logs_to_send
74
74
  watch_enabled excluded_controllers excluded_jobs exclusive_controllers exclusive_jobs
75
75
  monitor_enabled enable_db_stats enable_process_stats enable_system_stats
76
- sample_rates_by_type
76
+ sample_rates_by_type report_routing_errors
77
77
  ].freeze
78
78
 
79
79
  def initialize
@@ -116,6 +116,10 @@ module DeadBro
116
116
  @enable_db_stats = false
117
117
  @enable_process_stats = false
118
118
  @enable_system_stats = false
119
+ # A request that matches no route raises ActionController::RoutingError in
120
+ # the middleware stack, not in a controller. ErrorMiddleware sends those
121
+ # only when this is on.
122
+ @report_routing_errors = false
119
123
 
120
124
  @settings_received_at = nil
121
125
  @skip_until = nil
@@ -190,7 +194,8 @@ module DeadBro
190
194
  # (explain_enabled) stays untouched; see #explain_active?
191
195
  @remote_explain_enabled = !!value
192
196
  when "enabled", "memory_tracking_enabled", "allocation_tracking_enabled", "watch_enabled",
193
- "monitor_enabled", "enable_db_stats", "enable_process_stats", "enable_system_stats"
197
+ "monitor_enabled", "enable_db_stats", "enable_process_stats", "enable_system_stats",
198
+ "report_routing_errors"
194
199
  send(:"#{k}=", !!value)
195
200
  when "excluded_controllers", "excluded_jobs", "exclusive_controllers", "exclusive_jobs"
196
201
  send(:"#{k}=", Array(value).map(&:to_s))
@@ -12,14 +12,17 @@ module DeadBro
12
12
  end
13
13
 
14
14
  def call(env)
15
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
16
  @app.call(env)
16
17
  rescue Exception => exception # rubocop:disable Lint/RescueException
17
18
  begin
18
- payload = build_payload(exception, env)
19
- # Use the error class name as the event name
20
- event_name = exception.class.name.to_s
21
- event_name = EVENT_NAME if event_name.empty?
22
- @client.post_metric(event_name: event_name, payload: payload, force: true)
19
+ if report?(exception, env)
20
+ payload = build_payload(exception, env, started)
21
+ # Use the error class name as the event name
22
+ event_name = exception.class.name.to_s
23
+ event_name = EVENT_NAME if event_name.empty?
24
+ @client.post_metric(event_name: event_name, payload: payload, force: true)
25
+ end
23
26
  rescue *DeadBro::CONTAINED_ERRORS
24
27
  # Never let APM reporting interfere with the host app — in particular,
25
28
  # never replace the host's exception with one of ours.
@@ -29,7 +32,23 @@ module DeadBro
29
32
 
30
33
  private
31
34
 
32
- def build_payload(exception, env)
35
+ # Only exceptions that never reached a controller action are this
36
+ # middleware's: the request subscriber reports the others, with the
37
+ # request's details, and flags the env. A request that matches no route
38
+ # raises ActionController::RoutingError here; those are sent only when the
39
+ # app's report_routing_errors setting is on.
40
+ def report?(exception, env)
41
+ return false if env[DeadBro::Subscriber::REPORTED_ENV_KEY]
42
+ return !!DeadBro.configuration.report_routing_errors if routing_error?(exception)
43
+
44
+ true
45
+ end
46
+
47
+ def routing_error?(exception)
48
+ defined?(::ActionController::RoutingError) && exception.is_a?(::ActionController::RoutingError)
49
+ end
50
+
51
+ def build_payload(exception, env, started)
33
52
  req = rack_request(env)
34
53
 
35
54
  payload = {
@@ -39,6 +58,11 @@ module DeadBro
39
58
  fingerprint: DeadBro::Subscriber.compute_error_fingerprint(exception),
40
59
  cause_chain: DeadBro::Subscriber.build_cause_chain(exception),
41
60
  occurred_at: Time.now.utc.to_i,
61
+ # What the backend needs to store the report as a request.
62
+ status: response_status(exception),
63
+ duration_ms: elapsed_ms(started),
64
+ host: DeadBro.safe_hostname,
65
+ request_host: DeadBro::Subscriber.safe_request_host({env: env}),
42
66
  rack:
43
67
  {
44
68
  method: req&.request_method,
@@ -69,6 +93,22 @@ module DeadBro
69
93
  nil
70
94
  end
71
95
 
96
+ # The status Rails answers the exception with: 404 for a RoutingError, else
97
+ # 500 unless the app maps the class elsewhere (action_dispatch.rescue_responses).
98
+ def response_status(exception)
99
+ return 500 unless defined?(::ActionDispatch::ExceptionWrapper)
100
+
101
+ ::ActionDispatch::ExceptionWrapper.status_code_for_exception(exception.class.name)
102
+ rescue
103
+ 500
104
+ end
105
+
106
+ def elapsed_ms(started)
107
+ ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000.0).round(2)
108
+ rescue
109
+ nil
110
+ end
111
+
72
112
  def safe_backtrace(exception)
73
113
  Array(exception.backtrace).first(50)
74
114
  rescue
@@ -9,6 +9,10 @@ module DeadBro
9
9
 
10
10
  def self.subscribe!(client: Client.new)
11
11
  ActiveSupport::Notifications.subscribe(EVENT_NAME) do |name, started, finished, _unique_id, data|
12
+ # An exception the action raised is this listener's to report (or to
13
+ # drop with an excluded request) — ErrorMiddleware must not send it again.
14
+ mark_reported(data)
15
+
12
16
  # When disabled remotely, fire a heartbeat at most once per minute so the gem
13
17
  # can detect when tracking has been re-enabled, then skip all tracking.
14
18
  unless DeadBro.configuration.enabled
@@ -279,6 +283,23 @@ module DeadBro
279
283
  # Best effort — draining must never raise from the notifications callback.
280
284
  end
281
285
 
286
+ # Set in the Rack env when the controller action raised. The exception then
287
+ # passes ErrorMiddleware on its way out (in production DebugExceptions
288
+ # re-raises it), which skips flagged requests so one error is one POST.
289
+ REPORTED_ENV_KEY = "dead_bro.reported"
290
+
291
+ def self.mark_reported(data)
292
+ return unless data[:exception] || data[:exception_object]
293
+
294
+ # The payload carries the request since Rails 6.1. Not data[:headers]:
295
+ # ActionDispatch::Http::Headers#env is a copy, the flag would be lost.
296
+ request = data[:request]
297
+ env = request.env if request.respond_to?(:env)
298
+ env[REPORTED_ENV_KEY] = true if env.is_a?(Hash)
299
+ rescue
300
+ # Best effort — without the flag ErrorMiddleware reports the exception too.
301
+ end
302
+
282
303
  def self.safe_path(data)
283
304
  path = data[:path] || (data[:request] && data[:request].path)
284
305
  sanitize_string(path)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.34"
4
+ VERSION = "0.2.35"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.34
4
+ version: 0.2.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa