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.
- checksums.yaml +4 -4
- data/app/controllers/rails_error_dashboard/application_controller.rb +92 -11
- data/app/controllers/rails_error_dashboard/errors_controller.rb +111 -34
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +3 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +46 -1
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +8 -0
- data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +71 -0
- data/app/jobs/rails_error_dashboard/notification_burst_summary_job.rb +55 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +68 -1
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +8 -44
- data/app/models/rails_error_dashboard/error_baseline.rb +12 -9
- data/app/models/rails_error_dashboard/error_comment.rb +0 -5
- data/app/models/rails_error_dashboard/error_log.rb +9 -3
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/models/rails_error_dashboard/error_occurrence.rb +9 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +11 -3
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +18 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +22 -8
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +15 -6
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +14 -8
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/index.html.erb +6 -1
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/releases.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/settings.html.erb +2 -0
- data/config/locales/de.yml +28 -0
- data/config/locales/en.yml +35 -0
- data/config/locales/es.yml +28 -0
- data/config/locales/fr.yml +28 -0
- data/config/locales/it.yml +28 -0
- data/config/locales/ja.yml +28 -0
- data/config/locales/pl.yml +28 -0
- data/config/locales/pt-BR.yml +28 -0
- data/config/locales/ru.yml +28 -0
- data/config/locales/uk.yml +28 -0
- data/config/locales/zh-CN.yml +28 -0
- data/db/migrate/20260917000001_add_last_notified_at_to_error_logs.rb +40 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +12 -1
- data/lib/rails_error_dashboard/commands/assign_error.rb +12 -2
- data/lib/rails_error_dashboard/commands/backfill_environments.rb +2 -0
- data/lib/rails_error_dashboard/commands/backfill_resolved_at.rb +42 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -0
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +2 -0
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +39 -7
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +3 -1
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +27 -4
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +5 -2
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +1 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +82 -23
- data/lib/rails_error_dashboard/commands/mute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -0
- data/lib/rails_error_dashboard/commands/scrub_invalid_encoding.rb +104 -0
- data/lib/rails_error_dashboard/commands/snooze_error.rb +34 -10
- data/lib/rails_error_dashboard/commands/unmute_error.rb +1 -0
- data/lib/rails_error_dashboard/commands/update_error_priority.rb +23 -2
- data/lib/rails_error_dashboard/commands/update_error_status.rb +33 -6
- data/lib/rails_error_dashboard/configuration.rb +19 -1
- data/lib/rails_error_dashboard/engine.rb +15 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +107 -26
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +55 -50
- data/lib/rails_error_dashboard/queries/error_correlation.rb +6 -3
- data/lib/rails_error_dashboard/queries/errors_list.rb +15 -2
- data/lib/rails_error_dashboard/queries/similar_errors.rb +1 -1
- data/lib/rails_error_dashboard/services/analytics_cache_manager.rb +43 -18
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +3 -1
- data/lib/rails_error_dashboard/services/cause_chain_extractor.rb +3 -1
- data/lib/rails_error_dashboard/services/codeberg_issue_client.rb +13 -2
- data/lib/rails_error_dashboard/services/diagnostic_dump_generator.rb +5 -3
- data/lib/rails_error_dashboard/services/encoding_sanitizer.rb +80 -0
- data/lib/rails_error_dashboard/services/error_broadcaster.rb +180 -32
- data/lib/rails_error_dashboard/services/error_hash_generator.rb +9 -3
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +13 -0
- data/lib/rails_error_dashboard/services/exception_filter.rb +55 -0
- data/lib/rails_error_dashboard/services/git_head_reader.rb +102 -0
- data/lib/rails_error_dashboard/services/notification_throttler.rb +172 -28
- data/lib/rails_error_dashboard/services/sensitive_data_filter.rb +47 -1
- data/lib/rails_error_dashboard/services/storm_protection/circuit_breaker.rb +59 -3
- data/lib/rails_error_dashboard/services/storm_protection/fingerprint_buckets.rb +25 -2
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +32 -5
- data/lib/rails_error_dashboard/services/swallowed_exception_tracker.rb +99 -23
- data/lib/rails_error_dashboard/services/url_safety.rb +40 -0
- data/lib/rails_error_dashboard/subscribers/issue_tracker_subscriber.rb +11 -2
- data/lib/rails_error_dashboard/value_objects/error_context.rb +3 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +31 -0
- data/lib/tasks/error_dashboard.rake +54 -4
- metadata +10 -2
data/config/locales/pl.yml
CHANGED
|
@@ -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:
|
data/config/locales/pt-BR.yml
CHANGED
|
@@ -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:
|
data/config/locales/ru.yml
CHANGED
|
@@ -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:
|
data/config/locales/uk.yml
CHANGED
|
@@ -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:
|
data/config/locales/zh-CN.yml
CHANGED
|
@@ -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
|
|
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:
|
|
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?
|
|
@@ -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
|
{
|
|
@@ -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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|