rails_error_dashboard 0.11.4 → 0.11.6
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 +3 -2
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +31 -3
- data/app/jobs/rails_error_dashboard/baseline_calculation_job.rb +21 -0
- data/app/models/rails_error_dashboard/error_log.rb +4 -7
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/config/locales/fr.yml +30 -30
- data/db/migrate/20260908000001_add_release_to_error_occurrences.rb +45 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +20 -9
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +63 -28
- data/lib/rails_error_dashboard/commands/log_error.rb +11 -2
- data/lib/rails_error_dashboard/configuration.rb +7 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +4 -1
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +39 -4
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +5 -16
- data/lib/rails_error_dashboard/queries/release_timeline.rb +72 -1
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +75 -62
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -0
- data/lib/rails_error_dashboard/services/local_variable_capturer.rb +33 -3
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +83 -22
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +52 -12
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +61 -2
- data/lib/rails_error_dashboard/value_objects/error_context.rb +19 -2
- data/lib/rails_error_dashboard/version.rb +1 -1
- metadata +4 -2
|
@@ -155,22 +155,35 @@ module RailsErrorDashboard
|
|
|
155
155
|
# Cheap in-process bucketing key. Deliberately NOT the canonical
|
|
156
156
|
# error_hash (that needs application_id = DB); the flush job
|
|
157
157
|
# recomputes the canonical hash from the stored parts.
|
|
158
|
+
# Environment is a MATCH dimension (one row per environment), so it
|
|
159
|
+
# is part of the in-process key too: a staging event and a
|
|
160
|
+
# production event of the same error must not share one entry, or
|
|
161
|
+
# the flush job would reconcile both into whichever environment
|
|
162
|
+
# the worker runs in.
|
|
158
163
|
def gate_key(parts)
|
|
159
|
-
parts[:gate_key] ||=
|
|
160
|
-
"#{parts[:
|
|
161
|
-
"#{parts[:first_app_frame]}|#{parts[:controller_name]}|#{parts[:action_name]}"
|
|
164
|
+
parts[:gate_key] ||= Digest::SHA256.hexdigest(
|
|
165
|
+
"#{parts[:custom_hash] || identity_key(parts)}|#{parts[:environment]}"
|
|
162
166
|
)[0..15]
|
|
163
167
|
end
|
|
164
168
|
|
|
169
|
+
def identity_key(parts)
|
|
170
|
+
"#{parts[:error_class]}|#{ErrorHashGenerator.normalize_message(parts[:message])}|" \
|
|
171
|
+
"#{parts[:first_app_frame]}|#{parts[:controller_name]}|#{parts[:action_name]}"
|
|
172
|
+
end
|
|
173
|
+
|
|
165
174
|
def gate_parts(exception, context)
|
|
166
175
|
{
|
|
167
176
|
error_class: exception.class.name,
|
|
168
|
-
message: exception.message.to_s[0,
|
|
177
|
+
message: exception.message.to_s[0, ErrorHashGenerator::HASH_MESSAGE_LIMIT],
|
|
169
178
|
first_app_frame: ErrorHashGenerator.extract_app_frame_from_locations(exception) ||
|
|
170
179
|
ErrorHashGenerator.extract_app_frame(exception.backtrace),
|
|
171
180
|
controller_name: context[:controller_name]&.to_s,
|
|
172
181
|
action_name: context[:action_name]&.to_s,
|
|
173
|
-
custom_hash: custom_hash_for(exception, context)
|
|
182
|
+
custom_hash: custom_hash_for(exception, context),
|
|
183
|
+
# Only an EXPLICIT environment from the caller. nil means "the
|
|
184
|
+
# worker's own environment", resolved at flush time exactly as
|
|
185
|
+
# LogError resolves it for a full capture.
|
|
186
|
+
environment: context[:environment].to_s.strip.presence&.[](0, 64)
|
|
174
187
|
}
|
|
175
188
|
end
|
|
176
189
|
|
|
@@ -203,19 +216,46 @@ module RailsErrorDashboard
|
|
|
203
216
|
@last_flush = now
|
|
204
217
|
snapshot = count_buffer.snapshot!
|
|
205
218
|
episode = breaker.episode_snapshot
|
|
206
|
-
breaker.clear_closed_episode!
|
|
207
219
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
220
|
+
begin
|
|
221
|
+
job = StormFlushJob.perform_later(
|
|
222
|
+
entries: snapshot[:entries],
|
|
223
|
+
overflow: snapshot[:overflow],
|
|
224
|
+
episode: serialize_episode(episode)
|
|
225
|
+
)
|
|
226
|
+
# Active Job swallows ActiveJob::EnqueueError and returns false
|
|
227
|
+
# (and a job that was not enqueued says so) — a failed handoff
|
|
228
|
+
# that never raises.
|
|
229
|
+
unless enqueued?(job)
|
|
230
|
+
reason = (job.respond_to?(:enqueue_error) && job.enqueue_error&.message) || "enqueue returned #{job.inspect}"
|
|
231
|
+
raise "StormFlushJob was not enqueued: #{reason}"
|
|
232
|
+
end
|
|
233
|
+
rescue => e
|
|
234
|
+
# The queue is often the very thing that is down during a storm
|
|
235
|
+
# (a SolidQueue enqueue is a DB write). The batch is not gone:
|
|
236
|
+
# put it back so the next interval retries, and leave the
|
|
237
|
+
# closed episode in place so it is persisted by that retry.
|
|
238
|
+
count_buffer.restore(snapshot[:entries], snapshot[:overflow])
|
|
239
|
+
RailsErrorDashboard::Logger.error(
|
|
240
|
+
"[RailsErrorDashboard] Storm flush enqueue failed (batch retained for retry): #{e.class} - #{e.message}"
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
breaker.clear_closed_episode!
|
|
213
246
|
rescue => e
|
|
214
247
|
RailsErrorDashboard::Logger.error(
|
|
215
|
-
"[RailsErrorDashboard] Storm flush
|
|
248
|
+
"[RailsErrorDashboard] Storm flush failed: #{e.class} - #{e.message}"
|
|
216
249
|
)
|
|
217
250
|
end
|
|
218
251
|
|
|
252
|
+
def enqueued?(job)
|
|
253
|
+
return false unless job
|
|
254
|
+
return job.successfully_enqueued? if job.respond_to?(:successfully_enqueued?)
|
|
255
|
+
|
|
256
|
+
true
|
|
257
|
+
end
|
|
258
|
+
|
|
219
259
|
def serialize_episode(episode)
|
|
220
260
|
return nil unless episode
|
|
221
261
|
|
|
@@ -8,11 +8,18 @@ module RailsErrorDashboard
|
|
|
8
8
|
# Puma stats, job queue, RubyVM/YJIT, ActionCable, file descriptors, system load,
|
|
9
9
|
# system memory pressure, GC context, and TCP connection states.
|
|
10
10
|
#
|
|
11
|
-
# NOT memoized — fresh data every call (unlike EnvironmentSnapshot)
|
|
11
|
+
# NOT memoized — fresh data every call (unlike EnvironmentSnapshot), with
|
|
12
|
+
# one exception: job-queue depth counts are queries against the queue
|
|
13
|
+
# store, so they are cached per process for
|
|
14
|
+
# config.system_health_queue_stats_cache_seconds (failures included), with
|
|
15
|
+
# one refresh in flight at a time (and can be switched off with
|
|
16
|
+
# config.system_health_queue_stats = false).
|
|
12
17
|
# Every metric call individually wrapped in rescue => nil.
|
|
13
18
|
#
|
|
14
19
|
# Safety contract (from HOST_APP_SAFETY.md):
|
|
15
|
-
# -
|
|
20
|
+
# - In-process metrics < 1ms budget (~0.3ms typical on Linux). The
|
|
21
|
+
# queue-depth counts are the documented exception: their latency is
|
|
22
|
+
# the queue store's, which is why they are cached and optional.
|
|
16
23
|
# - NEVER ObjectSpace.each_object or ObjectSpace.count_objects (heap scan)
|
|
17
24
|
# - NEVER Thread.list.map(&:backtrace) (GVL hold)
|
|
18
25
|
# - Thread.list.count only (O(1), safe)
|
|
@@ -133,7 +140,59 @@ module RailsErrorDashboard
|
|
|
133
140
|
|
|
134
141
|
# Auto-detect and capture job queue stats
|
|
135
142
|
# @return [Hash, nil] Job queue stats with :adapter key, or nil
|
|
143
|
+
# Process-wide cache for the queue-depth counts: { at: monotonic, stats: Hash|nil }.
|
|
144
|
+
# A nil stats entry is a FAILED collection, cached for the same interval
|
|
145
|
+
# so a broken queue store is not re-queried on every error.
|
|
146
|
+
def self.queue_stats_cache
|
|
147
|
+
@queue_stats_cache ||= Concurrent::AtomicReference.new(nil)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Single-flight guard for the refresh. Never blocks: a thread that finds
|
|
151
|
+
# a refresh already running serves the stale entry (or nil) instead of
|
|
152
|
+
# starting a second one, so a burst of errors on a cold cache runs the
|
|
153
|
+
# queue counts once, not once per thread.
|
|
154
|
+
def self.queue_stats_refresh_lock
|
|
155
|
+
@queue_stats_refresh_lock ||= Mutex.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.reset_queue_stats_cache!
|
|
159
|
+
queue_stats_cache.set(nil)
|
|
160
|
+
end
|
|
161
|
+
|
|
136
162
|
def job_queue_stats
|
|
163
|
+
config = RailsErrorDashboard.configuration
|
|
164
|
+
return nil unless config.system_health_queue_stats
|
|
165
|
+
|
|
166
|
+
ttl = config.system_health_queue_stats_cache_seconds.to_f
|
|
167
|
+
return collect_job_queue_stats if ttl <= 0
|
|
168
|
+
|
|
169
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
170
|
+
cached = self.class.queue_stats_cache.get
|
|
171
|
+
return present_cached(cached, now) if cached && (now - cached[:at]) < ttl
|
|
172
|
+
|
|
173
|
+
lock = self.class.queue_stats_refresh_lock
|
|
174
|
+
return present_cached(cached, now, stale: true) unless lock.try_lock
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
stats = collect_job_queue_stats
|
|
178
|
+
self.class.queue_stats_cache.set({ at: now, stats: stats })
|
|
179
|
+
stats
|
|
180
|
+
ensure
|
|
181
|
+
lock.unlock
|
|
182
|
+
end
|
|
183
|
+
rescue => e
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def present_cached(cached, now, stale: false)
|
|
188
|
+
return nil unless cached && cached[:stats]
|
|
189
|
+
|
|
190
|
+
presented = cached[:stats].merge(cached_age_seconds: (now - cached[:at]).round(1))
|
|
191
|
+
presented[:stale] = true if stale
|
|
192
|
+
presented
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def collect_job_queue_stats
|
|
137
196
|
if defined?(::Sidekiq::Stats)
|
|
138
197
|
sidekiq_stats
|
|
139
198
|
elsif defined?(::SolidQueue)
|
|
@@ -7,7 +7,7 @@ module RailsErrorDashboard
|
|
|
7
7
|
class ErrorContext
|
|
8
8
|
attr_reader :user_id, :request_url, :request_params, :user_agent, :ip_address, :platform,
|
|
9
9
|
:controller_name, :action_name, :request_id, :session_id,
|
|
10
|
-
:http_method, :hostname, :content_type, :request_duration_ms
|
|
10
|
+
:http_method, :hostname, :content_type, :request_duration_ms, :environment
|
|
11
11
|
|
|
12
12
|
def initialize(context, source = nil)
|
|
13
13
|
@context = context
|
|
@@ -27,8 +27,15 @@ module RailsErrorDashboard
|
|
|
27
27
|
@hostname = extract_hostname
|
|
28
28
|
@content_type = extract_content_type
|
|
29
29
|
@request_duration_ms = extract_request_duration_ms
|
|
30
|
+
@environment = extract_environment
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Everything a second ErrorContext needs to rebuild THIS context from a
|
|
34
|
+
# plain hash. ErrorReporter hands `to_h` to LogError, which constructs a
|
|
35
|
+
# new ErrorContext from it, so any reader that is not represented here
|
|
36
|
+
# is silently nil on the far side of that hop. request_id/session_id
|
|
37
|
+
# were exactly that: extracted from the request, dropped here, and every
|
|
38
|
+
# occurrence on the main capture path stored nil for both.
|
|
32
39
|
def to_h
|
|
33
40
|
{
|
|
34
41
|
user_id: user_id,
|
|
@@ -39,10 +46,13 @@ module RailsErrorDashboard
|
|
|
39
46
|
platform: platform,
|
|
40
47
|
controller_name: controller_name,
|
|
41
48
|
action_name: action_name,
|
|
49
|
+
request_id: request_id,
|
|
50
|
+
session_id: session_id,
|
|
42
51
|
http_method: http_method,
|
|
43
52
|
hostname: hostname,
|
|
44
53
|
content_type: content_type,
|
|
45
|
-
request_duration_ms: request_duration_ms
|
|
54
|
+
request_duration_ms: request_duration_ms,
|
|
55
|
+
environment: environment
|
|
46
56
|
}
|
|
47
57
|
end
|
|
48
58
|
|
|
@@ -246,6 +256,13 @@ module RailsErrorDashboard
|
|
|
246
256
|
nil
|
|
247
257
|
end
|
|
248
258
|
|
|
259
|
+
# An explicit environment from the caller (a sender attributing an event
|
|
260
|
+
# to its own environment). nil means "resolve from configuration".
|
|
261
|
+
def extract_environment
|
|
262
|
+
value = @context[:environment].to_s.strip
|
|
263
|
+
value.empty? ? nil : value
|
|
264
|
+
end
|
|
265
|
+
|
|
249
266
|
# Auto-detect user_id from ActiveSupport::CurrentAttributes
|
|
250
267
|
# Checks for common patterns: Current.user, Current.account (with .id)
|
|
251
268
|
# Returns nil if CurrentAttributes is not used or user is not set
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -279,6 +279,7 @@ files:
|
|
|
279
279
|
- app/jobs/rails_error_dashboard/application_job.rb
|
|
280
280
|
- app/jobs/rails_error_dashboard/async_error_logging_job.rb
|
|
281
281
|
- app/jobs/rails_error_dashboard/baseline_alert_job.rb
|
|
282
|
+
- app/jobs/rails_error_dashboard/baseline_calculation_job.rb
|
|
282
283
|
- app/jobs/rails_error_dashboard/close_linked_issue_job.rb
|
|
283
284
|
- app/jobs/rails_error_dashboard/concerns/localized_job.rb
|
|
284
285
|
- app/jobs/rails_error_dashboard/create_issue_job.rb
|
|
@@ -407,6 +408,7 @@ files:
|
|
|
407
408
|
- db/migrate/20260730000001_create_rails_error_dashboard_rack_attack_events.rb
|
|
408
409
|
- db/migrate/20260824000001_add_user_agent_to_rack_attack_events.rb
|
|
409
410
|
- db/migrate/20260826000001_add_environment_to_error_logs.rb
|
|
411
|
+
- db/migrate/20260908000001_add_release_to_error_occurrences.rb
|
|
410
412
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
411
413
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
412
414
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -575,7 +577,7 @@ metadata:
|
|
|
575
577
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
576
578
|
post_install_message: |
|
|
577
579
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
578
|
-
RED (Rails Error Dashboard) v0.11.
|
|
580
|
+
RED (Rails Error Dashboard) v0.11.6
|
|
579
581
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
580
582
|
|
|
581
583
|
First install:
|