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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +2 -0
- data/lib/dead_bro/configuration.rb +8 -3
- data/lib/dead_bro/error_middleware.rb +46 -6
- data/lib/dead_bro/subscriber.rb +21 -0
- data/lib/dead_bro/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2544ca10e878c96153137ba46f40a543e157eff3c877b1bd3211eec15b01a9e7
|
|
4
|
+
data.tar.gz: 046c6f63e058e9273e8f978f42a727164c4e366c65111169c1a3d86b29ebae43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
data/lib/dead_bro/subscriber.rb
CHANGED
|
@@ -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)
|
data/lib/dead_bro/version.rb
CHANGED