rails_error_dashboard 0.16.0 → 0.16.2

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: 84ad49626b8dddc0665ed515f3fc8e3357adadddcb5991679324cffa40d2aa2e
4
- data.tar.gz: af06a63fb80ded2960e23f6e6c25ace1a741b85e13f014ae4bfbfd4b4b16980a
3
+ metadata.gz: 9cb19fbd3a24300d9d8c31627b53ccb7e8b8b81a2a6760d167ecf056e9955ba3
4
+ data.tar.gz: dceec815d4b0fce705543ceb0b48ba6d96f839843b5a19b4b82a22bccd40dd3e
5
5
  SHA512:
6
- metadata.gz: c3154ebfa3b1d46d497a86cfca33dca5a09ed98127a699e7a9eb17ce3d05380f807172b377a2a824fbffff806e6f1e9a6c6ba96e8cb61802368e81ffc56ea93a
7
- data.tar.gz: 571ca72a5e588371357a427c02a2ebed3bf565cf70f03b8c8e5877245e060bd7d8ddff873ae50235ac1c4103c6944d4f5e852c221766f7292225b4af8406ceca
6
+ metadata.gz: f3fcb870b3f4fd00fd7a1bf35d1bc8fff35593f772c78e006beb643c2d0a4ab2ba52b24e8218b612b60aa79bb5018e9bb24524d23874e68d8adbb3a8de58ebd6
7
+ data.tar.gz: 646a9ab4b5f291fe298bc4566f3ef0ff5cac2fb023f5f8facfb46128552453eba9e68316ccc3b4447f5b02f531209a48cec678d5dbaf645611a804901489546e
data/README.md CHANGED
@@ -127,7 +127,7 @@ No RED licence or event-ingestion fee, and no plan limits — your database is t
127
127
  - **Host-app safety** — nothing in the capture path raises into your app; every subscriber and callback is rescue-wrapped, `Thread.current` is cleaned up in `ensure`, and the original exception is always re-raised. Variables, health and breadcrumbs are opt-in and off by default; storm protection is on by default and fails open.
128
128
  - **Performance** — the storm-protection hot path is a digest plus an atomic increment with no I/O; the figures quoted below are a maintainer's single-machine measurements and no benchmark script ships with the gem yet.
129
129
  - **Security** — HTTP Basic Auth or your own `authenticate_with` lambda (Devise, Warden, session); your Rails `filter_parameters` are applied to params, variables and breadcrumbs; prompts are never recorded by LLM observability. Vulnerability reports: [SECURITY.md](SECURITY.md).
130
- - **Compatibility** — Rails 7.0–8.1, Ruby 3.2–4.0, PostgreSQL, MySQL/Trilogy or SQLite; `turbo-rails` plus ActionCable are needed for live updates (no polling fallback); the gem's own CSS/JS is inline but Bootstrap JS, Chart.js, highlight.js and Google Fonts load from CDNs, so it is not air-gap clean.
130
+ - **Compatibility** — Rails 7.0–8.1 (on Rails 7.0.0 to 7.0.8.7, unhandled request errors are not recorded; Rails 7.0.10 fixes it), Ruby 3.2–4.0, PostgreSQL, MySQL/Trilogy or SQLite; `turbo-rails` plus ActionCable are needed for live updates (no polling fallback); the gem's own CSS/JS is inline but Bootstrap JS, Chart.js, highlight.js and Google Fonts load from CDNs, so it is not air-gap clean.
131
131
 
132
132
  ---
133
133
 
@@ -632,6 +632,7 @@ gem 'rails_error_dashboard'
632
632
 
633
633
  RED requires Rails 7.0 or newer, Ruby 3.2 or newer, `pagy ~> 43` and `groupdate ~> 6`. An app that pins an older Pagy or groupdate must upgrade it first, or `bundle install` fails.
634
634
 
635
+ - **Rails 7.0.0 to 7.0.8.7**: unhandled exceptions from web requests are not recorded, because these versions render the error page without reporting the exception. Upgrade to Rails 7.0.10 or later, which reports them; until then RED logs a warning at boot. See [Troubleshooting](docs/TROUBLESHOOTING.md#request-errors-not-recorded-on-rails-70).
635
636
  - **Rails before 8.1.4 or 7.2.4** (every 8.0, 7.1 and 7.0 release): also add `gem "json", "< 3"`. json 3 breaks those Rails versions, and the dashboard returns 500. See [the json pin](docs/UPGRADING.md#rails-before-814-or-724-pin-json-below-3).
636
637
  - **MySQL**: load the time-zone tables (`mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql`), or the charts fail.
637
638
 
@@ -15,7 +15,7 @@ module RailsErrorDashboard
15
15
  context = context.symbolize_keys if context.respond_to?(:symbolize_keys)
16
16
 
17
17
  # Reconstruct the exception from serialized data
18
- exception = reconstruct_exception(exception_data)
18
+ exception = reconstruct_exception(exception_data, context)
19
19
 
20
20
  # Pass pre-extracted cause chain via context so LogError can use it
21
21
  # (reconstructed exceptions don't have Ruby's built-in cause set)
@@ -74,14 +74,22 @@ module RailsErrorDashboard
74
74
  # set the message with Exception's own initializer, bypassing whatever
75
75
  # the subclass expects. Fall back to the constructor for classes whose
76
76
  # `message` needs state their initializer sets.
77
- def reconstruct_exception(data)
78
- exception_class = begin
79
- data[:class_name].constantize
80
- rescue NameError
81
- # If class doesn't exist, use StandardError
82
- StandardError
77
+ #
78
+ # A client report (ManualErrorReporter) is rebuilt as the same
79
+ # SyntheticException the sync path logs. Its type is the caller's string,
80
+ # usually taken from a frontend or mobile client, so it is never looked up
81
+ # as a constant.
82
+ def reconstruct_exception(data, context)
83
+ if context[:_capture_source] == "client"
84
+ return ManualErrorReporter::SyntheticException.new(
85
+ error_type: data[:class_name].to_s,
86
+ message: data[:message],
87
+ backtrace: Array(data[:backtrace])
88
+ )
83
89
  end
84
90
 
91
+ exception_class = resolve_exception_class(data[:class_name])
92
+
85
93
  exception = allocate_exception(exception_class, data[:message]) ||
86
94
  construct_exception(exception_class, data[:message]) ||
87
95
  StandardError.new(data[:message])
@@ -92,6 +100,18 @@ module RailsErrorDashboard
92
100
  exception
93
101
  end
94
102
 
103
+ # Only an Exception subclass is ever built from a queued name. Anything
104
+ # else (a missing constant, a module, a non-exception class, a plain
105
+ # value) becomes a StandardError; the stored error_type still comes from
106
+ # _reported_error_type. Payloads enqueued before 0.16.0 carry no capture
107
+ # source, so a client report from one of them arrives here.
108
+ def resolve_exception_class(name)
109
+ klass = name.constantize
110
+ klass.is_a?(Class) && klass < Exception ? klass : StandardError
111
+ rescue NameError
112
+ StandardError
113
+ end
114
+
95
115
  def allocate_exception(exception_class, message)
96
116
  return nil unless exception_class < Exception
97
117
 
@@ -109,7 +109,7 @@
109
109
  <td><%= release[:error_types] %></td>
110
110
  <td>
111
111
  <% release[:platforms].each do |platform| %>
112
- <span class="badge bg-secondary text-capitalize me-1"><%= platform %></span>
112
+ <span class="badge bg-secondary me-1"><%= platform %></span>
113
113
  <% end %>
114
114
  </td>
115
115
  <td>
@@ -326,7 +326,7 @@
326
326
  <div class="row">
327
327
  <% @platform_specific_errors.each do |platform, errors| %>
328
328
  <div class="col-md-6 mb-3">
329
- <h6 class="text-capitalize"><%= platform %></h6>
329
+ <h6><%= platform %></h6>
330
330
  <div class="list-group">
331
331
  <% errors.each do |error_data| %>
332
332
  <div class="list-group-item">
@@ -211,6 +211,10 @@
211
211
  <div class="progress" style="height: 4px; margin-bottom: var(--space-3);">
212
212
  <div class="progress-bar" style="width: <%= health[:stability_score] %>%; background: var(--status-<%= health[:health_status] == :healthy ? 'success' : health[:health_status] == :warning ? 'warning' : 'critical' %>);"></div>
213
213
  </div>
214
+ <%# Scored on volume alone: nothing from the window is resolved %>
215
+ <% unless health[:resolution_measured] %>
216
+ <div style="font-size: 12px; color: var(--text-tertiary); margin-bottom: var(--space-2);"><%= red_t("red.common.health_status.nothing_resolved") %></div>
217
+ <% end %>
214
218
  <% end %>
215
219
  <div style="font-size: 12px; color: var(--text-tertiary);">
216
220
  <%= red_t("red.errors.overview_page.platform_summary", total: health[:total_errors], critical: health[:critical_errors], unresolved: health[:unresolved_errors]) %>
@@ -38,7 +38,7 @@
38
38
  <div class="col-md-6 col-lg-3 mb-3">
39
39
  <div class="card h-100 <%= { healthy: 'border-success', warning: 'border-warning', not_rated: 'border-secondary' }.fetch(health[:health_status], 'border-danger') %>">
40
40
  <div class="card-body">
41
- <h5 class="card-title text-capitalize">
41
+ <h5 class="card-title">
42
42
  <%= platform || red_t("red.common.unknown") %>
43
43
  <% if health[:health_status] == :healthy %>
44
44
  <span class="badge bg-success float-end"><%= red_t("red.analytics.platform_comparison.health_card.healthy") %></span>
@@ -64,6 +64,10 @@
64
64
  style="width: <%= health[:stability_score] %>%">
65
65
  </div>
66
66
  </div>
67
+ <%# Scored on volume alone: nothing from the window is resolved %>
68
+ <% unless health[:resolution_measured] %>
69
+ <small class="text-muted d-block mt-1"><%= red_t("red.common.health_status.nothing_resolved") %></small>
70
+ <% end %>
67
71
  </div>
68
72
  <% end %>
69
73
 
@@ -139,7 +143,7 @@
139
143
  <div class="card-body">
140
144
  <% @severity_distribution.each do |platform, severities| %>
141
145
  <div class="mb-3">
142
- <h6 class="text-capitalize"><%= platform || red_t("red.common.unknown") %></h6>
146
+ <h6><%= platform || red_t("red.common.unknown") %></h6>
143
147
  <% total = severities.values.sum %>
144
148
  <% if total > 0 %>
145
149
  <div class="progress" style="height: 25px;">
@@ -239,7 +243,7 @@
239
243
  </td>
240
244
  <td>
241
245
  <% error[:platforms].each do |platform| %>
242
- <span class="badge bg-secondary text-capitalize me-1"><%= platform %></span>
246
+ <span class="badge bg-secondary me-1"><%= platform %></span>
243
247
  <% end %>
244
248
  </td>
245
249
  <td>
@@ -318,7 +322,7 @@
318
322
  new Chart(errorRateCtx, {
319
323
  type: 'bar',
320
324
  data: {
321
- labels: <%= js_safe_json(@error_rate_by_platform.keys.map { |k| k.to_s.capitalize }) %>,
325
+ labels: <%= js_safe_json(@error_rate_by_platform.keys.map(&:to_s)) %>,
322
326
  datasets: [{
323
327
  label: '<%= red_js_t("red.ui_js.charts.series_total_errors") %>',
324
328
  data: <%= js_safe_json(@error_rate_by_platform.values) %>,
@@ -345,7 +349,7 @@
345
349
  if (dailyTrendCtx) {
346
350
  const datasets = <%= js_safe_json(@daily_trends.map { |platform, data|
347
351
  {
348
- label: platform.to_s.capitalize,
352
+ label: platform.to_s,
349
353
  data: data.values,
350
354
  platform: platform.to_s.downcase,
351
355
  tension: 0.4
@@ -382,7 +386,7 @@
382
386
  // Resolution Time Chart
383
387
  const resolutionTimeCtx = document.getElementById('resolutionTimeChart');
384
388
  if (resolutionTimeCtx) {
385
- const platforms = <%= js_safe_json(@resolution_times.keys.map { |k| k.to_s.capitalize }) %>;
389
+ const platforms = <%= js_safe_json(@resolution_times.keys.map(&:to_s)) %>;
386
390
  const times = <%= js_safe_json(@resolution_times.values.map { |v| v || 0 }) %>;
387
391
 
388
392
  new Chart(resolutionTimeCtx, {
@@ -38,6 +38,7 @@ de:
38
38
  critical: "\U0001F534 Kritisch"
39
39
  not_rated: "Nicht bewertet"
40
40
  not_rated_hint: "Eine Bewertung vergleicht Plattformen und braucht daher Fehler auf mindestens zwei."
41
+ nothing_resolved: "Noch keine Fehler aus diesem Zeitraum behoben"
41
42
  device_type:
42
43
  mobile: Mobil
43
44
  tablet: Tablet
@@ -349,7 +350,7 @@ de:
349
350
  types:
350
351
  business_hours:
351
352
  label: Muster der Geschäftszeiten
352
- description: Fehler häufen sich während der Geschäftszeiten (9–17 Uhr),
353
+ description: Fehler häufen sich während der Geschäftszeiten (9:00–17:59),
353
354
  was auf einen Zusammenhang mit der Nutzeraktivität hindeutet
354
355
  night:
355
356
  label: Nachtmuster
@@ -84,6 +84,7 @@ en:
84
84
  critical: "🔴 Critical"
85
85
  not_rated: "Not rated"
86
86
  not_rated_hint: "A rating compares platforms, so it needs errors on at least two."
87
+ nothing_resolved: "No errors from this period resolved yet"
87
88
 
88
89
  # Device classes reported by the user-agent parser. Browser and OS names
89
90
  # (Chrome, Safari, Windows, iOS) are brands and stay as written; these
@@ -520,7 +521,7 @@ en:
520
521
  types:
521
522
  business_hours:
522
523
  label: "Business Hours Pattern"
523
- description: "Errors peak during business hours (9am-5pm), suggesting user activity correlation"
524
+ description: "Errors peak during business hours (9:00–17:59), suggesting user activity correlation"
524
525
  night:
525
526
  label: "Night Pattern"
526
527
  description: "Errors peak during night hours (midnight-6am), possibly from background jobs or automated tasks"
@@ -38,6 +38,7 @@ es:
38
38
  critical: "\U0001F534 Crítico"
39
39
  not_rated: "Sin calificar"
40
40
  not_rated_hint: "La calificación compara plataformas, así que necesita errores en al menos dos."
41
+ nothing_resolved: "Aún no se ha resuelto ningún error de este período"
41
42
  device_type:
42
43
  mobile: Móvil
43
44
  tablet: Tableta
@@ -363,7 +364,7 @@ es:
363
364
  types:
364
365
  business_hours:
365
366
  label: Patrón de horario laboral
366
- description: Los errores se concentran en el horario laboral (9:00-17:00),
367
+ description: Los errores se concentran en el horario laboral (9:00–17:59),
367
368
  lo que sugiere relación con la actividad de los usuarios
368
369
  night:
369
370
  label: Patrón nocturno
@@ -38,6 +38,7 @@ fr:
38
38
  critical: "\U0001F534 Critique"
39
39
  not_rated: "Non évalué"
40
40
  not_rated_hint: "L'évaluation compare les plateformes : il faut des erreurs sur au moins deux."
41
+ nothing_resolved: "Aucune erreur de cette période n'est encore résolue"
41
42
  device_type:
42
43
  mobile: Mobile
43
44
  tablet: Tablette
@@ -364,7 +365,7 @@ fr:
364
365
  types:
365
366
  business_hours:
366
367
  label: Motif des heures ouvrées
367
- description: Les erreurs culminent pendant les heures ouvrées (9h-17h),
368
+ description: Les erreurs culminent pendant les heures ouvrées (9:00–17:59),
368
369
  ce qui suggère un lien avec l'activité des utilisateurs
369
370
  night:
370
371
  label: Motif nocturne
@@ -38,6 +38,7 @@ it:
38
38
  critical: "\U0001F534 Critico"
39
39
  not_rated: "Non valutato"
40
40
  not_rated_hint: "La valutazione confronta le piattaforme, quindi servono errori su almeno due."
41
+ nothing_resolved: "Nessun errore di questo periodo è stato ancora risolto"
41
42
  device_type:
42
43
  mobile: Mobile
43
44
  tablet: Tablet
@@ -358,7 +359,7 @@ it:
358
359
  types:
359
360
  business_hours:
360
361
  label: Schema da orario d'ufficio
361
- description: Il picco di errori cade nell'orario d'ufficio (9–17), il
362
+ description: Il picco di errori cade nell'orario d'ufficio (9:00–17:59), il
362
363
  che suggerisce una correlazione con l'attività degli utenti
363
364
  night:
364
365
  label: Schema notturno
@@ -38,6 +38,7 @@ ja:
38
38
  critical: "\U0001F534 重大"
39
39
  not_rated: "評価なし"
40
40
  not_rated_hint: "評価はプラットフォーム同士の比較なので、2つ以上のプラットフォームのエラーが必要です。"
41
+ nothing_resolved: "この期間のエラーはまだ1件も解決されていません"
41
42
  device_type:
42
43
  mobile: モバイル
43
44
  tablet: タブレット
@@ -325,7 +326,7 @@ ja:
325
326
  types:
326
327
  business_hours:
327
328
  label: 業務時間帯パターン
328
- description: エラーは業務時間帯(9時〜17時)に集中しており、ユーザーの利用状況との関連が考えられます
329
+ description: エラーは業務時間帯(9:00–17:59)に集中しており、ユーザーの利用状況との関連が考えられます
329
330
  night:
330
331
  label: 夜間パターン
331
332
  description: エラーは夜間(0時〜6時)に集中しており、バックグラウンドジョブや自動処理が原因の可能性があります
@@ -38,6 +38,7 @@ pl:
38
38
  critical: "\U0001F534 Krytyczny"
39
39
  not_rated: "Bez oceny"
40
40
  not_rated_hint: "Ocena porównuje platformy, więc potrzebne są błędy na co najmniej dwóch."
41
+ nothing_resolved: "Żaden błąd z tego okresu nie został jeszcze rozwiązany"
41
42
  device_type:
42
43
  mobile: Mobile
43
44
  tablet: Tablet
@@ -366,7 +367,7 @@ pl:
366
367
  types:
367
368
  business_hours:
368
369
  label: Wzorzec godzin pracy
369
- description: Szczyt błędów przypada na godziny pracy (9:00–17:00), co
370
+ description: Szczyt błędów przypada na godziny pracy (9:00–17:59), co
370
371
  sugeruje związek z aktywnością użytkowników
371
372
  night:
372
373
  label: Wzorzec nocny
@@ -38,6 +38,7 @@ pt-BR:
38
38
  critical: "\U0001F534 Crítico"
39
39
  not_rated: "Sem avaliação"
40
40
  not_rated_hint: "A avaliação compara plataformas, então precisa de erros em pelo menos duas."
41
+ nothing_resolved: "Nenhum erro deste período foi resolvido ainda"
41
42
  device_type:
42
43
  mobile: Celular
43
44
  tablet: Tablet
@@ -363,7 +364,7 @@ pt-BR:
363
364
  types:
364
365
  business_hours:
365
366
  label: Padrão de horário comercial
366
- description: Os erros se concentram no horário comercial (9h-17h), o que
367
+ description: Os erros se concentram no horário comercial (9:00–17:59), o que
367
368
  sugere relação com a atividade dos usuários
368
369
  night:
369
370
  label: Padrão noturno
@@ -38,6 +38,7 @@ ru:
38
38
  critical: "\U0001F534 Критично"
39
39
  not_rated: "Без оценки"
40
40
  not_rated_hint: "Оценка сравнивает платформы, поэтому нужны ошибки как минимум на двух."
41
+ nothing_resolved: "Ни одна ошибка за этот период ещё не решена"
41
42
  device_type:
42
43
  mobile: Мобильное
43
44
  tablet: Планшет
@@ -368,7 +369,7 @@ ru:
368
369
  types:
369
370
  business_hours:
370
371
  label: Рабочие часы
371
- description: Пик ошибок приходится на рабочие часы (9:00–17:00), что указывает
372
+ description: Пик ошибок приходится на рабочие часы (9:00–17:59), что указывает
372
373
  на связь с активностью пользователей
373
374
  night:
374
375
  label: Ночная закономерность
@@ -38,6 +38,7 @@ uk:
38
38
  critical: "\U0001F534 Критичний"
39
39
  not_rated: "Без оцінки"
40
40
  not_rated_hint: "Оцінка порівнює платформи, тому потрібні помилки щонайменше на двох."
41
+ nothing_resolved: "Жодну помилку за цей період ще не вирішено"
41
42
  device_type:
42
43
  mobile: Mobile
43
44
  tablet: Tablet
@@ -366,7 +367,7 @@ uk:
366
367
  types:
367
368
  business_hours:
368
369
  label: Закономірність робочих годин
369
- description: Пік помилок припадає на робочі години (9:00–17:00), що вказує
370
+ description: Пік помилок припадає на робочі години (9:00–17:59), що вказує
370
371
  на звʼязок з активністю користувачів
371
372
  night:
372
373
  label: Нічна закономірність
@@ -38,6 +38,7 @@ zh-CN:
38
38
  critical: "\U0001F534 严重"
39
39
  not_rated: "未评级"
40
40
  not_rated_hint: "评级是在平台之间比较的,因此至少需要两个平台上的错误。"
41
+ nothing_resolved: "此期间的错误均尚未解决"
41
42
  device_type:
42
43
  mobile: Mobile
43
44
  tablet: Tablet
@@ -323,7 +324,7 @@ zh-CN:
323
324
  types:
324
325
  business_hours:
325
326
  label: 工作时段规律
326
- description: 错误高峰出现在工作时段(9:00–17:00),说明与用户活动相关
327
+ description: 错误高峰出现在工作时段(9:00–17:59),说明与用户活动相关
327
328
  night:
328
329
  label: 夜间规律
329
330
  description: 错误高峰出现在夜间(0:00–6:00),可能来自后台任务或定时作业
@@ -547,10 +547,6 @@ module RailsErrorDashboard
547
547
  say "\n"
548
548
  end
549
549
 
550
- def show_readme
551
- # Skip the old README display since we have the new summary
552
- end
553
-
554
550
  private
555
551
 
556
552
  def quick_mode?
@@ -735,6 +735,13 @@ module RailsErrorDashboard
735
735
  "Feature has been auto-disabled. Upgrade Ruby to use this feature."
736
736
  @detect_swallowed_exceptions = false
737
737
  end
738
+
739
+ # Rails 7.0.0-7.0.8.7 render request exceptions without reporting them, so
740
+ # RED never sees them. Rails 7.0.10 backported 7.1's reporting.
741
+ if defined?(ActionPack) && ActionPack.gem_version < Gem::Version.new("7.0.10")
742
+ warnings << "Rails #{ActionPack.gem_version} does not report the request exceptions it renders as error pages, " \
743
+ "so unhandled request errors are not recorded. Upgrading to Rails 7.0.10 or later fixes this."
744
+ end
738
745
  # Validate sub-settings only if feature is still active after version check
739
746
  if detect_swallowed_exceptions
740
747
  if swallowed_exception_max_cache_size && swallowed_exception_max_cache_size < 1
@@ -65,6 +65,13 @@ module RailsErrorDashboard
65
65
  @volume ||= Queries::EventVolume.new(base_scope, @start_date)
66
66
  end
67
67
 
68
+ # Events per platform for the platforms with errors in the window, the
69
+ # only ones scored or given a health card. Errors with no platform value
70
+ # belong to none.
71
+ def active_error_rates
72
+ @active_error_rates ||= error_rate_by_platform.except(nil)
73
+ end
74
+
68
75
  # platform => { error_type => events in the window }.
69
76
  #
70
77
  # Severity, critical counts and cross-platform totals all fold from this
@@ -158,32 +165,50 @@ module RailsErrorDashboard
158
165
  # Higher score = more stable (fewer errors, faster resolution)
159
166
  # @return [Hash] Platform => stability score
160
167
  def platform_stability_scores
161
- platforms = base_scope.distinct.pluck(:platform).compact
162
- error_rates = error_rate_by_platform
168
+ # Only platforms with errors in the window are scored. Every platform
169
+ # ever seen used to be, and one with none in the window scored 100.
170
+ error_rates = active_error_rates
163
171
  # A score compares platforms. With fewer than two that have errors in
164
172
  # the window there is nothing to compare against, so none is rated (a
165
- # lone platform used to come out Critical every time). The count is of
166
- # platforms with errors in the window: the list above is every
167
- # platform ever seen.
173
+ # lone platform used to come out Critical every time).
168
174
  return {} if error_rates.size < 2
169
175
  resolution_times = resolution_time_by_platform
170
176
 
171
177
  total_errors = error_rates.values.sum
172
- max_resolution_time = resolution_times.values.compact.max || 1
178
+ even_share = 1.0 / error_rates.size
179
+ fastest_resolution = resolution_times.values_at(*error_rates.keys).compact.min
180
+
181
+ error_rates.each_with_object({}) do |(platform, error_count), result|
182
+ avg_resolution = resolution_times[platform]
183
+
184
+ # This platform's share of all errors against an even split: full
185
+ # credit at or below it, falling linearly to 0 when one platform has
186
+ # every error. Against the raw share instead, no evenly split app
187
+ # could be Healthy. The even-split test is done in integers so an
188
+ # exact split is never lost to rounding.
189
+ error_score = if error_count * error_rates.size <= total_errors
190
+ 1.0
191
+ else
192
+ 1.0 - (((error_count.to_f / total_errors) - even_share) / (1.0 - even_share))
193
+ end
173
194
 
174
- platforms.each_with_object({}) do |platform, result|
175
- error_count = error_rates[platform] || 0
176
- avg_resolution = resolution_times[platform] || 0
177
-
178
- # This platform's share of all errors, inverted. Against the busiest
179
- # platform instead, the busiest always scored 0 here and so was always
180
- # Critical.
181
- error_score = total_errors.positive? ? 1.0 - (error_count.to_f / total_errors) : 1.0
182
- resolution_score = avg_resolution.positive? ? 1.0 - (avg_resolution / max_resolution_time) : 1.0
183
-
184
- # Weight: 70% error count, 30% resolution time
185
- # Convert to 0-100 scale
186
- result[platform] = ((error_score * 0.7 + resolution_score * 0.3) * 100).round(1)
195
+ # The fastest platform's average resolution time over this one's,
196
+ # each floored at one hour: the fastest and any as fast get full
197
+ # credit, one twice as slow gets half. Against the slowest instead,
198
+ # the slowest always scored 0 here. The floor keeps an average of 0.0
199
+ # (resolved at once) from leaving every other platform with none.
200
+ result[platform] = if avg_resolution
201
+ resolution_score = [ fastest_resolution, 1.0 ].max / [ avg_resolution, 1.0 ].max
202
+
203
+ # Weight: 70% error count, 30% resolution time
204
+ # Convert to 0-100 scale
205
+ ((error_score * 0.7 + resolution_score * 0.3) * 100).round(1)
206
+ else
207
+ # Nothing resolved in the window: no resolution time to compare, so
208
+ # the score is the volume credit alone, on the same scale. It used
209
+ # to get the full resolution credit. The cards say which it is.
210
+ (error_score * 100).round(1)
211
+ end
187
212
  end
188
213
  end
189
214
 
@@ -227,12 +252,13 @@ module RailsErrorDashboard
227
252
  # Get platform health summary
228
253
  # @return [Hash] Platform => health metrics
229
254
  def platform_health_summary
230
- error_rates = error_rate_by_platform
255
+ error_rates = active_error_rates
231
256
  stability_scores = platform_stability_scores
232
257
  severities = severity_distribution_by_platform
233
258
  midpoint = @start_date + (@days / 2.0).days
234
259
 
235
- platforms.each_with_object({}) do |platform, result|
260
+ # A card for each platform with errors in the window, as the scores
261
+ error_rates.each_key.with_object({}) do |platform, result|
236
262
  platform_scope = base_scope.where(platform: platform)
237
263
  groups_in_window = platform_scope.where("occurred_at >= ?", @start_date)
238
264
 
@@ -259,6 +285,9 @@ module RailsErrorDashboard
259
285
  critical_errors: critical_errors,
260
286
  unresolved_errors: unresolved_errors,
261
287
  resolution_rate: resolution_rate,
288
+ # false when nothing from the window is resolved, so the stability
289
+ # score is volume alone
290
+ resolution_measured: resolved_errors.positive?,
262
291
  # nil and :not_rated when there are fewer than two platforms
263
292
  stability_score: stability_scores[platform],
264
293
  error_velocity: velocity, # Positive = increasing, negative = decreasing
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.16.0"
2
+ VERSION = "0.16.2"
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.16.0
4
+ version: 0.16.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -149,20 +149,6 @@ dependencies:
149
149
  - - "~>"
150
150
  - !ruby/object:Gem::Version
151
151
  version: '3.0'
152
- - !ruby/object:Gem::Dependency
153
- name: vcr
154
- requirement: !ruby/object:Gem::Requirement
155
- requirements:
156
- - - "~>"
157
- - !ruby/object:Gem::Version
158
- version: '6.0'
159
- type: :development
160
- prerelease: false
161
- version_requirements: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - "~>"
164
- - !ruby/object:Gem::Version
165
- version: '6.0'
166
152
  - !ruby/object:Gem::Dependency
167
153
  name: simplecov
168
154
  requirement: !ruby/object:Gem::Requirement
@@ -177,20 +163,6 @@ dependencies:
177
163
  - - "~>"
178
164
  - !ruby/object:Gem::Version
179
165
  version: '1.1'
180
- - !ruby/object:Gem::Dependency
181
- name: appraisal
182
- requirement: !ruby/object:Gem::Requirement
183
- requirements:
184
- - - "~>"
185
- - !ruby/object:Gem::Version
186
- version: '2.5'
187
- type: :development
188
- prerelease: false
189
- version_requirements: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - "~>"
192
- - !ruby/object:Gem::Version
193
- version: '2.5'
194
166
  - !ruby/object:Gem::Dependency
195
167
  name: capybara
196
168
  requirement: !ruby/object:Gem::Requirement
@@ -384,7 +356,6 @@ files:
384
356
  - config/locales/uk.yml
385
357
  - config/locales/zh-CN.yml
386
358
  - config/routes.rb
387
- - db/development.sqlite3
388
359
  - db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb
389
360
  - db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb
390
361
  - db/migrate/20251224081522_add_better_tracking_to_error_logs.rb
@@ -438,7 +409,6 @@ files:
438
409
  - db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb
439
410
  - db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb
440
411
  - lib/generators/rails_error_dashboard/install/install_generator.rb
441
- - lib/generators/rails_error_dashboard/install/templates/README
442
412
  - lib/generators/rails_error_dashboard/install/templates/initializer.rb
443
413
  - lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb
444
414
  - lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb
@@ -628,7 +598,7 @@ metadata:
628
598
  funding_uri: https://github.com/sponsors/AnjanJ
629
599
  post_install_message: |
630
600
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
631
- RED (Rails Error Dashboard) v0.16.0
601
+ RED (Rails Error Dashboard) v0.16.2
632
602
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
633
603
 
634
604
  First install:
Binary file
@@ -1,24 +0,0 @@
1
- ===============================================================================
2
-
3
- RED (Rails Error Dashboard) has been installed!
4
-
5
- Next steps:
6
-
7
- 1. Run migrations:
8
- rails db:migrate
9
-
10
- 2. Start your Rails server:
11
- rails server
12
-
13
- 3. Visit the dashboard:
14
- http://localhost:3000/red
15
-
16
- 4. Change the default credentials in:
17
- config/initializers/rails_error_dashboard.rb
18
-
19
- For separate database setup, see:
20
- https://github.com/AnjanJ/rails_error_dashboard/blob/main/docs/guides/CONFIGURATION.md
21
-
22
- Full docs: https://github.com/AnjanJ/rails_error_dashboard
23
-
24
- ===============================================================================