rails_error_dashboard 0.12.0 → 0.13.0

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/rails_error_dashboard/application_controller.rb +92 -11
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +111 -34
  4. data/app/controllers/rails_error_dashboard/webhooks_controller.rb +3 -0
  5. data/app/helpers/rails_error_dashboard/application_helper.rb +46 -1
  6. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +8 -0
  7. data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +71 -0
  8. data/app/jobs/rails_error_dashboard/notification_burst_summary_job.rb +55 -0
  9. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +68 -1
  10. data/app/jobs/rails_error_dashboard/storm_notification_job.rb +8 -44
  11. data/app/models/rails_error_dashboard/error_baseline.rb +12 -9
  12. data/app/models/rails_error_dashboard/error_comment.rb +0 -5
  13. data/app/models/rails_error_dashboard/error_log.rb +9 -3
  14. data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
  15. data/app/models/rails_error_dashboard/error_occurrence.rb +9 -1
  16. data/app/views/layouts/rails_error_dashboard.html.erb +11 -3
  17. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +18 -11
  18. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +22 -8
  19. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +15 -6
  20. data/app/views/rails_error_dashboard/errors/analytics.html.erb +14 -8
  21. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
  22. data/app/views/rails_error_dashboard/errors/index.html.erb +6 -1
  23. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +7 -7
  24. data/app/views/rails_error_dashboard/errors/releases.html.erb +2 -2
  25. data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
  26. data/config/locales/de.yml +28 -0
  27. data/config/locales/en.yml +35 -0
  28. data/config/locales/es.yml +28 -0
  29. data/config/locales/fr.yml +28 -0
  30. data/config/locales/it.yml +28 -0
  31. data/config/locales/ja.yml +28 -0
  32. data/config/locales/pl.yml +28 -0
  33. data/config/locales/pt-BR.yml +28 -0
  34. data/config/locales/ru.yml +28 -0
  35. data/config/locales/uk.yml +28 -0
  36. data/config/locales/zh-CN.yml +28 -0
  37. data/db/migrate/20260917000001_add_last_notified_at_to_error_logs.rb +40 -0
  38. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -1
  39. data/lib/rails_error_dashboard/commands/assign_error.rb +12 -2
  40. data/lib/rails_error_dashboard/commands/backfill_environments.rb +2 -0
  41. data/lib/rails_error_dashboard/commands/backfill_resolved_at.rb +42 -0
  42. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -0
  43. data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +2 -0
  44. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -0
  45. data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +2 -0
  46. data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +39 -7
  47. data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +3 -1
  48. data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +27 -4
  49. data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +5 -2
  50. data/lib/rails_error_dashboard/commands/link_existing_issue.rb +1 -0
  51. data/lib/rails_error_dashboard/commands/log_error.rb +82 -23
  52. data/lib/rails_error_dashboard/commands/mute_error.rb +1 -0
  53. data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -0
  54. data/lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb +104 -0
  55. data/lib/rails_error_dashboard/commands/snooze_error.rb +34 -10
  56. data/lib/rails_error_dashboard/commands/unmute_error.rb +1 -0
  57. data/lib/rails_error_dashboard/commands/update_error_priority.rb +23 -2
  58. data/lib/rails_error_dashboard/commands/update_error_status.rb +33 -6
  59. data/lib/rails_error_dashboard/configuration.rb +19 -1
  60. data/lib/rails_error_dashboard/engine.rb +15 -0
  61. data/lib/rails_error_dashboard/queries/analytics_stats.rb +107 -26
  62. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  63. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +55 -50
  64. data/lib/rails_error_dashboard/queries/error_correlation.rb +6 -3
  65. data/lib/rails_error_dashboard/queries/errors_list.rb +15 -2
  66. data/lib/rails_error_dashboard/queries/similar_errors.rb +1 -1
  67. data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +43 -18
  68. data/lib/rails_error_dashboard/services/backtrace_processor.rb +3 -1
  69. data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +3 -1
  70. data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +13 -2
  71. data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +5 -3
  72. data/lib/rails_error_dashboard/services/encoding_sanitizer.rb +80 -0
  73. data/lib/rails_error_dashboard/services/error_broadcaster.rb +180 -32
  74. data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -3
  75. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +13 -0
  76. data/lib/rails_error_dashboard/services/exception_filter.rb +55 -0
  77. data/lib/rails_error_dashboard/services/git_head_reader.rb +102 -0
  78. data/lib/rails_error_dashboard/services/notification_throttler.rb +172 -28
  79. data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +47 -1
  80. data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +59 -3
  81. data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +25 -2
  82. data/lib/rails_error_dashboard/services/storm_protection/gate.rb +32 -5
  83. data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +99 -23
  84. data/lib/rails_error_dashboard/services/url_safety.rb +40 -0
  85. data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +11 -2
  86. data/lib/rails_error_dashboard/value_objects/error_context.rb +3 -1
  87. data/lib/rails_error_dashboard/version.rb +1 -1
  88. data/lib/rails_error_dashboard.rb +31 -0
  89. data/lib/tasks/error_dashboard.rake +54 -4
  90. metadata +10 -2
@@ -438,6 +438,7 @@ pl:
438
438
  issue_section:
439
439
  title: System zgłoszeń
440
440
  view_issue: Zobacz zgłoszenie
441
+ unsafe_link: "Ten odnośnik do zgłoszenia nie jest adresem URL http(s), dlatego jest wyświetlany jako tekst i nie można go otworzyć."
441
442
  linked: Powiązane
442
443
  issue_reference: "%{provider} #%{number}"
443
444
  states:
@@ -1016,6 +1017,16 @@ pl:
1016
1017
  other: "%{count} typu"
1017
1018
  few: "%{count} typy"
1018
1019
  many: "%{count} typów"
1020
+ errors_page:
1021
+ csrf:
1022
+ title: "Token sesji wygasł"
1023
+ message: "Wróć, odśwież stronę i spróbuj ponownie. Nic nie zostało zmienione."
1024
+ bad_request:
1025
+ title: "Nie udało się zrozumieć tego żądania"
1026
+ message: "Brakowało wymaganej wartości lub była ona nieprawidłowa. Wróć i spróbuj ponownie."
1027
+ not_acceptable:
1028
+ title: "Ten format jest niedostępny"
1029
+ message: "Panel nie może wygenerować formatu, o który poproszono w tym żądaniu."
1019
1030
  settings:
1020
1031
  title: Ustawienia
1021
1032
  version: v%{version}
@@ -1149,6 +1160,8 @@ pl:
1149
1160
  ustawiono)
1150
1161
  environment: Środowisko, do którego przypisywane są błędy (Rails.env, jeśli
1151
1162
  nie ustawiono)
1163
+ notification_burst_limit: "Powiadomienia o nowych błędach na okno czasowe i proces (0 = bez limitu)"
1164
+ notification_burst_window_seconds: "Długość okna czasowego powiadomień o nowych błędach"
1152
1165
  notification_environments: Powiadamiaj tylko dla tych środowisk (wszystkie,
1153
1166
  jeśli nie ustawiono)
1154
1167
  database: Aktywna baza danych dla dzienników błędów
@@ -1262,6 +1275,16 @@ pl:
1262
1275
  linked: Zgłoszenie zostało powiązane
1263
1276
  create_failed: 'Nie udało się utworzyć zgłoszenia: %{reason}'
1264
1277
  link_failed: 'Nie udało się powiązać zgłoszenia: %{reason}'
1278
+ status:
1279
+ updated: "Status zmieniono na %{status}"
1280
+ invalid_transition: "Nie można zmienić statusu z %{from} na %{to}"
1281
+ unknown: "Taki status nie istnieje"
1282
+ snooze:
1283
+ invalid_hours: "Czas odłożenia musi być całkowitą liczbą godzin od 1 do %{max}"
1284
+ priority:
1285
+ invalid: "Taki poziom priorytetu nie istnieje"
1286
+ assign:
1287
+ blank: "Podaj nazwę osoby, do której chcesz przypisać ten błąd"
1265
1288
  not_enabled: "%{feature} nie jest włączone. Włącz w %{file}"
1266
1289
  not_enabled_plural: "%{feature} nie są włączone. Włącz je w %{file}"
1267
1290
  not_enabled_options: "%{feature} nie jest włączone. Włącz %{options} w %{file}"
@@ -1303,6 +1326,7 @@ pl:
1303
1326
  not_configured: Śledzenie zgłoszeń nie jest skonfigurowane
1304
1327
  already_linked: 'Błąd ma już powiązane zgłoszenie: %{url}'
1305
1328
  url_required: Adres URL zgłoszenia jest wymagany
1329
+ url_invalid: Adres URL zgłoszenia musi zaczynać się od http:// lub https://
1306
1330
  error_not_found: 'Nie znaleziono błędu: %{id}'
1307
1331
  batch_failed:
1308
1332
  mute:
@@ -1441,6 +1465,10 @@ pl:
1441
1465
  few: "%{value} błędy"
1442
1466
  many: "%{value} błędów"
1443
1467
  view_in_dashboard: Zobacz w panelu
1468
+ burst:
1469
+ suppressed: "W aplikacji %{application} zgłoszono ponad %{limit} nowych błędów w ciągu %{window} sekund."
1470
+ still_recorded: "Powiadomienia o kolejnych nowych błędach są wstrzymane do końca tego okna czasowego; każdy błąd jest nadal zapisywany."
1471
+ dashboard: "Panel: %{url}"
1444
1472
  storm:
1445
1473
  detected: Wykryto sztorm błędów w %{application} o %{started_at}.
1446
1474
  mode:
@@ -436,6 +436,7 @@ pt-BR:
436
436
  issue_section:
437
437
  title: Rastreador de issues
438
438
  view_issue: Ver a issue
439
+ unsafe_link: "Este link da issue não é uma URL http(s), por isso é exibido como texto e não pode ser aberto."
439
440
  linked: Vinculada
440
441
  issue_reference: "%{provider} nº %{number}"
441
442
  states:
@@ -1000,6 +1001,16 @@ pt-BR:
1000
1001
  one: "%{count} tipo"
1001
1002
  other: "%{count} tipos"
1002
1003
  many: "%{count} tipos"
1004
+ errors_page:
1005
+ csrf:
1006
+ title: "O token da sua sessão expirou"
1007
+ message: "Volte, recarregue a página e tente novamente. Nada foi alterado."
1008
+ bad_request:
1009
+ title: "Não foi possível interpretar esta solicitação"
1010
+ message: "Um valor obrigatório estava ausente ou malformado. Volte e tente novamente."
1011
+ not_acceptable:
1012
+ title: "Este formato não está disponível"
1013
+ message: "O painel não consegue gerar o formato pedido por esta solicitação."
1003
1014
  settings:
1004
1015
  title: Configurações
1005
1016
  version: v%{version}
@@ -1122,6 +1133,8 @@ pt-BR:
1122
1133
  application_name: Nome desta aplicação (detectado automaticamente se não for
1123
1134
  definido)
1124
1135
  environment: Ambiente ao qual os erros são atribuídos (Rails.env se não definido)
1136
+ notification_burst_limit: "Notificações de novos erros por intervalo e por processo (0 = sem limite)"
1137
+ notification_burst_window_seconds: "Duração do intervalo de notificações de novos erros"
1125
1138
  notification_environments: Notificar apenas para estes ambientes (todos se
1126
1139
  não definido)
1127
1140
  database: Banco de dados ativo para os logs de erros
@@ -1232,6 +1245,16 @@ pt-BR:
1232
1245
  linked: Issue vinculada com sucesso
1233
1246
  create_failed: 'Não foi possível criar a issue: %{reason}'
1234
1247
  link_failed: 'Não foi possível vincular a issue: %{reason}'
1248
+ status:
1249
+ updated: "Status alterado para %{status}"
1250
+ invalid_transition: "Não é possível alterar o status de %{from} para %{to}"
1251
+ unknown: "Esse status não existe"
1252
+ snooze:
1253
+ invalid_hours: "O tempo de adiamento deve ser um número inteiro de horas entre 1 e %{max}"
1254
+ priority:
1255
+ invalid: "Esse nível de prioridade não existe"
1256
+ assign:
1257
+ blank: "Informe um nome para atribuir este erro"
1235
1258
  not_enabled: "%{feature} não está ativado. Ative em %{file}"
1236
1259
  not_enabled_plural: "%{feature} não estão ativados. Ative-os em %{file}"
1237
1260
  not_enabled_options: "%{feature} não está ativado. Ative %{options} em %{file}"
@@ -1274,6 +1297,7 @@ pt-BR:
1274
1297
  not_configured: O rastreamento de issues não está configurado
1275
1298
  already_linked: 'O erro já tem uma issue vinculada: %{url}'
1276
1299
  url_required: A URL da issue é obrigatória
1300
+ url_invalid: A URL da issue deve começar com http:// ou https://
1277
1301
  error_not_found: 'Erro não encontrado: %{id}'
1278
1302
  batch_failed:
1279
1303
  mute:
@@ -1405,6 +1429,10 @@ pt-BR:
1405
1429
  other: "%{value} erros"
1406
1430
  many: "%{value} erros"
1407
1431
  view_in_dashboard: Ver no painel
1432
+ burst:
1433
+ suppressed: "Mais de %{limit} novos erros foram registrados em %{application} em %{window} segundos."
1434
+ still_recorded: "As notificações de outros novos erros ficam pausadas até o fim desse intervalo; todos os erros continuam sendo registrados."
1435
+ dashboard: "Painel: %{url}"
1408
1436
  storm:
1409
1437
  detected: Tempestade de erros detectada em %{application} às %{started_at}.
1410
1438
  mode:
@@ -441,6 +441,7 @@ ru:
441
441
  issue_section:
442
442
  title: Трекер задач
443
443
  view_issue: Открыть задачу
444
+ unsafe_link: "Эта ссылка на задачу не является URL-адресом http(s), поэтому она показана как текст и не может быть открыта."
444
445
  linked: Привязана
445
446
  issue_reference: "%{provider} #%{number}"
446
447
  states:
@@ -1019,6 +1020,16 @@ ru:
1019
1020
  other: "%{count} типа"
1020
1021
  few: "%{count} типа"
1021
1022
  many: "%{count} типов"
1023
+ errors_page:
1024
+ csrf:
1025
+ title: "Срок действия токена сеанса истёк"
1026
+ message: "Вернитесь назад, перезагрузите страницу и повторите попытку. Ничего не было изменено."
1027
+ bad_request:
1028
+ title: "Не удалось обработать этот запрос"
1029
+ message: "Обязательное значение отсутствовало или имело неверный формат. Вернитесь назад и повторите попытку."
1030
+ not_acceptable:
1031
+ title: "Этот формат недоступен"
1032
+ message: "Панель не может сформировать формат, запрошенный в этом запросе."
1022
1033
  settings:
1023
1034
  title: Настройки
1024
1035
  version: v%{version}
@@ -1151,6 +1162,8 @@ ru:
1151
1162
  application_name: Имя этого приложения (определяется автоматически, если не
1152
1163
  задано)
1153
1164
  environment: Окружение, к которому относятся ошибки (Rails.env, если не задано)
1165
+ notification_burst_limit: "Уведомления о новых ошибках за интервал на процесс (0 = без ограничения)"
1166
+ notification_burst_window_seconds: "Длительность интервала уведомлений о новых ошибках"
1154
1167
  notification_environments: Уведомлять только для этих окружений (все, если
1155
1168
  не задано)
1156
1169
  database: Активная база данных для журнала ошибок
@@ -1261,6 +1274,16 @@ ru:
1261
1274
  linked: Задача успешно привязана
1262
1275
  create_failed: 'Не удалось создать задачу: %{reason}'
1263
1276
  link_failed: 'Не удалось привязать задачу: %{reason}'
1277
+ status:
1278
+ updated: "Статус изменён на %{status}"
1279
+ invalid_transition: "Нельзя изменить статус с %{from} на %{to}"
1280
+ unknown: "Такого статуса не существует"
1281
+ snooze:
1282
+ invalid_hours: "Время откладывания должно быть целым числом часов от 1 до %{max}"
1283
+ priority:
1284
+ invalid: "Такого уровня приоритета не существует"
1285
+ assign:
1286
+ blank: "Введите имя того, кому назначить эту ошибку"
1264
1287
  not_enabled: "%{feature} не включено. Включите в %{file}"
1265
1288
  not_enabled_plural: "%{feature} не включены. Включите их в %{file}"
1266
1289
  not_enabled_options: "%{feature} не включено. Включите %{options} в %{file}"
@@ -1303,6 +1326,7 @@ ru:
1303
1326
  not_configured: Отслеживание задач не настроено
1304
1327
  already_linked: 'К ошибке уже привязана задача: %{url}'
1305
1328
  url_required: Требуется URL задачи
1329
+ url_invalid: URL задачи должен начинаться с http:// или https://
1306
1330
  error_not_found: 'Ошибка не найдена: %{id}'
1307
1331
  batch_failed:
1308
1332
  mute:
@@ -1441,6 +1465,10 @@ ru:
1441
1465
  few: "%{value} ошибки"
1442
1466
  many: "%{value} ошибок"
1443
1467
  view_in_dashboard: Открыть в панели
1468
+ burst:
1469
+ suppressed: "В приложении %{application} за %{window} секунд зафиксировано более %{limit} новых ошибок."
1470
+ still_recorded: "Уведомления о дальнейших новых ошибках приостановлены до конца этого интервала; каждая ошибка по-прежнему записывается."
1471
+ dashboard: "Панель: %{url}"
1444
1472
  storm:
1445
1473
  detected: Обнаружен шторм ошибок в %{application} в %{started_at}.
1446
1474
  mode:
@@ -438,6 +438,7 @@ uk:
438
438
  issue_section:
439
439
  title: Трекер задач
440
440
  view_issue: Переглянути задачу
441
+ unsafe_link: "Це посилання на задачу не є URL-адресою http(s), тому воно показане як текст і не може бути відкрите."
441
442
  linked: Приєднано
442
443
  issue_reference: "%{provider} #%{number}"
443
444
  states:
@@ -1016,6 +1017,16 @@ uk:
1016
1017
  other: "%{count} типу"
1017
1018
  few: "%{count} типи"
1018
1019
  many: "%{count} типів"
1020
+ errors_page:
1021
+ csrf:
1022
+ title: "Термін дії токена сеансу минув"
1023
+ message: "Поверніться назад, перезавантажте сторінку та спробуйте ще раз. Нічого не було змінено."
1024
+ bad_request:
1025
+ title: "Не вдалося обробити цей запит"
1026
+ message: "Обов'язкове значення було відсутнє або мало неправильний формат. Поверніться назад і спробуйте ще раз."
1027
+ not_acceptable:
1028
+ title: "Цей формат недоступний"
1029
+ message: "Панель не може сформувати формат, запитаний у цьому запиті."
1019
1030
  settings:
1020
1031
  title: Налаштування
1021
1032
  version: v%{version}
@@ -1147,6 +1158,8 @@ uk:
1147
1158
  application_name: Назва цього застосунку (визначається автоматично, якщо не
1148
1159
  задано)
1149
1160
  environment: Середовище, до якого належать помилки (Rails.env, якщо не задано)
1161
+ notification_burst_limit: "Сповіщення про нові помилки за інтервал на процес (0 = без обмеження)"
1162
+ notification_burst_window_seconds: "Тривалість інтервалу сповіщень про нові помилки"
1150
1163
  notification_environments: Сповіщати лише для цих середовищ (усі, якщо не
1151
1164
  задано)
1152
1165
  database: Активна база даних для журналів помилок
@@ -1260,6 +1273,16 @@ uk:
1260
1273
  linked: Задачу успішно приєднано
1261
1274
  create_failed: 'Не вдалося створити задачу: %{reason}'
1262
1275
  link_failed: 'Не вдалося приєднати задачу: %{reason}'
1276
+ status:
1277
+ updated: "Статус змінено на %{status}"
1278
+ invalid_transition: "Неможливо змінити статус з %{from} на %{to}"
1279
+ unknown: "Такого статусу не існує"
1280
+ snooze:
1281
+ invalid_hours: "Час відкладення має бути цілим числом годин від 1 до %{max}"
1282
+ priority:
1283
+ invalid: "Такого рівня пріоритету не існує"
1284
+ assign:
1285
+ blank: "Введіть ім'я того, кому призначити цю помилку"
1263
1286
  not_enabled: "%{feature} не увімкнено. Увімкніть у %{file}"
1264
1287
  not_enabled_plural: "%{feature} не увімкнено. Увімкніть їх у %{file}"
1265
1288
  not_enabled_options: "%{feature} не увімкнено. Увімкніть %{options} у %{file}"
@@ -1301,6 +1324,7 @@ uk:
1301
1324
  not_configured: Відстеження задач не налаштовано
1302
1325
  already_linked: 'До помилки вже приєднано задачу: %{url}'
1303
1326
  url_required: Потрібна URL-адреса задачі
1327
+ url_invalid: URL-адреса задачі має починатися з http:// або https://
1304
1328
  error_not_found: 'Помилку не знайдено: %{id}'
1305
1329
  batch_failed:
1306
1330
  mute:
@@ -1439,6 +1463,10 @@ uk:
1439
1463
  few: "%{value} помилки"
1440
1464
  many: "%{value} помилок"
1441
1465
  view_in_dashboard: Переглянути в панелі
1466
+ burst:
1467
+ suppressed: "У застосунку %{application} за %{window} секунд зафіксовано понад %{limit} нових помилок."
1468
+ still_recorded: "Сповіщення про подальші нові помилки призупинено до кінця цього інтервалу; кожна помилка, як і раніше, записується."
1469
+ dashboard: "Панель: %{url}"
1442
1470
  storm:
1443
1471
  detected: Виявлено шторм помилок у %{application} о %{started_at}.
1444
1472
  mode:
@@ -380,6 +380,7 @@ zh-CN:
380
380
  issue_section:
381
381
  title: 工单系统
382
382
  view_issue: 查看工单
383
+ unsafe_link: "此工单链接不是 http(s) URL,因此仅以文本显示,无法打开。"
383
384
  linked: 已关联
384
385
  issue_reference: "%{provider} #%{number}"
385
386
  states:
@@ -870,6 +871,16 @@ zh-CN:
870
871
  user_types_html: 用户 %{user} · %{types}
871
872
  types:
872
873
  other: "%{count} 种类型"
874
+ errors_page:
875
+ csrf:
876
+ title: "会话令牌已过期"
877
+ message: "请返回并重新加载页面后重试。未做任何更改。"
878
+ bad_request:
879
+ title: "无法理解此请求"
880
+ message: "缺少必需的值或其格式不正确。请返回后重试。"
881
+ not_acceptable:
882
+ title: "此格式不可用"
883
+ message: "仪表板无法生成此请求所要求的格式。"
873
884
  settings:
874
885
  title: 设置
875
886
  version: v%{version}
@@ -963,6 +974,8 @@ zh-CN:
963
974
  sampling_rate: 错误捕获的采样比例
964
975
  application_name: 本应用的名称(未设置时自动检测)
965
976
  environment: 错误所属的环境(未设置时为 Rails.env)
977
+ notification_burst_limit: "每个时间窗口、每个进程的新错误通知数(0 = 不限制)"
978
+ notification_burst_window_seconds: "新错误通知时间窗口的长度"
966
979
  notification_environments: 仅对这些环境发送通知(未设置时为全部)
967
980
  database: 错误日志使用的数据库
968
981
  use_separate_database: 为错误日志使用独立数据库
@@ -1049,6 +1062,16 @@ zh-CN:
1049
1062
  linked: 工单关联成功
1050
1063
  create_failed: 创建工单失败:%{reason}
1051
1064
  link_failed: 关联工单失败:%{reason}
1065
+ status:
1066
+ updated: "状态已更改为 %{status}"
1067
+ invalid_transition: "无法将状态从 %{from} 更改为 %{to}"
1068
+ unknown: "该状态不存在"
1069
+ snooze:
1070
+ invalid_hours: "延后时间必须是 1 到 %{max} 之间的整数小时"
1071
+ priority:
1072
+ invalid: "该优先级不存在"
1073
+ assign:
1074
+ blank: "请输入要分配此错误的人员姓名"
1052
1075
  not_enabled: "%{feature}未启用。请在 %{file} 中启用。"
1053
1076
  not_enabled_plural: "%{feature}未启用。请在 %{file} 中启用。"
1054
1077
  not_enabled_options: "%{feature}未启用。请在 %{file} 中启用 %{options}。"
@@ -1087,6 +1110,7 @@ zh-CN:
1087
1110
  not_configured: 工单跟踪尚未配置
1088
1111
  already_linked: 该错误已关联工单:%{url}
1089
1112
  url_required: 必须提供工单 URL
1113
+ url_invalid: 工单 URL 必须以 http:// 或 https:// 开头
1090
1114
  error_not_found: 未找到错误:%{id}
1091
1115
  batch_failed:
1092
1116
  mute:
@@ -1203,6 +1227,10 @@ zh-CN:
1203
1227
  threshold_errors:
1204
1228
  other: "%{value} 个错误"
1205
1229
  view_in_dashboard: 在面板中查看
1230
+ burst:
1231
+ suppressed: "%{application} 在 %{window} 秒内报告了超过 %{limit} 个新错误。"
1232
+ still_recorded: "在该时间窗口结束之前,后续新错误的通知将暂停;所有错误仍会被记录。"
1233
+ dashboard: "面板:%{url}"
1206
1234
  storm:
1207
1235
  detected: 在 %{application} 于 %{started_at} 检测到错误风暴。
1208
1236
  mode:
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When a notification was last sent for this error group.
4
+ #
5
+ # The notification cooldown used to live in a Hash inside each process. Every
6
+ # Puma worker and every job process had its own copy, so one bad deploy that
7
+ # reopened an error notified once PER PROCESS, and a restart forgot the
8
+ # cooldown entirely.
9
+ #
10
+ # With the timestamp on the row the cooldown is claimed in the database with a
11
+ # single conditional UPDATE, which exactly one process can win:
12
+ #
13
+ # UPDATE ... SET last_notified_at = now
14
+ # WHERE id = ? AND (last_notified_at IS NULL OR last_notified_at < cutoff)
15
+ #
16
+ # No index: the row is always addressed by primary key.
17
+ #
18
+ # Nullable, no default: NULL means "never notified". Until this migration has
19
+ # run the gem falls back to the in-process cooldown, so upgrading the gem
20
+ # before migrating is safe.
21
+ #
22
+ # up/down rather than `change`: the column_exists? guard that makes `up` safe
23
+ # to re-run would make a reversed `change` skip the removal.
24
+ class AddLastNotifiedAtToErrorLogs < ActiveRecord::Migration[7.0]
25
+ TABLE = :rails_error_dashboard_error_logs
26
+
27
+ def up
28
+ return unless table_exists?(TABLE)
29
+ return if column_exists?(TABLE, :last_notified_at)
30
+
31
+ add_column TABLE, :last_notified_at, :datetime
32
+ end
33
+
34
+ def down
35
+ return unless table_exists?(TABLE)
36
+ return unless column_exists?(TABLE, :last_notified_at)
37
+
38
+ remove_column TABLE, :last_notified_at
39
+ end
40
+ end
@@ -21,6 +21,16 @@ RailsErrorDashboard.configure do |config|
21
21
  # Only notify (Slack, email, Discord, PagerDuty, webhooks, storm and
22
22
  # baseline alerts) for these environments. nil = every environment.
23
23
  # config.notification_environments = %w[production]
24
+ #
25
+ # Notification throttling. The per-error cooldown stops one error that keeps
26
+ # being reopened from paging repeatedly; it is held in the database, so it
27
+ # applies across every worker. The burst cap is for the deploy that throws
28
+ # hundreds of DIFFERENT new errors: after N new-error notifications in a
29
+ # window (per process) one summary message replaces the rest. Every error
30
+ # is still recorded.
31
+ # config.notification_cooldown_minutes = 5 # 0 = no cooldown
32
+ # config.notification_burst_limit = 10 # 0 = no cap
33
+ # config.notification_burst_window_seconds = 60
24
34
 
25
35
  # === Custom Authentication (optional) ===
26
36
  # Use your app's existing auth instead of HTTP Basic Auth.
@@ -61,7 +71,8 @@ RailsErrorDashboard.configure do |config|
61
71
  # User model for error associations
62
72
  config.user_model = "User"
63
73
 
64
- # Error retention policy (days to keep errors before automatic deletion)
74
+ # Error retention policy: an error is deleted once it has not been seen for
75
+ # this many days (an error that is still occurring is never deleted)
65
76
  # Set to nil to keep errors forever (not recommended for production)
66
77
  # Run cleanup manually: rails error_dashboard:retention_cleanup
67
78
  # Or schedule the job: RailsErrorDashboard::RetentionCleanupJob.perform_later
@@ -4,7 +4,11 @@ module RailsErrorDashboard
4
4
  module Commands
5
5
  # Command: Assign an error to a user
6
6
  # This is a write operation that updates assignment fields on an ErrorLog record
7
+ # Returns {success: bool, error: ErrorLog}; a failure also carries
8
+ # reason: :blank_assignee and writes nothing.
7
9
  class AssignError
10
+ MAX_ASSIGNEE_LENGTH = 255
11
+
8
12
  def self.call(error_id, assigned_to:)
9
13
  new(error_id, assigned_to).call
10
14
  end
@@ -16,12 +20,18 @@ module RailsErrorDashboard
16
20
 
17
21
  def call
18
22
  error = ErrorLog.find(@error_id)
23
+
24
+ # A blank name used to "assign" the error to nobody and still move it to
25
+ # in_progress. A nested parameter is not a name either.
26
+ assignee = @assigned_to.is_a?(String) ? @assigned_to.strip.presence : nil
27
+ return { success: false, error: error, reason: :blank_assignee } unless assignee
28
+
19
29
  error.update!(
20
- assigned_to: @assigned_to,
30
+ assigned_to: assignee.truncate(MAX_ASSIGNEE_LENGTH, omission: ""),
21
31
  assigned_at: Time.current,
22
32
  status: "in_progress" # Auto-transition to in_progress when assigned
23
33
  )
24
- error
34
+ { success: true, error: error }
25
35
  end
26
36
  end
27
37
  end
@@ -39,6 +39,8 @@ module RailsErrorDashboard
39
39
  updated += ErrorLog.where(id: id, environment: nil).update_all(environment: env)
40
40
  end
41
41
  end
42
+ # Analytics groups by environment, and its result is cached.
43
+ Services::AnalyticsCacheManager.clear if updated.positive?
42
44
  updated
43
45
  end
44
46
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Commands
5
+ # Command: give resolved errors that have no resolved_at a best-known one
6
+ #
7
+ # Errors resolved through the status workflow before 0.13.0 were marked
8
+ # resolved without a resolved_at, so MTTR never counted them. The moment of
9
+ # resolution was not recorded anywhere, but it was the row's last write
10
+ # unless something touched it afterwards, so updated_at is the closest
11
+ # available value.
12
+ #
13
+ # Behind `rake error_dashboard:backfill_resolved_at`. Idempotent: it only
14
+ # selects rows whose resolved_at is still NULL.
15
+ #
16
+ # @example
17
+ # BackfillResolvedAt.call # => { updated: 42 }
18
+ class BackfillResolvedAt
19
+ BATCH_SIZE = 1000
20
+
21
+ def self.call(batch_size: BATCH_SIZE)
22
+ new(batch_size: batch_size).call
23
+ end
24
+
25
+ def initialize(batch_size: BATCH_SIZE)
26
+ @batch_size = batch_size
27
+ end
28
+
29
+ def call
30
+ updated = 0
31
+
32
+ ErrorLog.where(resolved: true, resolved_at: nil).in_batches(of: @batch_size) do |batch|
33
+ # SQL column reference, not a Ruby value: each row gets its own
34
+ # updated_at. update_all leaves updated_at itself alone.
35
+ updated += batch.update_all("resolved_at = updated_at")
36
+ end
37
+
38
+ { updated: updated }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -23,6 +23,7 @@ module RailsErrorDashboard
23
23
  error_ids_to_delete = errors.pluck(:id)
24
24
 
25
25
  errors.destroy_all
26
+ Services::AnalyticsCacheManager.clear
26
27
 
27
28
  # Dispatch plugin event for batch deleted errors
28
29
  PluginRegistry.dispatch(:on_errors_batch_deleted, error_ids_to_delete) if error_ids_to_delete.any?
@@ -41,6 +41,8 @@ module RailsErrorDashboard
41
41
  end
42
42
  end
43
43
 
44
+ Services::AnalyticsCacheManager.clear if muted_count.positive?
45
+
44
46
  PluginRegistry.dispatch(:on_errors_batch_muted, muted_errors) if muted_errors.any?
45
47
 
46
48
  {
@@ -45,6 +45,8 @@ module RailsErrorDashboard
45
45
  end
46
46
 
47
47
  # Dispatch plugin event for batch resolved errors
48
+ Services::AnalyticsCacheManager.clear if resolved_count.positive?
49
+
48
50
  PluginRegistry.dispatch(:on_errors_batch_resolved, resolved_errors) if resolved_errors.any?
49
51
 
50
52
  {
@@ -39,6 +39,8 @@ module RailsErrorDashboard
39
39
  end
40
40
  end
41
41
 
42
+ Services::AnalyticsCacheManager.clear if unmuted_count.positive?
43
+
42
44
  PluginRegistry.dispatch(:on_errors_batch_unmuted, unmuted_errors) if unmuted_errors.any?
43
45
 
44
46
  {
@@ -10,8 +10,9 @@ module RailsErrorDashboard
10
10
  # and two concurrent captures could both read count N and both write N+1.
11
11
  #
12
12
  # Search order:
13
+ # 0. wont_fix errors with same hash (any age) → increment, status untouched
13
14
  # 1. Unresolved errors with same hash within 24 hours → increment occurrence count
14
- # 2. Resolved/wont_fix errors with same hash (any age) → reopen and increment
15
+ # 2. Resolved errors with same hash (any age) → reopen and increment
15
16
  # 3. No match → create new error record
16
17
  #
17
18
  # Environment is a MATCH dimension, not part of the hash: the same error in
@@ -42,11 +43,15 @@ module RailsErrorDashboard
42
43
 
43
44
  def call
44
45
  ErrorLog.transaction do
46
+ # Priority 0: a wont_fix row absorbs its recurrences, at any age
47
+ sticky = find_wont_fix
48
+ next increment_existing(sticky) if sticky
49
+
45
50
  # Priority 1: Find unresolved match (existing behavior)
46
51
  existing = find_unresolved
47
52
  next increment_existing(existing) if existing
48
53
 
49
- # Priority 2: Find resolved/wont_fix match → reopen
54
+ # Priority 2: Find resolved match → reopen
50
55
  resolved = find_resolved
51
56
  next reopen_existing(resolved) if resolved
52
57
 
@@ -57,9 +62,26 @@ module RailsErrorDashboard
57
62
 
58
63
  private
59
64
 
65
+ # The three lookups are DISJOINT by status, so which row a recurrence
66
+ # lands on never depends on the order they happen to run in.
67
+
68
+ # "Won't fix" is a decision that the error recurs and will not be acted
69
+ # on, so it has no time window: the row counts its recurrences for as
70
+ # long as it keeps the status. It used to be matched by find_unresolved
71
+ # for 24 hours and then REOPENED by find_resolved -- sticky for a day,
72
+ # after which the triage decision was silently thrown away.
73
+ def find_wont_fix
74
+ with_environment(
75
+ ErrorLog
76
+ .where(error_hash: @error_hash)
77
+ .where(application_id: @attributes[:application_id])
78
+ .where(status: "wont_fix")
79
+ ).lock.order(last_seen_at: :desc).first
80
+ end
81
+
60
82
  def find_unresolved
61
83
  with_environment(
62
- ErrorLog.unresolved
84
+ not_wont_fix(ErrorLog.unresolved)
63
85
  .where(error_hash: @error_hash)
64
86
  .where(application_id: @attributes[:application_id])
65
87
  .where("occurred_at >= ?", 24.hours.ago)
@@ -71,10 +93,16 @@ module RailsErrorDashboard
71
93
  ErrorLog
72
94
  .where(error_hash: @error_hash)
73
95
  .where(application_id: @attributes[:application_id])
74
- .where(status: %w[resolved wont_fix])
96
+ .where(status: "resolved")
75
97
  ).lock.order(last_seen_at: :desc).first
76
98
  end
77
99
 
100
+ # Spelled out rather than where.not(status: "wont_fix"): in SQL that also
101
+ # drops every row whose status is NULL.
102
+ def not_wont_fix(scope)
103
+ scope.where("status IS NULL OR status <> ?", "wont_fix")
104
+ end
105
+
78
106
  # Restrict to this occurrence's environment or a legacy NULL row, exact
79
107
  # first. Literal SQL, no interpolation. A blank environment (column not
80
108
  # migrated yet, or an attribute-less caller) leaves the scope unchanged.
@@ -235,9 +263,13 @@ module RailsErrorDashboard
235
263
  ErrorLog.create!(new_record_attributes)
236
264
  end
237
265
  rescue ActiveRecord::RecordNotUnique
238
- # Race condition: another process created the same error
266
+ # Race condition: another process created the same error. Same three
267
+ # lookups, same order, as the first pass.
268
+ retry_sticky = find_wont_fix
269
+ return increment_existing(retry_sticky) if retry_sticky
270
+
239
271
  retry_existing = with_environment(
240
- ErrorLog.unresolved
272
+ not_wont_fix(ErrorLog.unresolved)
241
273
  .where(error_hash: @error_hash)
242
274
  .where(application_id: @attributes[:application_id])
243
275
  .where("occurred_at >= ?", 24.hours.ago)
@@ -257,7 +289,7 @@ module RailsErrorDashboard
257
289
  ErrorLog
258
290
  .where(error_hash: @error_hash)
259
291
  .where(application_id: @attributes[:application_id])
260
- .where(status: %w[resolved wont_fix])
292
+ .where(status: "resolved")
261
293
  ).lock.first
262
294
 
263
295
  if retry_resolved
@@ -28,8 +28,10 @@ module RailsErrorDashboard
28
28
  app_id = current_application_id
29
29
 
30
30
  @counts.each do |key, count|
31
+ # Path and user agent are attacker-supplied, so invalid bytes are the
32
+ # expected case here. Scrub the whole key before it is split.
31
33
  rule, match_type, discriminator, path, http_method, user_agent =
32
- Services::RackAttackTracker.parse_key(key)
34
+ Services::RackAttackTracker.parse_key(Services::EncodingSanitizer.scrub(key.to_s))
33
35
 
34
36
  next if rule.blank? || match_type.blank?
35
37