rails_error_dashboard 0.6.3 → 0.6.4

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +8 -2
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +25 -14
  5. data/app/helpers/rails_error_dashboard/application_helper.rb +40 -10
  6. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +1 -0
  7. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -2
  8. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +1 -0
  9. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +1 -0
  10. data/app/views/rails_error_dashboard/errors/_modals.html.erb +1 -1
  11. data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +21 -20
  12. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +33 -19
  13. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -2
  14. data/app/views/rails_error_dashboard/errors/analytics.html.erb +5 -1
  15. data/app/views/rails_error_dashboard/errors/correlation.html.erb +16 -4
  16. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +7 -3
  17. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
  18. data/app/views/rails_error_dashboard/errors/index.html.erb +7 -1
  19. data/app/views/rails_error_dashboard/errors/overview.html.erb +2 -2
  20. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +3 -1
  21. data/app/views/rails_error_dashboard/errors/releases.html.erb +3 -1
  22. data/app/views/rails_error_dashboard/errors/settings.html.erb +0 -1
  23. data/app/views/rails_error_dashboard/errors/show.html.erb +4 -2
  24. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +5 -1
  25. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +3 -1
  26. data/lib/rails_error_dashboard/queries/analytics_stats.rb +4 -1
  27. data/lib/rails_error_dashboard/queries/error_correlation.rb +17 -13
  28. data/lib/rails_error_dashboard/queries/errors_list.rb +14 -0
  29. data/lib/rails_error_dashboard/services/cascade_detector.rb +28 -18
  30. data/lib/rails_error_dashboard/version.rb +1 -1
  31. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 938ab24cad38b8b20bc0dd0f2a1eb6e5c9e46a4634fb97e2c01b3487221c9fc3
4
- data.tar.gz: 6e053a3f8c41e3c3f75f95ab1f9bd8908e9bcea4c473e2d1d3a0c11b7bdf0bdb
3
+ metadata.gz: 3d2151cd1323d1261e27376d1d7d8eb9f47fcb6ec171fcc6c3dbab36fcf52f6a
4
+ data.tar.gz: 68367fa080c8532c9e69f3aecf13eef84ef8638ac91f83a586cce56341f38b1c
5
5
  SHA512:
6
- metadata.gz: 2b84b4a4f917863e7577e253f2324100a06b6fef6f33d56cef211f28a4f1dc4688932f618489f19924baa8aa065135cfcaeffb2c7431f15bea898d3e334395b7
7
- data.tar.gz: 4149d8da317c77751e04db12b46f02924fe46e1a420075232e3fa7c96f12218166249596b33a55f1790e696b5fca3580cd5da62d38a39585e356660a702a518c
6
+ metadata.gz: fd3167ec31230e80ca89eb723b78f4d97f073a2c68efa4fbc9fe83e85bfa6b1361fdc43f1f822fb1eadd9d00254d12f105e863d09825843bdeb14386a1cef95a
7
+ data.tar.gz: da46299157c3cc47a45e55d006ff6657f4d9728832fefc0c051b8e94c1c919be6320b22663742a94ebfd1f8df8cd93e19ac27e9d8e0f45e578a6a2f6d047712e
data/README.md CHANGED
@@ -63,7 +63,7 @@ gem 'rails_error_dashboard'
63
63
 
64
64
  ### Core (Always Enabled)
65
65
 
66
- Error capture from controllers, jobs, and middleware. Beautiful Bootstrap 5 dashboard with dark/light mode, search, filtering, and real-time updates. Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, mute/unmute (notification suppression), comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
66
+ Error capture from controllers, jobs, and middleware. Custom-designed dashboard with dark/light mode, search, filtering, and real-time updates. Analytics with trend charts, severity breakdown, and spike detection. Workflow management with assignment, priority, snooze, mute/unmute (notification suppression), comments, and batch operations. Security via HTTP Basic Auth or custom lambda (Devise, Warden, session-based). Exception cause chains, enriched HTTP context, custom fingerprinting, CurrentAttributes integration, auto-reopen on recurrence, and sensitive data filtering — all built in.
67
67
 
68
68
  ### Optional Features
69
69
 
@@ -521,7 +521,7 @@ Available as open source under the [MIT License](https://opensource.org/licenses
521
521
 
522
522
  ## Acknowledgments
523
523
 
524
- Built with [Rails](https://rubyonrails.org/) · UI by [Bootstrap 5](https://getbootstrap.com/) · Charts by [Chart.js](https://www.chartjs.org/) · Pagination by [Pagy](https://github.com/ddnexus/pagy) · Docs theme by [Jekyll VitePress Theme](https://jekyll-vitepress.dev/) by [@crmne](https://github.com/crmne)
524
+ Built with [Rails](https://rubyonrails.org/) · Custom design tokens with [Bootstrap 5 JS](https://getbootstrap.com/) for tooltips and modals · Charts by [Chart.js](https://www.chartjs.org/) · Pagination by [Pagy](https://github.com/ddnexus/pagy) · Docs theme by [Jekyll VitePress Theme](https://jekyll-vitepress.dev/) by [@crmne](https://github.com/crmne)
525
525
 
526
526
  ## Contributors
527
527
 
@@ -46,10 +46,16 @@ module RailsErrorDashboard
46
46
  )
47
47
  end
48
48
 
49
- # Handle Pagy pagination errors — redirect to page 1
49
+ # Handle Pagy pagination errors — redirect to page 1, preserving filters.
50
+ # Drop both :page and :per_page from the preserved query string. Either can
51
+ # trigger the rescue (page out of range, per_page negative or non-numeric);
52
+ # carrying them into the redirect would loop the user right back into the
53
+ # same error.
50
54
  rescue_from Pagy::RangeError, Pagy::OptionError do |exception|
51
55
  Rails.logger.warn("[RailsErrorDashboard] Pagination error: #{exception.message}")
52
- redirect_to request.path, status: :moved_permanently
56
+ preserved = request.query_parameters.except("page", :page, "per_page", :per_page)
57
+ target = preserved.any? ? "#{request.path}?#{preserved.to_query}" : request.path
58
+ redirect_to target, status: :moved_permanently
53
59
  end
54
60
 
55
61
  private
@@ -22,6 +22,9 @@ module RailsErrorDashboard
22
22
  hide_snoozed
23
23
  hide_muted
24
24
  reopened
25
+ user_id
26
+ app_version
27
+ git_sha
25
28
  sort_by
26
29
  sort_direction
27
30
  ].freeze
@@ -171,7 +174,7 @@ module RailsErrorDashboard
171
174
  end
172
175
 
173
176
  def analytics
174
- days = (params[:days] || 30).to_i
177
+ days = days_param(default: 30)
175
178
  @days = days
176
179
 
177
180
  # Use Query to get analytics data (pass application filter)
@@ -212,7 +215,7 @@ module RailsErrorDashboard
212
215
  return
213
216
  end
214
217
 
215
- days = (params[:days] || 7).to_i
218
+ days = days_param(default: 7)
216
219
  @days = days
217
220
 
218
221
  # Use Query to get platform comparison data (pass application filter)
@@ -266,7 +269,7 @@ module RailsErrorDashboard
266
269
  return
267
270
  end
268
271
 
269
- days = (params[:days] || 30).to_i
272
+ days = days_param(default: 30)
270
273
  @days = days
271
274
  correlation = Queries::ErrorCorrelation.new(days: days, application_id: @current_application_id)
272
275
 
@@ -280,7 +283,7 @@ module RailsErrorDashboard
280
283
  end
281
284
 
282
285
  def releases
283
- days = (params[:days] || 30).to_i
286
+ days = days_param(default: 30)
284
287
  @days = days
285
288
  result = Queries::ReleaseTimeline.call(days, application_id: @current_application_id)
286
289
  all_releases = result[:releases]
@@ -290,7 +293,7 @@ module RailsErrorDashboard
290
293
  end
291
294
 
292
295
  def user_impact
293
- days = (params[:days] || 30).to_i
296
+ days = days_param(default: 30)
294
297
  @days = days
295
298
  result = Queries::UserImpactSummary.call(days, application_id: @current_application_id)
296
299
  all_entries = result[:entries]
@@ -306,7 +309,7 @@ module RailsErrorDashboard
306
309
  return
307
310
  end
308
311
 
309
- days = (params[:days] || 30).to_i
312
+ days = days_param(default: 30)
310
313
  @days = days
311
314
  result = Queries::DeprecationWarnings.call(days, application_id: @current_application_id)
312
315
  all_deprecations = result[:deprecations]
@@ -326,7 +329,7 @@ module RailsErrorDashboard
326
329
  return
327
330
  end
328
331
 
329
- days = (params[:days] || 30).to_i
332
+ days = days_param(default: 30)
330
333
  @days = days
331
334
  result = Queries::NplusOneSummary.call(days, application_id: @current_application_id)
332
335
  all_patterns = result[:patterns]
@@ -346,7 +349,7 @@ module RailsErrorDashboard
346
349
  return
347
350
  end
348
351
 
349
- days = (params[:days] || 30).to_i
352
+ days = days_param(default: 30)
350
353
  @days = days
351
354
  result = Queries::CacheHealthSummary.call(days, application_id: @current_application_id)
352
355
  all_entries = result[:entries]
@@ -367,7 +370,7 @@ module RailsErrorDashboard
367
370
  return
368
371
  end
369
372
 
370
- days = (params[:days] || 30).to_i
373
+ days = days_param(default: 30)
371
374
  @days = days
372
375
  result = Queries::JobHealthSummary.call(days, application_id: @current_application_id)
373
376
  all_entries = result[:entries]
@@ -387,7 +390,7 @@ module RailsErrorDashboard
387
390
  return
388
391
  end
389
392
 
390
- days = (params[:days] || 30).to_i
393
+ days = days_param(default: 30)
391
394
  @days = days
392
395
 
393
396
  # Live database health (display-time only)
@@ -423,7 +426,7 @@ module RailsErrorDashboard
423
426
  return
424
427
  end
425
428
 
426
- days = (params[:days] || 30).to_i
429
+ days = days_param(default: 30)
427
430
  @days = days
428
431
  result = Queries::SwallowedExceptionSummary.call(days, application_id: @current_application_id)
429
432
  all_entries = result[:entries]
@@ -444,7 +447,7 @@ module RailsErrorDashboard
444
447
  return
445
448
  end
446
449
 
447
- days = (params[:days] || 30).to_i
450
+ days = days_param(default: 30)
448
451
  @days = days
449
452
  result = Queries::RackAttackSummary.call(days, application_id: @current_application_id)
450
453
  all_events = result[:events]
@@ -465,7 +468,7 @@ module RailsErrorDashboard
465
468
  return
466
469
  end
467
470
 
468
- days = (params[:days] || 30).to_i
471
+ days = days_param(default: 30)
469
472
  @days = days
470
473
  result = Queries::ActionCableSummary.call(days, application_id: @current_application_id)
471
474
  all_channels = result[:channels]
@@ -486,7 +489,7 @@ module RailsErrorDashboard
486
489
  return
487
490
  end
488
491
 
489
- days = (params[:days] || 30).to_i
492
+ days = days_param(default: 30)
490
493
  @days = days
491
494
  result = Queries::ActiveStorageSummary.call(days, application_id: @current_application_id)
492
495
  all_services = result[:services]
@@ -610,6 +613,14 @@ module RailsErrorDashboard
610
613
  params.permit(*FILTERABLE_PARAMS).to_h.symbolize_keys
611
614
  end
612
615
 
616
+ # Coerce params[:days] into a sane integer in [1, 365]. Without clamping,
617
+ # a request like ?days=99999999 would scan the full table on every health
618
+ # query, defeating index pruning and burning CPU.
619
+ def days_param(default:)
620
+ raw = params[:days].presence || default
621
+ raw.to_i.clamp(1, 365)
622
+ end
623
+
613
624
  def set_application_context
614
625
  @current_application_id = params[:application_id].presence
615
626
  @applications = Application.ordered_by_name.pluck(:name, :id)
@@ -27,6 +27,16 @@ module RailsErrorDashboard
27
27
  end
28
28
  end
29
29
 
30
+ # Serialize a value to JSON safely for inlining inside a <script> block.
31
+ # Ruby's #to_json escapes JSON special chars but does NOT escape "</" — a
32
+ # value containing the literal string "</script>" would break out of the
33
+ # surrounding <script> tag. Replace "</" with "<\/" (semantically equivalent
34
+ # in JSON and in JavaScript string literals) to neutralize the close tag.
35
+ # Returns html_safe for direct interpolation into a script body.
36
+ def js_safe_json(value)
37
+ value.to_json.gsub("</", '<\/').html_safe
38
+ end
39
+
30
40
  # Returns Bootstrap color class for error severity
31
41
  # Uses Catppuccin Mocha colors in dark theme via CSS variables
32
42
  # @param severity [Symbol] The severity level (:critical, :high, :medium, :low, :info)
@@ -209,6 +219,17 @@ module RailsErrorDashboard
209
219
  )
210
220
  end
211
221
 
222
+ # Raw connection.select_all returns timestamps as Time in some PG configs
223
+ # and as ISO8601 strings in others — accept both shapes.
224
+ def parse_pg_timestamp(value)
225
+ return nil if value.blank?
226
+ return value if value.is_a?(Time) || value.is_a?(DateTime)
227
+
228
+ Time.parse(value.to_s)
229
+ rescue ArgumentError
230
+ nil
231
+ end
232
+
212
233
  # Renders a relative time ("3 hours ago") that updates automatically
213
234
  # @param time [Time, DateTime, nil] The timestamp to display
214
235
  # @param fallback [String] Text to show if time is nil
@@ -265,6 +286,13 @@ module RailsErrorDashboard
265
286
  def auto_link_urls(text, error: nil)
266
287
  return "" if text.blank?
267
288
 
289
+ # SECURITY: escape HTML special chars in the input before any further
290
+ # processing. simple_format(..., sanitize: false) at the end means
291
+ # whatever survives this pipeline is rendered as raw HTML; any unescaped
292
+ # `<script>` or `<img onerror=>` in the user's text would XSS.
293
+ # We only intentionally inject our own <a> / <code> tags after this point.
294
+ text = ERB::Util.html_escape(text)
295
+
268
296
  # Get repository URL from error's application or global config
269
297
  repo_url = if error&.application.respond_to?(:repository_url) && error.application.repository_url.present?
270
298
  error.application.repository_url
@@ -302,32 +330,34 @@ module RailsErrorDashboard
302
330
  )
303
331
  }xi
304
332
 
305
- # Replace URLs with clickable links
333
+ # Replace URLs with clickable links. The url, code_content, and file_path
334
+ # values below are slices of `text` which we already escaped at function
335
+ # entry, so we don't re-escape them here (would double-escape `&amp;`,
336
+ # breaking visual rendering). repo_url is from config so we escape it
337
+ # explicitly before interpolating.
338
+ escaped_repo_url = repo_url ? ERB::Util.html_escape(repo_url.chomp("/")) : nil
339
+
306
340
  linked_text = text_with_placeholders.gsub(url_regex) do |url|
307
- # Clean up the URL
308
341
  clean_url = url.strip
309
342
 
310
- # Add protocol if missing
311
343
  href = clean_url.start_with?("http://", "https://") ? clean_url : "https://#{clean_url}"
312
344
 
313
- # Truncate display text for very long URLs
314
345
  display_text = clean_url.length > 60 ? "#{clean_url[0..57]}..." : clean_url
315
346
 
316
- "<a href=\"#{ERB::Util.html_escape(href)}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary text-decoration-underline\">#{ERB::Util.html_escape(display_text)}</a>"
347
+ "<a href=\"#{href}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary text-decoration-underline\">#{display_text}</a>"
317
348
  end
318
349
 
319
- # Restore file paths with GitHub links (elvish magic! 🧝‍♀️)
350
+ # Restore file paths with GitHub links
320
351
  linked_text.gsub!(/###FILE_PATH_(\d+)###/) do
321
352
  file_path = file_paths[Regexp.last_match(1).to_i]
322
- github_url = "#{repo_url.chomp('/')}/blob/main/#{file_path}"
323
- "<a href=\"#{ERB::Util.html_escape(github_url)}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-decoration-none\" title=\"View on GitHub\">" \
324
- "<code class=\"inline-code-highlight file-path-link\">#{ERB::Util.html_escape(file_path)}</code></a>"
353
+ "<a href=\"#{escaped_repo_url}/blob/main/#{file_path}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-decoration-none\" title=\"View on GitHub\">" \
354
+ "<code class=\"inline-code-highlight file-path-link\">#{file_path}</code></a>"
325
355
  end
326
356
 
327
357
  # Restore code blocks with styling
328
358
  linked_text.gsub!(/###CODE_BLOCK_(\d+)###/) do
329
359
  code_content = code_blocks[Regexp.last_match(1).to_i]
330
- "<code class=\"inline-code-highlight\">#{ERB::Util.html_escape(code_content)}</code>"
360
+ "<code class=\"inline-code-highlight\">#{code_content}</code>"
331
361
  end
332
362
 
333
363
  # Preserve line breaks and return as HTML safe
@@ -1,6 +1,7 @@
1
1
  <!-- Breadcrumbs (Request Activity Trail) -->
2
2
  <% if RailsErrorDashboard.configuration.enable_breadcrumbs && error.respond_to?(:breadcrumbs) && error.breadcrumbs.present? %>
3
3
  <% breadcrumbs = JSON.parse(error.breadcrumbs) rescue [] %>
4
+ <% breadcrumbs = [] unless breadcrumbs.is_a?(Array) %>
4
5
  <% if breadcrumbs.any? %>
5
6
  <% deprecation_crumbs = breadcrumbs.select { |c| c["c"] == "deprecation" } %>
6
7
  <% if deprecation_crumbs.any? %>
@@ -57,11 +57,11 @@
57
57
  <% end %>
58
58
  </td>
59
59
  <td style="padding: var(--space-3) var(--space-4); text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--text-primary);">
60
- <%= error.occurrence_count.to_s(:delimited) rescue error.occurrence_count %>
60
+ <%= number_with_delimiter(error.occurrence_count) %>
61
61
  </td>
62
62
  <td style="padding: var(--space-3) var(--space-4); text-align: right; font-variant-numeric: tabular-nums; color: var(--text-secondary);">
63
63
  <% if error.user_id %>
64
- <%= error.user_id %>
64
+ <%= link_to error.user_id, errors_path(user_id: error.user_id, unresolved: '0'), style: "color: inherit; text-decoration: none;", title: "View all errors for user #{error.user_id}" %>
65
65
  <% else %>
66
66
  <span style="color: var(--text-tertiary);">&mdash;</span>
67
67
  <% end %>
@@ -1,6 +1,7 @@
1
1
  <!-- Instance Variables (captured from receiver object at raise time) -->
2
2
  <% if RailsErrorDashboard.configuration.enable_instance_variables && error.class.column_names.include?("instance_variables") && error.read_attribute(:instance_variables).present? %>
3
3
  <% instance_vars = JSON.parse(error.read_attribute(:instance_variables)) rescue {} %>
4
+ <% instance_vars = {} unless instance_vars.is_a?(Hash) %>
4
5
  <% self_class = instance_vars.delete("_self_class") %>
5
6
  <% if instance_vars.any? %>
6
7
  <div class="card mb-4" id="section-instance-variables">
@@ -1,6 +1,7 @@
1
1
  <!-- Local Variables (captured via TracePoint) -->
2
2
  <% if RailsErrorDashboard.configuration.enable_local_variables && error.respond_to?(:local_variables) && error.local_variables.present? %>
3
3
  <% local_vars = JSON.parse(error.local_variables) rescue {} %>
4
+ <% local_vars = {} unless local_vars.is_a?(Hash) %>
4
5
  <% if local_vars.any? %>
5
6
  <div class="card mb-4" id="section-local-variables">
6
7
  <div class="card-header">
@@ -2,7 +2,7 @@
2
2
  <div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
3
3
  <div class="modal-dialog">
4
4
  <div class="modal-content">
5
- <%= form_with url: resolve_error_path(error), method: :post, class: "modal-content" do |f| %>
5
+ <%= form_with url: resolve_error_path(error), method: :post do |f| %>
6
6
  <div class="modal-header">
7
7
  <h5 class="modal-title" id="resolveModalLabel">
8
8
  <i class="bi bi-check-circle"></i> Mark Error as Resolved
@@ -1,32 +1,33 @@
1
1
  <script<%= " nonce=\"#{red_csp_nonce}\"".html_safe if red_csp_nonce %>>
2
2
  window.downloadErrorJSON = function(event) {
3
3
  const errorData = {
4
- id: <%= raw error.id.to_json %>,
5
- error_type: <%= raw error.error_type.to_json %>,
6
- message: <%= raw error.message.to_json %>,
7
- backtrace: <%= raw error.backtrace.to_json %>,
8
- occurred_at: <%= raw error.occurred_at.to_json %>,
9
- first_seen_at: <%= raw error.first_seen_at.to_json %>,
10
- last_seen_at: <%= raw error.last_seen_at.to_json %>,
11
- occurrence_count: <%= raw error.occurrence_count.to_json %>,
12
- resolved: <%= raw error.resolved?.to_json %>,
13
- resolved_at: <%= raw error.resolved_at.to_json %>,
14
- resolved_by_name: <%= raw error.resolved_by_name.to_json %>,
15
- platform: <%= raw error.platform.to_json %>,
16
- user_id: <%= raw error.user_id.to_json %>,
17
- severity: <%= raw error.severity.to_json %>,
18
- priority_level: <%= raw (error.priority_level || 0).to_json %>,
4
+ id: <%= js_safe_json(error.id) %>,
5
+ error_type: <%= js_safe_json(error.error_type) %>,
6
+ message: <%= js_safe_json(error.message) %>,
7
+ backtrace: <%= js_safe_json(error.backtrace) %>,
8
+ occurred_at: <%= js_safe_json(error.occurred_at) %>,
9
+ first_seen_at: <%= js_safe_json(error.first_seen_at) %>,
10
+ last_seen_at: <%= js_safe_json(error.last_seen_at) %>,
11
+ occurrence_count: <%= js_safe_json(error.occurrence_count) %>,
12
+ resolved: <%= js_safe_json(error.resolved?) %>,
13
+ resolved_at: <%= js_safe_json(error.resolved_at) %>,
14
+ resolved_by_name: <%= js_safe_json(error.resolved_by_name) %>,
15
+ platform: <%= js_safe_json(error.platform) %>,
16
+ user_id: <%= js_safe_json(error.user_id) %>,
17
+ severity: <%= js_safe_json(error.severity) %>,
18
+ priority_level: <%= js_safe_json(error.priority_level || 0) %>,
19
19
  <% if error.respond_to?(:exception_cause) && error.exception_cause.present? %>
20
- exception_cause: <%= raw error.exception_cause %>,
20
+ <% parsed_cause = (JSON.parse(error.exception_cause) rescue nil) %>
21
+ exception_cause: <%= js_safe_json(parsed_cause) %>,
21
22
  <% end %>
22
23
  <% if error.respond_to?(:app_version) %>
23
- app_version: <%= raw error.app_version.to_json %>,
24
+ app_version: <%= js_safe_json(error.app_version) %>,
24
25
  <% end %>
25
26
  <% if error.respond_to?(:git_commit) %>
26
- git_commit: <%= raw error.git_commit.to_json %>,
27
+ git_commit: <%= js_safe_json(error.git_commit) %>,
27
28
  <% end %>
28
- created_at: <%= raw error.created_at.to_json %>,
29
- updated_at: <%= raw error.updated_at.to_json %>
29
+ created_at: <%= js_safe_json(error.created_at) %>,
30
+ updated_at: <%= js_safe_json(error.updated_at) %>
30
31
  };
31
32
 
32
33
  const jsonString = JSON.stringify(errorData, null, 2);
@@ -35,37 +35,49 @@
35
35
  <div class="mb-3">
36
36
  <small class="metadata-label d-block mb-1">Severity Level</small>
37
37
  <% severity = error.severity %>
38
- <% if severity == :critical %>
39
- <span class="badge bg-danger fs-6">CRITICAL</span>
40
- <% elsif severity == :high %>
41
- <span class="badge bg-warning text-dark fs-6">HIGH</span>
42
- <% elsif severity == :medium %>
43
- <span class="badge bg-info text-dark fs-6">MEDIUM</span>
44
- <% else %>
45
- <span class="badge bg-secondary fs-6">LOW</span>
38
+ <%= link_to errors_path(severity: severity, unresolved: '0'), class: "text-decoration-none", title: "View all #{severity} errors" do %>
39
+ <% if severity == :critical %>
40
+ <span class="badge bg-danger fs-6">CRITICAL</span>
41
+ <% elsif severity == :high %>
42
+ <span class="badge bg-warning text-dark fs-6">HIGH</span>
43
+ <% elsif severity == :medium %>
44
+ <span class="badge bg-info text-dark fs-6">MEDIUM</span>
45
+ <% else %>
46
+ <span class="badge bg-secondary fs-6">LOW</span>
47
+ <% end %>
46
48
  <% end %>
47
49
  </div>
48
50
 
49
51
  <div class="mb-3">
50
52
  <small class="metadata-label d-block mb-1">Platform</small>
51
- <% if error.platform == 'iOS' %>
52
- <span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
53
- <% elsif error.platform == 'Android' %>
54
- <span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
55
- <% elsif error.platform == 'Web' %>
56
- <span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
53
+ <% if error.platform.present? %>
54
+ <%= link_to errors_path(platform: error.platform, unresolved: '0'), class: "text-decoration-none", title: "View all #{error.platform} errors" do %>
55
+ <% if error.platform == 'iOS' %>
56
+ <span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
57
+ <% elsif error.platform == 'Android' %>
58
+ <span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
59
+ <% elsif error.platform == 'Web' %>
60
+ <span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
61
+ <% else %>
62
+ <span class="badge badge-api"><i class="bi bi-server"></i> <%= error.platform %></span>
63
+ <% end %>
64
+ <% end %>
57
65
  <% else %>
58
- <span class="badge badge-api"><i class="bi bi-server"></i> <%= error.platform || 'API' %></span>
66
+ <span class="badge badge-api"><i class="bi bi-server"></i> API</span>
59
67
  <% end %>
60
68
  </div>
61
69
 
62
70
  <div class="mb-3">
63
71
  <small class="metadata-label d-block mb-1">User</small>
64
72
  <% if error.respond_to?(:user) && error.user %>
65
- <strong><%= error.user.email %></strong><br>
66
- <small class="text-muted">ID: <%= error.user_id %></small>
73
+ <%= link_to errors_path(user_id: error.user_id, unresolved: '0'), class: "text-decoration-none", title: "View all errors for this user" do %>
74
+ <strong><%= error.user.email %></strong><br>
75
+ <small class="text-muted">ID: <%= error.user_id %></small>
76
+ <% end %>
67
77
  <% elsif error.user_id %>
68
- <span class="text-muted">User ID: <%= error.user_id %></span>
78
+ <%= link_to errors_path(user_id: error.user_id, unresolved: '0'), class: "text-decoration-none text-muted", title: "View all errors for this user" do %>
79
+ User ID: <%= error.user_id %>
80
+ <% end %>
69
81
  <% else %>
70
82
  <span class="text-muted">Guest / Unauthenticated</span>
71
83
  <% end %>
@@ -249,7 +261,9 @@
249
261
  <% if error.app_version.present? %>
250
262
  <div class="mb-1">
251
263
  <small class="text-muted">App Version:</small>
252
- <code class="ms-1"><%= error.app_version %></code>
264
+ <%= link_to errors_path(app_version: error.app_version, unresolved: '0'), class: "ms-1 text-decoration-none", title: "View all errors in #{error.app_version}" do %>
265
+ <code><%= error.app_version %></code>
266
+ <% end %>
253
267
  </div>
254
268
  <% end %>
255
269
 
@@ -78,8 +78,7 @@
78
78
  <% if error.resolution_comment.present? %>
79
79
  data-bs-toggle="tooltip"
80
80
  data-bs-placement="top"
81
- data-bs-html="true"
82
- title="<strong>Resolution Notes:</strong><br><%= error.resolution_comment.truncate(150).gsub('"', '&quot;') %>"
81
+ title="Resolution Notes: <%= error.resolution_comment.truncate(150) %>"
83
82
  <% end %>>
84
83
  <i class="bi bi-check-circle"></i> Resolved
85
84
  </span>
@@ -339,7 +339,11 @@
339
339
  <tbody>
340
340
  <% @recurring_data[:high_frequency_errors].first(10).each do |error| %>
341
341
  <tr>
342
- <td><code class="small"><%= error[:error_type] %></code></td>
342
+ <td>
343
+ <%= link_to errors_path(error_type: error[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
344
+ <code class="small"><%= error[:error_type] %></code>
345
+ <% end %>
346
+ </td>
343
347
  <td><span class="badge bg-danger"><%= error[:total_occurrences] %></span></td>
344
348
  <td><%= error[:duration_days] %> days</td>
345
349
  <td><small class="text-muted"><%= local_time(error[:first_seen], format: :date_only) %></small></td>
@@ -90,7 +90,11 @@
90
90
  <tbody>
91
91
  <% @problematic_releases.each do |release| %>
92
92
  <tr>
93
- <td><code><%= release[:version] %></code></td>
93
+ <td>
94
+ <%= link_to errors_path(app_version: release[:version], unresolved: '0'), class: "text-decoration-none" do %>
95
+ <code><%= release[:version] %></code>
96
+ <% end %>
97
+ </td>
94
98
  <td><strong class="text-danger"><%= release[:error_count] %></strong></td>
95
99
  <td>
96
100
  <span class="badge bg-danger">
@@ -105,7 +109,7 @@
105
109
  <% end %>
106
110
  </td>
107
111
  <td>
108
- <%= link_to "View", errors_path(search: release[:version], unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
112
+ <%= link_to "View", errors_path(app_version: release[:version], unresolved: '0'), class: "btn btn-sm btn-outline-primary" %>
109
113
  </td>
110
114
  </tr>
111
115
  <% end %>
@@ -141,7 +145,11 @@
141
145
  <tbody>
142
146
  <% @errors_by_version.sort_by { |_, v| -v[:count] }.first(10).each do |version, data| %>
143
147
  <tr>
144
- <td><code><%= version %></code></td>
148
+ <td>
149
+ <%= link_to errors_path(app_version: version, unresolved: '0'), class: "text-decoration-none" do %>
150
+ <code><%= version %></code>
151
+ <% end %>
152
+ </td>
145
153
  <td><%= data[:count] %></td>
146
154
  <td><%= data[:error_types] %></td>
147
155
  <td>
@@ -185,7 +193,11 @@
185
193
  <tbody>
186
194
  <% @errors_by_git_sha.sort_by { |_, v| -v[:count] }.first(10).each do |sha, data| %>
187
195
  <tr>
188
- <td><code class="small"><%= sha[0..7] %></code></td>
196
+ <td>
197
+ <%= link_to errors_path(git_sha: sha, unresolved: '0'), class: "text-decoration-none" do %>
198
+ <code class="small"><%= sha[0..7] %></code>
199
+ <% end %>
200
+ </td>
189
201
  <td><%= data[:count] %></td>
190
202
  <td><%= data[:error_types] %></td>
191
203
  <td>
@@ -150,7 +150,7 @@
150
150
  <%= number_with_delimiter(table[:dead_tuples]) %>
151
151
  <% end %>
152
152
  </td>
153
- <td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
153
+ <td><%= table[:last_autovacuum] ? local_time_ago(parse_pg_timestamp(table[:last_autovacuum])) : "Never" %></td>
154
154
  </tr>
155
155
  <% end %>
156
156
  </tbody>
@@ -203,7 +203,7 @@
203
203
  <%= number_with_delimiter(table[:dead_tuples]) %>
204
204
  <% end %>
205
205
  </td>
206
- <td><%= table[:last_autovacuum] ? local_time_ago(Time.parse(table[:last_autovacuum])) : "Never" %></td>
206
+ <td><%= table[:last_autovacuum] ? local_time_ago(parse_pg_timestamp(table[:last_autovacuum])) : "Never" %></td>
207
207
  </tr>
208
208
  <% end %>
209
209
  </tbody>
@@ -338,7 +338,11 @@
338
338
  <% @entries.each do |entry| %>
339
339
  <tr>
340
340
  <td><%= link_to "##{entry[:error_id]}", error_path(entry[:error_id]), class: "text-decoration-none" %></td>
341
- <td><code><%= truncate(entry[:error_type].to_s, length: 40) %></code></td>
341
+ <td>
342
+ <%= link_to errors_path(error_type: entry[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
343
+ <code><%= truncate(entry[:error_type].to_s, length: 40) %></code>
344
+ <% end %>
345
+ </td>
342
346
  <td>
343
347
  <% util = entry[:utilization] %>
344
348
  <% util_badge = util >= 80 ? "danger" : (util >= 60 ? "warning" : "success") %>
@@ -139,7 +139,7 @@
139
139
  <small class="text-muted d-block">GC</small>
140
140
  <small>
141
141
  <% gc_health = health["gc"] || {} %>
142
- Live: <%= gc_health["heap_live_slots"]&.to_s(:delimited) rescue gc_health["heap_live_slots"] || "?" %> /
142
+ Live: <%= gc_health["heap_live_slots"] ? number_with_delimiter(gc_health["heap_live_slots"]) : "?" %> /
143
143
  Major GC: <%= gc_health["major_gc_count"] || "?" %>
144
144
  </small>
145
145
  </div>
@@ -172,7 +172,13 @@
172
172
  active_filters << { label: "Timeframe: #{params[:timeframe].humanize}", param: :timeframe } if params[:timeframe].present?
173
173
  active_filters << { label: "Frequency: #{params[:frequency].humanize}", param: :frequency } if params[:frequency].present?
174
174
  active_filters << { label: "Status: #{params[:status].humanize}", param: :status } if params[:status].present?
175
- active_filters << { label: "Priority: P#{params[:priority_level]}", param: :priority_level } if params[:priority_level].present?
175
+ if params[:priority_level].present?
176
+ # priority_level is the integer key (3=Critical/P0, 0=Low/P3); look up the short_label
177
+ # rather than interpolating the raw integer, which inverts the P-number.
178
+ priority_data = RailsErrorDashboard::ErrorLog::PRIORITY_LEVELS[params[:priority_level].to_i]
179
+ priority_short = priority_data ? priority_data[:short_label] : "P?"
180
+ active_filters << { label: "Priority: #{priority_short}", param: :priority_level }
181
+ end
176
182
  if params[:assigned_to] == '__unassigned__'
177
183
  active_filters << { label: "Unassigned", param: :assigned_to }
178
184
  elsif params[:assigned_to] == '__assigned__'
@@ -92,7 +92,7 @@
92
92
  <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); margin-bottom: var(--space-6);">
93
93
  <!-- Affected Users -->
94
94
  <div class="card stat-card" style="padding: var(--space-5) var(--space-6);">
95
- <div class="stat-label" style="margin-bottom: 4px;">Affected Users</div>
95
+ <div class="stat-label" style="margin-bottom: 4px;">Affected Users (Today)</div>
96
96
  <div style="display: flex; align-items: baseline; gap: 8px;">
97
97
  <span class="stat-value"><%= @stats[:affected_users_today] %></span>
98
98
  <% if @stats[:affected_users_change] != 0 %>
@@ -149,7 +149,7 @@
149
149
  <div style="font-size: 12px; color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><%= error[:message]&.truncate(80) %></div>
150
150
  </div>
151
151
  <div style="text-align: right; flex-shrink: 0;">
152
- <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums;"><%= error[:occurrence_count]&.to_s(:delimited) rescue error[:occurrence_count] %></div>
152
+ <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); font-variant-numeric: tabular-nums;"><%= number_with_delimiter(error[:occurrence_count]) %></div>
153
153
  <div style="font-size: 11px; color: var(--text-tertiary);"><%= error[:affected_users] %> users</div>
154
154
  </div>
155
155
  <% end %>
@@ -227,7 +227,9 @@
227
227
  <% @cross_platform_errors.first(10).each do |error| %>
228
228
  <tr>
229
229
  <td>
230
- <code class="small"><%= error[:error_type] %></code>
230
+ <%= link_to errors_path(error_type: error[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
231
+ <code class="small"><%= error[:error_type] %></code>
232
+ <% end %>
231
233
  </td>
232
234
  <td>
233
235
  <% error[:platforms].each do |platform| %>
@@ -185,7 +185,9 @@
185
185
  <% @releases.each do |release| %>
186
186
  <tr class="<%= 'table-active' if release[:current] %>">
187
187
  <td>
188
- <strong><%= release[:version] %></strong>
188
+ <%= link_to errors_path(app_version: release[:version], unresolved: '0'), class: "text-decoration-none" do %>
189
+ <strong><%= release[:version] %></strong>
190
+ <% end %>
189
191
  <% if release[:current] %>
190
192
  <span class="badge bg-primary ms-1">Current</span>
191
193
  <% end %>
@@ -213,7 +213,6 @@
213
213
  <div class="card" style="margin-bottom: var(--space-4);">
214
214
  <div style="padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border-primary);">
215
215
  <span style="font-size: 14px; font-weight: 600; color: var(--text-primary);"><i class="bi bi-puzzle" style="margin-right: 6px;"></i> Active Plugins</span>
216
- </h5>
217
216
  </div>
218
217
  <div class="card-body">
219
218
  <% if RailsErrorDashboard::PluginRegistry.any? %>
@@ -211,8 +211,10 @@
211
211
  <i class="bi bi-person"></i> View User's Errors
212
212
  <% end %>
213
213
  <% end %>
214
- <%= link_to errors_path(app_context.merge(platform: @error.platform, unresolved: '0')), class: "btn btn-sm" do %>
215
- <i class="bi bi-phone"></i> View <%= @error.platform %> Errors
214
+ <% if @error.platform.present? %>
215
+ <%= link_to errors_path(app_context.merge(platform: @error.platform, unresolved: '0')), class: "btn btn-sm" do %>
216
+ <i class="bi bi-phone"></i> View <%= @error.platform %> Errors
217
+ <% end %>
216
218
  <% end %>
217
219
  <%= link_to analytics_errors_path(app_context), class: "btn btn-sm" do %>
218
220
  <i class="bi bi-bar-chart-line"></i> Analytics
@@ -93,7 +93,11 @@
93
93
  <tbody>
94
94
  <% @entries.each do |entry| %>
95
95
  <tr>
96
- <td><code style="font-size: 0.85em;"><%= entry[:exception_class] %></code></td>
96
+ <td>
97
+ <%= link_to errors_path(error_type: entry[:exception_class], unresolved: '0'), class: "text-decoration-none", title: "View any unrescued #{entry[:exception_class]} errors that leaked into error_log" do %>
98
+ <code style="font-size: 0.85em;"><%= entry[:exception_class] %></code>
99
+ <% end %>
100
+ </td>
97
101
  <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:raise_location] %></td>
98
102
  <td style="color: var(--text-secondary); font-size: 12px; word-break: break-all;"><%= entry[:rescue_location] || "Unknown" %></td>
99
103
  <td><span class="badge bg-info text-dark"><%= number_with_delimiter(entry[:raise_count]) %></span></td>
@@ -107,7 +107,9 @@
107
107
  <tr>
108
108
  <td><strong><%= (@pagy.from || 1) + index %></strong></td>
109
109
  <td>
110
- <strong><%= entry[:error_type] %></strong>
110
+ <%= link_to errors_path(error_type: entry[:error_type], unresolved: '0'), class: "text-decoration-none" do %>
111
+ <strong><%= entry[:error_type] %></strong>
112
+ <% end %>
111
113
  <br><small class="text-muted"><%= entry[:message] %></small>
112
114
  </td>
113
115
  <td>
@@ -89,7 +89,10 @@ module RailsErrorDashboard
89
89
  end
90
90
 
91
91
  def errors_by_hour
92
- base_query.group_by_hour(:occurred_at).count
92
+ # group_by_hour_of_day buckets into 0..23 to show diurnal patterns
93
+ # (when in the day errors peak). The chart title says "Errors by Hour
94
+ # of Day" — group_by_hour produced a chronological time series instead.
95
+ base_query.group_by_hour_of_day(:occurred_at).count
93
96
  end
94
97
 
95
98
  def top_affected_users
@@ -37,11 +37,13 @@ module RailsErrorDashboard
37
37
  versions.each_with_object({}) do |(version, count), result|
38
38
  errors = base_query.where(app_version: version)
39
39
 
40
- # Count unique error types
41
- error_types = errors.distinct.pluck(:error_type).count
42
-
43
- # Count critical errors
44
- critical_count = errors.select { |error| error.severity == :critical }.count
40
+ # Pluck error_types once; both the unique-types count and the critical
41
+ # count classify off this string array. Avoids loading full ErrorLog
42
+ # records into memory just to compute counts (severity is a Ruby-side
43
+ # method on error_type, not a column).
44
+ types_for_version = errors.pluck(:error_type)
45
+ error_types = types_for_version.uniq.size
46
+ critical_count = types_for_version.count { |t| Services::SeverityClassifier.classify(t) == :critical }
45
47
 
46
48
  # Get platforms for this version
47
49
  platforms = errors.distinct.pluck(:platform).compact
@@ -180,15 +182,17 @@ module RailsErrorDashboard
180
182
  error_types = base_query.distinct.pluck(:error_type)
181
183
  return {} if error_types.count < 2
182
184
 
183
- hourly_distributions = {}
184
- error_types.each do |error_type|
185
- distribution = base_query
186
- .where(error_type: error_type)
187
- .group_by { |error| error.occurred_at.hour }
188
- .transform_values(&:count)
185
+ # Bucket counts by (error_type, hour-of-day) in a single SQL GROUP BY.
186
+ # Groupdate's group_by_hour_of_day generates database-portable SQL
187
+ # (PG/MySQL/SQLite) so we don't load full ErrorLog records into Ruby.
188
+ # Result shape: { [hour_int, error_type] => count }
189
+ grouped = base_query
190
+ .group_by_hour_of_day(:occurred_at)
191
+ .group(:error_type)
192
+ .count
189
193
 
190
- # Normalize to 0-23 hours
191
- hourly_distributions[error_type] = (0..23).map { |h| distribution[h] || 0 }
194
+ hourly_distributions = error_types.each_with_object({}) do |error_type, h|
195
+ h[error_type] = (0..23).map { |hour| grouped[[ hour, error_type ]] || 0 }
192
196
  end
193
197
 
194
198
  # Calculate correlation between error type pairs
@@ -30,6 +30,8 @@ module RailsErrorDashboard
30
30
  query = filter_by_platform(query)
31
31
  query = filter_by_application(query)
32
32
  query = filter_by_user_id(query)
33
+ query = filter_by_app_version(query)
34
+ query = filter_by_git_sha(query)
33
35
  query = filter_by_search(query)
34
36
  query = filter_by_severity(query)
35
37
  query = filter_by_timeframe(query)
@@ -44,6 +46,18 @@ module RailsErrorDashboard
44
46
  query
45
47
  end
46
48
 
49
+ def filter_by_app_version(query)
50
+ return query unless @filters[:app_version].present?
51
+ return query unless ErrorLog.column_names.include?("app_version")
52
+ query.where(app_version: @filters[:app_version])
53
+ end
54
+
55
+ def filter_by_git_sha(query)
56
+ return query unless @filters[:git_sha].present?
57
+ return query unless ErrorLog.column_names.include?("git_sha")
58
+ query.where(git_sha: @filters[:git_sha])
59
+ end
60
+
47
61
  def filter_by_error_type(query)
48
62
  return query unless @filters[:error_type].present?
49
63
 
@@ -28,27 +28,37 @@ module RailsErrorDashboard
28
28
  def detect_cascades
29
29
  return { detected: 0, updated: 0 } unless can_detect?
30
30
 
31
- # Get recent error occurrences
31
+ # Pluck (error_log_id, occurred_at) for every occurrence in the window
32
+ # ordered chronologically. Using pluck instead of loading full
33
+ # ActiveRecord rows keeps memory bounded to ~16 bytes/row instead of
34
+ # ~5KB/row, which matters because the host app schedules this job and
35
+ # the lookback window may contain a lot of occurrences.
32
36
  start_time = @lookback_hours.hours.ago
33
- occurrences = ErrorOccurrence.where("occurred_at >= ?", start_time).order(:occurred_at)
34
-
35
- # For each error occurrence, find potential children
37
+ rows = ErrorOccurrence
38
+ .where("occurred_at >= ?", start_time)
39
+ .order(:occurred_at)
40
+ .pluck(:error_log_id, :occurred_at)
41
+
42
+ # Two-pointer sweep: occurrences are time-sorted, so for each parent we
43
+ # only advance the child pointer forward through occurrences within the
44
+ # detection window. O(N + pairs) instead of O(N) inner SQL queries.
36
45
  patterns_found = Hash.new { |h, k| h[k] = { delays: [], count: 0 } }
37
46
 
38
- occurrences.each do |parent_occ|
39
- # Find occurrences within detection window
40
- potential_children = ErrorOccurrence
41
- .where("occurred_at > ? AND occurred_at <= ?",
42
- parent_occ.occurred_at,
43
- parent_occ.occurred_at + DETECTION_WINDOW)
44
- .where.not(error_log_id: parent_occ.error_log_id)
45
-
46
- potential_children.each do |child_occ|
47
- key = [ parent_occ.error_log_id, child_occ.error_log_id ]
48
- delay = (child_occ.occurred_at - parent_occ.occurred_at).to_f
49
-
50
- patterns_found[key][:delays] << delay
51
- patterns_found[key][:count] += 1
47
+ rows.each_with_index do |(parent_id, parent_time), i|
48
+ window_end = parent_time + DETECTION_WINDOW
49
+ j = i + 1
50
+ while j < rows.length
51
+ child_id, child_time = rows[j]
52
+ break if child_time > window_end
53
+
54
+ # Match the original SQL `occurred_at > parent` — strict, so two
55
+ # occurrences with identical timestamps don't form a cascade pair.
56
+ if child_id != parent_id && child_time > parent_time
57
+ key = [ parent_id, child_id ]
58
+ patterns_found[key][:delays] << (child_time - parent_time).to_f
59
+ patterns_found[key][:count] += 1
60
+ end
61
+ j += 1
52
62
  end
53
63
  end
54
64
 
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.6.3"
2
+ VERSION = "0.6.4"
3
3
  end
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.6.3
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -498,7 +498,7 @@ metadata:
498
498
  funding_uri: https://github.com/sponsors/AnjanJ
499
499
  post_install_message: |
500
500
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
501
- RED (Rails Error Dashboard) v0.6.3
501
+ RED (Rails Error Dashboard) v0.6.4
502
502
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
503
503
 
504
504
  First install:
@@ -534,7 +534,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
534
534
  - !ruby/object:Gem::Version
535
535
  version: '0'
536
536
  requirements: []
537
- rubygems_version: 4.0.3
537
+ rubygems_version: 3.6.9
538
538
  specification_version: 4
539
539
  summary: Self-hosted error tracking and exception monitoring for Rails. Free, forever.
540
540
  test_files: []