rails_error_dashboard 0.11.5 → 0.11.7

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +31 -3
  4. data/app/jobs/rails_error_dashboard/baseline_calculation_job.rb +21 -0
  5. data/app/models/rails_error_dashboard/error_log.rb +4 -7
  6. data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
  7. data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +7 -0
  8. data/db/migrate/20260908000001_add_release_to_error_occurrences.rb +45 -0
  9. data/db/migrate/20260908220001_add_missing_postgresql_indexes_to_error_logs.rb +42 -0
  10. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +20 -9
  11. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +63 -28
  12. data/lib/rails_error_dashboard/commands/log_error.rb +11 -2
  13. data/lib/rails_error_dashboard/configuration.rb +7 -0
  14. data/lib/rails_error_dashboard/queries/analytics_stats.rb +4 -1
  15. data/lib/rails_error_dashboard/queries/baseline_stats.rb +39 -4
  16. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +5 -16
  17. data/lib/rails_error_dashboard/queries/release_timeline.rb +72 -1
  18. data/lib/rails_error_dashboard/services/baseline_calculator.rb +75 -62
  19. data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -0
  20. data/lib/rails_error_dashboard/services/local_variable_capturer.rb +33 -3
  21. data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +83 -22
  22. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +52 -12
  23. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +61 -2
  24. data/lib/rails_error_dashboard/value_objects/error_context.rb +19 -2
  25. data/lib/rails_error_dashboard/version.rb +1 -1
  26. data/lib/tasks/error_dashboard.rake +24 -0
  27. metadata +5 -2
@@ -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
- # - Total snapshot < 1ms budget (~0.3ms typical on Linux)
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
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.11.5"
2
+ VERSION = "0.11.7"
3
3
  end
@@ -52,6 +52,30 @@ namespace :error_dashboard do
52
52
  checks_failed += 1
53
53
  end
54
54
 
55
+ # 3b. MySQL only: named-zone conversion needs the server's time zone
56
+ # tables; without them groupdate (charts, baselines) raises.
57
+ begin
58
+ conn = RailsErrorDashboard::ErrorLogsRecord.connection
59
+ if conn.adapter_name.match?(/mysql|trilogy/i)
60
+ print " MySQL time zone tables... "
61
+ zone = (Time.zone || ActiveSupport::TimeZone["UTC"]).tzinfo.name
62
+ converted = conn.select_value("SELECT CONVERT_TZ(NOW(), '+00:00', #{conn.quote(zone)})")
63
+ if converted.nil?
64
+ puts "MISSING"
65
+ puts " CONVERT_TZ to '#{zone}' returned NULL: the server has no time zone tables,"
66
+ puts " so the dashboard's time-bucketed charts and baselines will raise."
67
+ puts " Fix (on the MySQL server): mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql"
68
+ puts " See docs/guides/DATABASE_OPTIONS.md (MySQL)."
69
+ warnings += 1
70
+ else
71
+ puts "OK (#{zone})"
72
+ checks_passed += 1
73
+ end
74
+ end
75
+ rescue => e
76
+ puts "SKIPPED (#{e.message.truncate(60)})"
77
+ end
78
+
55
79
  # 4. Tables check
56
80
  print " Required tables... "
57
81
  required_tables = %w[
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.5
4
+ version: 0.11.7
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,8 @@ 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
412
+ - db/migrate/20260908220001_add_missing_postgresql_indexes_to_error_logs.rb
410
413
  - lib/generators/rails_error_dashboard/install/install_generator.rb
411
414
  - lib/generators/rails_error_dashboard/install/templates/README
412
415
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
@@ -575,7 +578,7 @@ metadata:
575
578
  funding_uri: https://github.com/sponsors/AnjanJ
576
579
  post_install_message: |
577
580
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
578
- RED (Rails Error Dashboard) v0.11.5
581
+ RED (Rails Error Dashboard) v0.11.7
579
582
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
580
583
 
581
584
  First install: