rails_error_dashboard 0.15.0 → 0.16.1
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/README.md +9 -4
- data/app/controllers/rails_error_dashboard/errors_controller.rb +29 -2
- data/app/controllers/rails_error_dashboard/webhooks_controller.rb +10 -9
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +7 -14
- data/app/helpers/rails_error_dashboard/overview_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +5 -0
- data/app/models/rails_error_dashboard/error_log.rb +11 -8
- data/app/models/rails_error_dashboard/error_occurrence.rb +1 -0
- data/app/models/rails_error_dashboard/error_occurrence_context.rb +14 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +3 -1
- data/app/views/rails_error_dashboard/errors/_captured_occurrences.html.erb +45 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +10 -3
- data/app/views/rails_error_dashboard/errors/_occurrence_banner.html.erb +12 -0
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +2 -2
- data/app/views/rails_error_dashboard/errors/overview.html.erb +14 -7
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +26 -17
- data/app/views/rails_error_dashboard/errors/settings.html.erb +6 -5
- data/app/views/rails_error_dashboard/errors/show.html.erb +7 -4
- data/config/locales/de.yml +21 -1
- data/config/locales/en.yml +23 -1
- data/config/locales/es.yml +22 -1
- data/config/locales/fr.yml +22 -1
- data/config/locales/it.yml +22 -1
- data/config/locales/ja.yml +20 -1
- data/config/locales/pl.yml +23 -1
- data/config/locales/pt-BR.yml +22 -1
- data/config/locales/ru.yml +23 -1
- data/config/locales/uk.yml +23 -1
- data/config/locales/zh-CN.yml +20 -1
- data/db/migrate/20260106094256_backfill_application_for_existing_errors.rb +3 -3
- data/db/migrate/20261015000001_create_rails_error_dashboard_error_occurrence_contexts.rb +35 -0
- data/db/migrate/20261015000002_add_capture_source_to_error_occurrences_and_error_logs.rb +46 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +14 -5
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +11 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +7 -2
- data/lib/rails_error_dashboard/commands/find_or_increment_error.rb +6 -0
- data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_rack_attack_events.rb +2 -4
- data/lib/rails_error_dashboard/commands/flush_storm_counts.rb +7 -6
- data/lib/rails_error_dashboard/commands/flush_swallowed_exceptions.rb +2 -4
- data/lib/rails_error_dashboard/commands/log_error.rb +19 -7
- data/lib/rails_error_dashboard/commands/record_occurrence_context.rb +60 -0
- data/lib/rails_error_dashboard/configuration.rb +26 -3
- data/lib/rails_error_dashboard/error_reporter.rb +4 -2
- data/lib/rails_error_dashboard/manual_error_reporter.rb +11 -1
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +18 -4
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +0 -12
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +59 -33
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +55 -18
- data/lib/rails_error_dashboard/services/backtrace_processor.rb +16 -7
- data/lib/rails_error_dashboard/services/crash_capture.rb +5 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +1 -6
- data/lib/rails_error_dashboard/services/markdown_error_formatter.rb +15 -16
- data/lib/rails_error_dashboard/services/repository_url.rb +18 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +3 -9
- data/lib/rails_error_dashboard/services/storm_protection/count_buffer.rb +6 -3
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +6 -1
- data/lib/rails_error_dashboard/value_objects/occurrence_snapshot.rb +23 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +16 -0
- data/lib/tasks/error_dashboard.rake +2 -3
- metadata +10 -2
data/config/locales/ru.yml
CHANGED
|
@@ -36,6 +36,9 @@ ru:
|
|
|
36
36
|
healthy: "✅ В норме"
|
|
37
37
|
warning: "⚠️ Предупреждение"
|
|
38
38
|
critical: "\U0001F534 Критично"
|
|
39
|
+
not_rated: "Без оценки"
|
|
40
|
+
not_rated_hint: "Оценка сравнивает платформы, поэтому нужны ошибки как минимум на двух."
|
|
41
|
+
nothing_resolved: "Ни одна ошибка за этот период ещё не решена"
|
|
39
42
|
device_type:
|
|
40
43
|
mobile: Мобильное
|
|
41
44
|
tablet: Планшет
|
|
@@ -248,6 +251,10 @@ ru:
|
|
|
248
251
|
inactive_body: включите, чтобы видеть выполненные строки
|
|
249
252
|
enable: Включить
|
|
250
253
|
disable: Отключить
|
|
254
|
+
occurrence:
|
|
255
|
+
title: "Событие #%{id}"
|
|
256
|
+
body: "Breadcrumbs, состояние системы и переменные относятся к этому событию. Всё остальное показывает последний захват."
|
|
257
|
+
show_latest: "Показать последнее"
|
|
251
258
|
baselines:
|
|
252
259
|
title: Статистика базовой линии
|
|
253
260
|
anomaly_label: 'Аномалия:'
|
|
@@ -362,7 +369,7 @@ ru:
|
|
|
362
369
|
types:
|
|
363
370
|
business_hours:
|
|
364
371
|
label: Рабочие часы
|
|
365
|
-
description: Пик ошибок приходится на рабочие часы (9:00–17:
|
|
372
|
+
description: Пик ошибок приходится на рабочие часы (9:00–17:59), что указывает
|
|
366
373
|
на связь с активностью пользователей
|
|
367
374
|
night:
|
|
368
375
|
label: Ночная закономерность
|
|
@@ -523,6 +530,14 @@ ru:
|
|
|
523
530
|
few: "+ ещё %{count}"
|
|
524
531
|
many: "+ ещё %{count}"
|
|
525
532
|
view_errors: Смотреть ошибки
|
|
533
|
+
captured_occurrences:
|
|
534
|
+
title: "Сохранённые события"
|
|
535
|
+
hint: "Breadcrumbs, состояние системы и переменные хранятся для последних событий (occurrence_context_limit). Откройте событие, чтобы их увидеть."
|
|
536
|
+
column_occurrence: "Событие"
|
|
537
|
+
column_occurred_at: "Время"
|
|
538
|
+
column_version: "Версия"
|
|
539
|
+
view: "Открыть"
|
|
540
|
+
viewing: "Открыто"
|
|
526
541
|
co_occurring:
|
|
527
542
|
title: Сопутствующие ошибки
|
|
528
543
|
badge: Временные закономерности
|
|
@@ -777,6 +792,7 @@ ru:
|
|
|
777
792
|
many: "%{count} кадров"
|
|
778
793
|
click_to_expand: "(нажмите, чтобы развернуть)"
|
|
779
794
|
view_source: Смотреть исходный код
|
|
795
|
+
client_report_note: "Эти фреймы пришли из отчёта клиента (ManualErrorReporter). Они описывают код, выполнявшийся на клиенте, поэтому RED не открывает для них исходные файлы, не показывает фрагменты и не запускает git blame."
|
|
780
796
|
none: Backtrace недоступен
|
|
781
797
|
row:
|
|
782
798
|
new_badge: НОВАЯ
|
|
@@ -1157,6 +1173,11 @@ ru:
|
|
|
1157
1173
|
other: "%{count} события"
|
|
1158
1174
|
few: "%{count} события"
|
|
1159
1175
|
many: "%{count} событий"
|
|
1176
|
+
occurrences:
|
|
1177
|
+
one: "%{count} событие"
|
|
1178
|
+
other: "%{count} события"
|
|
1179
|
+
few: "%{count} события"
|
|
1180
|
+
many: "%{count} событий"
|
|
1160
1181
|
queries:
|
|
1161
1182
|
one: "%{count} запрос"
|
|
1162
1183
|
other: "%{count} запроса"
|
|
@@ -1211,6 +1232,7 @@ ru:
|
|
|
1211
1232
|
enable_middleware: Перехватывает необработанные исключения
|
|
1212
1233
|
enable_error_subscriber: Подписывается на события ошибок Rails
|
|
1213
1234
|
retention_days: "Удалять ошибки, не встречавшиеся столько дней. Выполняет RetentionCleanupJob, который гем сам не планирует"
|
|
1235
|
+
occurrence_context_limit: "Сколько последних событий каждой ошибки сохраняют breadcrumbs, состояние системы и переменные (0 = выключено)"
|
|
1214
1236
|
max_backtrace_lines: Ограничение глубины backtrace
|
|
1215
1237
|
sampling_rate: Доля захватываемых ошибок
|
|
1216
1238
|
application_name: Имя этого приложения (определяется автоматически, если не
|
data/config/locales/uk.yml
CHANGED
|
@@ -36,6 +36,9 @@ uk:
|
|
|
36
36
|
healthy: "✅ Справний"
|
|
37
37
|
warning: "⚠️ Попередження"
|
|
38
38
|
critical: "\U0001F534 Критичний"
|
|
39
|
+
not_rated: "Без оцінки"
|
|
40
|
+
not_rated_hint: "Оцінка порівнює платформи, тому потрібні помилки щонайменше на двох."
|
|
41
|
+
nothing_resolved: "Жодну помилку за цей період ще не вирішено"
|
|
39
42
|
device_type:
|
|
40
43
|
mobile: Mobile
|
|
41
44
|
tablet: Tablet
|
|
@@ -248,6 +251,10 @@ uk:
|
|
|
248
251
|
inactive_body: увімкніть, щоб бачити виконані рядки
|
|
249
252
|
enable: Увімкнути
|
|
250
253
|
disable: Вимкнути
|
|
254
|
+
occurrence:
|
|
255
|
+
title: "Подія #%{id}"
|
|
256
|
+
body: "Breadcrumbs, стан системи та змінні належать до цієї події. Усе інше показує останнє захоплення."
|
|
257
|
+
show_latest: "Показати останню"
|
|
251
258
|
baselines:
|
|
252
259
|
title: Статистика базового рівня
|
|
253
260
|
anomaly_label: 'Аномалія:'
|
|
@@ -360,7 +367,7 @@ uk:
|
|
|
360
367
|
types:
|
|
361
368
|
business_hours:
|
|
362
369
|
label: Закономірність робочих годин
|
|
363
|
-
description: Пік помилок припадає на робочі години (9:00–17:
|
|
370
|
+
description: Пік помилок припадає на робочі години (9:00–17:59), що вказує
|
|
364
371
|
на звʼязок з активністю користувачів
|
|
365
372
|
night:
|
|
366
373
|
label: Нічна закономірність
|
|
@@ -520,6 +527,14 @@ uk:
|
|
|
520
527
|
few: "+%{count} ще"
|
|
521
528
|
many: "+%{count} ще"
|
|
522
529
|
view_errors: Переглянути помилки
|
|
530
|
+
captured_occurrences:
|
|
531
|
+
title: "Збережені події"
|
|
532
|
+
hint: "Breadcrumbs, стан системи та змінні зберігаються для останніх подій (occurrence_context_limit). Відкрийте подію, щоб їх побачити."
|
|
533
|
+
column_occurrence: "Подія"
|
|
534
|
+
column_occurred_at: "Час"
|
|
535
|
+
column_version: "Версія"
|
|
536
|
+
view: "Відкрити"
|
|
537
|
+
viewing: "Відкрито"
|
|
523
538
|
co_occurring:
|
|
524
539
|
title: Супутні помилки
|
|
525
540
|
badge: Часові закономірності
|
|
@@ -775,6 +790,7 @@ uk:
|
|
|
775
790
|
many: "%{count} кадрів"
|
|
776
791
|
click_to_expand: "(натисніть, щоб розгорнути)"
|
|
777
792
|
view_source: Переглянути код
|
|
793
|
+
client_report_note: "Ці фрейми надійшли зі звіту клієнта (ManualErrorReporter). Вони описують код, що виконувався на клієнті, тому RED не відкриває для них вихідні файли, не показує фрагменти й не запускає git blame."
|
|
778
794
|
none: Backtrace недоступний
|
|
779
795
|
row:
|
|
780
796
|
new_badge: НОВА
|
|
@@ -1153,6 +1169,11 @@ uk:
|
|
|
1153
1169
|
other: "%{count} події"
|
|
1154
1170
|
few: "%{count} події"
|
|
1155
1171
|
many: "%{count} подій"
|
|
1172
|
+
occurrences:
|
|
1173
|
+
one: "%{count} подія"
|
|
1174
|
+
other: "%{count} події"
|
|
1175
|
+
few: "%{count} події"
|
|
1176
|
+
many: "%{count} подій"
|
|
1156
1177
|
queries:
|
|
1157
1178
|
one: "%{count} запит"
|
|
1158
1179
|
other: "%{count} запита"
|
|
@@ -1207,6 +1228,7 @@ uk:
|
|
|
1207
1228
|
enable_middleware: Перехоплює необроблені винятки
|
|
1208
1229
|
enable_error_subscriber: Підписується на події помилок Rails
|
|
1209
1230
|
retention_days: "Видаляти помилки, яких не було стільки днів. Виконує RetentionCleanupJob, який гем сам не планує"
|
|
1231
|
+
occurrence_context_limit: "Скільки останніх подій кожної помилки зберігають breadcrumbs, стан системи та змінні (0 = вимкнено)"
|
|
1210
1232
|
max_backtrace_lines: Обмеження глибини стеку викликів
|
|
1211
1233
|
sampling_rate: Відсоток захоплених помилок
|
|
1212
1234
|
application_name: Назва цього застосунку (визначається автоматично, якщо не
|
data/config/locales/zh-CN.yml
CHANGED
|
@@ -36,6 +36,9 @@ zh-CN:
|
|
|
36
36
|
healthy: "✅ 正常"
|
|
37
37
|
warning: "⚠️ 警告"
|
|
38
38
|
critical: "\U0001F534 严重"
|
|
39
|
+
not_rated: "未评级"
|
|
40
|
+
not_rated_hint: "评级是在平台之间比较的,因此至少需要两个平台上的错误。"
|
|
41
|
+
nothing_resolved: "此期间的错误均尚未解决"
|
|
39
42
|
device_type:
|
|
40
43
|
mobile: Mobile
|
|
41
44
|
tablet: Tablet
|
|
@@ -231,6 +234,10 @@ zh-CN:
|
|
|
231
234
|
inactive_body: 启用后可查看已执行的行
|
|
232
235
|
enable: 启用
|
|
233
236
|
disable: 停用
|
|
237
|
+
occurrence:
|
|
238
|
+
title: "发生 #%{id}"
|
|
239
|
+
body: "Breadcrumbs、系统状态和变量来自这次发生。其余内容显示最新一次捕获。"
|
|
240
|
+
show_latest: "查看最新"
|
|
234
241
|
baselines:
|
|
235
242
|
title: 基线统计
|
|
236
243
|
anomaly_label: 异常:
|
|
@@ -317,7 +324,7 @@ zh-CN:
|
|
|
317
324
|
types:
|
|
318
325
|
business_hours:
|
|
319
326
|
label: 工作时段规律
|
|
320
|
-
description: 错误高峰出现在工作时段(9:00–17:
|
|
327
|
+
description: 错误高峰出现在工作时段(9:00–17:59),说明与用户活动相关
|
|
321
328
|
night:
|
|
322
329
|
label: 夜间规律
|
|
323
330
|
description: 错误高峰出现在夜间(0:00–6:00),可能来自后台任务或定时作业
|
|
@@ -451,6 +458,14 @@ zh-CN:
|
|
|
451
458
|
more:
|
|
452
459
|
other: 另有 %{count} 项
|
|
453
460
|
view_errors: 查看错误
|
|
461
|
+
captured_occurrences:
|
|
462
|
+
title: "已捕获的发生记录"
|
|
463
|
+
hint: "仅为最近的发生保留 Breadcrumbs、系统状态和变量(occurrence_context_limit)。打开某一条即可查看。"
|
|
464
|
+
column_occurrence: "发生"
|
|
465
|
+
column_occurred_at: "发生时间"
|
|
466
|
+
column_version: "版本"
|
|
467
|
+
view: "查看"
|
|
468
|
+
viewing: "正在查看"
|
|
454
469
|
co_occurring:
|
|
455
470
|
title: 同时出现的错误
|
|
456
471
|
badge: 时间规律
|
|
@@ -679,6 +694,7 @@ zh-CN:
|
|
|
679
694
|
other: "%{count} 个栈帧"
|
|
680
695
|
click_to_expand: "(点击展开)"
|
|
681
696
|
view_source: 查看源码
|
|
697
|
+
client_report_note: "这些帧来自客户端报告(ManualErrorReporter)。它们描述的是在客户端运行的代码,因此 RED 不会为其打开源文件、显示代码片段或运行 git blame。"
|
|
682
698
|
none: 无可用的 backtrace
|
|
683
699
|
row:
|
|
684
700
|
new_badge: 新
|
|
@@ -986,6 +1002,8 @@ zh-CN:
|
|
|
986
1002
|
other: "%{count} 秒"
|
|
987
1003
|
events:
|
|
988
1004
|
other: "%{count} 个事件"
|
|
1005
|
+
occurrences:
|
|
1006
|
+
other: "%{count} 次发生"
|
|
989
1007
|
queries:
|
|
990
1008
|
other: "%{count} 条查询"
|
|
991
1009
|
std_devs: "%{count}σ"
|
|
@@ -1025,6 +1043,7 @@ zh-CN:
|
|
|
1025
1043
|
enable_middleware: 捕获未处理的异常
|
|
1026
1044
|
enable_error_subscriber: 订阅 Rails 错误事件
|
|
1027
1045
|
retention_days: "删除这么多天内未再出现的错误。由 RetentionCleanupJob 执行,gem 本身不会安排它运行"
|
|
1046
|
+
occurrence_context_limit: "每个错误保留 Breadcrumbs、系统状态和变量的最近发生次数(0 = 关闭)"
|
|
1028
1047
|
max_backtrace_lines: 堆栈跟踪深度上限
|
|
1029
1048
|
sampling_rate: 错误捕获的采样比例
|
|
1030
1049
|
application_name: 本应用的名称(未设置时自动检测)
|
|
@@ -3,9 +3,9 @@ class BackfillApplicationForExistingErrors < ActiveRecord::Migration[7.0]
|
|
|
3
3
|
return if RailsErrorDashboard::ErrorLog.count.zero?
|
|
4
4
|
|
|
5
5
|
# Create default application
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# The name new errors are captured under, config.application_name first,
|
|
7
|
+
# so old and new errors land in the same Application
|
|
8
|
+
default_name = RailsErrorDashboard.resolved_application_name
|
|
9
9
|
|
|
10
10
|
app = RailsErrorDashboard::Application.find_or_create_by!(name: default_name) do |a|
|
|
11
11
|
a.description = 'Auto-created during migration for existing errors'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The moment-of-failure context of individual occurrences: breadcrumbs, system
|
|
4
|
+
# health, local and instance variables, as the error row stores them for its
|
|
5
|
+
# latest capture. The error row keeps only the latest; this keeps the last
|
|
6
|
+
# config.occurrence_context_limit occurrences per error (default 25).
|
|
7
|
+
#
|
|
8
|
+
# A side table, not columns on error_occurrences: the co-occurrence queries
|
|
9
|
+
# load occurrence rows whole, and these payloads run to several KB.
|
|
10
|
+
class CreateRailsErrorDashboardErrorOccurrenceContexts < ActiveRecord::Migration[7.0]
|
|
11
|
+
def change
|
|
12
|
+
# Guard against the squashed schema migration having already created this
|
|
13
|
+
# table -- without it, every later migration is silently cancelled.
|
|
14
|
+
return if table_exists?(:rails_error_dashboard_error_occurrence_contexts)
|
|
15
|
+
|
|
16
|
+
create_table :rails_error_dashboard_error_occurrence_contexts do |t|
|
|
17
|
+
t.bigint :error_occurrence_id, null: false
|
|
18
|
+
# Denormalized from the occurrence, so trimming to the newest N per error
|
|
19
|
+
# and retention's delete-by-error need no join
|
|
20
|
+
t.bigint :error_log_id, null: false
|
|
21
|
+
t.text :breadcrumbs
|
|
22
|
+
t.text :system_health
|
|
23
|
+
t.text :local_variables
|
|
24
|
+
t.text :instance_variables
|
|
25
|
+
t.datetime :created_at, null: false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Named explicitly: an auto-generated name would exceed PostgreSQL's
|
|
29
|
+
# 63-character limit and fail during the HOST app's deploy.
|
|
30
|
+
add_index :rails_error_dashboard_error_occurrence_contexts, :error_occurrence_id,
|
|
31
|
+
unique: true, name: "index_red_occurrence_contexts_on_occurrence"
|
|
32
|
+
add_index :rails_error_dashboard_error_occurrence_contexts, :error_log_id,
|
|
33
|
+
name: "index_red_occurrence_contexts_on_error_log"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Where each capture came from: "server" for RED's own captures (middleware,
|
|
4
|
+
# the Rails.error subscriber, jobs, crash import), "client" for errors sent
|
|
5
|
+
# through ManualErrorReporter by a browser, mobile app or other client.
|
|
6
|
+
#
|
|
7
|
+
# error_occurrences.capture_source how this occurrence was captured
|
|
8
|
+
# error_logs.backtrace_source where the backtrace the error page shows
|
|
9
|
+
# came from (the group's backtrace can be
|
|
10
|
+
# replaced by a later capture's)
|
|
11
|
+
#
|
|
12
|
+
# A client's backtrace describes code that ran on the client, so the features
|
|
13
|
+
# that read this app's source files for a frame (View Source, git blame, the
|
|
14
|
+
# snippets in Copy for LLM and AI help) skip it.
|
|
15
|
+
#
|
|
16
|
+
# Nullable, no default: NULL is a row from before this migration and reads as
|
|
17
|
+
# server, which is how those rows have always been treated. No index: nothing
|
|
18
|
+
# queries by it. Not part of the fingerprint: a client report and a server
|
|
19
|
+
# capture of the same error stay one group.
|
|
20
|
+
#
|
|
21
|
+
# up/down rather than `change`: the column_exists? guards that make `up` safe
|
|
22
|
+
# to re-run would make a reversed `change` skip the removal.
|
|
23
|
+
class AddCaptureSourceToErrorOccurrencesAndErrorLogs < ActiveRecord::Migration[7.0]
|
|
24
|
+
COLUMNS = {
|
|
25
|
+
rails_error_dashboard_error_occurrences: :capture_source,
|
|
26
|
+
rails_error_dashboard_error_logs: :backtrace_source
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
def up
|
|
30
|
+
COLUMNS.each do |table, column|
|
|
31
|
+
next unless table_exists?(table)
|
|
32
|
+
next if column_exists?(table, column)
|
|
33
|
+
|
|
34
|
+
add_column table, column, :string, limit: 10
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def down
|
|
39
|
+
COLUMNS.each do |table, column|
|
|
40
|
+
next unless table_exists?(table)
|
|
41
|
+
next unless column_exists?(table, column)
|
|
42
|
+
|
|
43
|
+
remove_column table, column
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -41,11 +41,14 @@ RailsErrorDashboard.configure do |config|
|
|
|
41
41
|
# warden, session, request, params, cookies, redirect_to, etc.
|
|
42
42
|
# Return truthy to allow access, falsy to deny (403 Forbidden).
|
|
43
43
|
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# app's ApplicationController
|
|
47
|
-
#
|
|
48
|
-
#
|
|
44
|
+
# Devise helpers (current_user, user_signed_in?, authenticate_user!) work:
|
|
45
|
+
# Devise adds them to ActionController::Base, which the engine's controller
|
|
46
|
+
# inherits. Methods defined only in your app's ApplicationController do not:
|
|
47
|
+
# the lambda raises and access is denied (logged as "authenticate_with lambda
|
|
48
|
+
# raised").
|
|
49
|
+
#
|
|
50
|
+
# Devise examples:
|
|
51
|
+
# config.authenticate_with = -> { current_user&.admin? }
|
|
49
52
|
# config.authenticate_with = -> { warden.authenticated? }
|
|
50
53
|
#
|
|
51
54
|
# Warden with redirect to login:
|
|
@@ -408,6 +411,12 @@ RailsErrorDashboard.configure do |config|
|
|
|
408
411
|
config.enable_system_health = false
|
|
409
412
|
|
|
410
413
|
<% end -%>
|
|
414
|
+
# Occurrence Context History (NEW in 0.16.0)
|
|
415
|
+
# Each occurrence keeps its own breadcrumbs, system health and variables, so
|
|
416
|
+
# any of an error's recent occurrences can be opened, not only the latest.
|
|
417
|
+
# Kept for the newest N occurrences per error; 0 stops storing them.
|
|
418
|
+
config.occurrence_context_limit = 25
|
|
419
|
+
|
|
411
420
|
<% if @enable_swallowed_exceptions -%>
|
|
412
421
|
# Swallowed Exception Detection - ENABLED
|
|
413
422
|
# Requires Ruby 3.3+ — detects exceptions that are raised then silently rescued
|
|
@@ -23,6 +23,7 @@ module RailsErrorDashboard
|
|
|
23
23
|
|
|
24
24
|
muted_count = 0
|
|
25
25
|
failed_ids = []
|
|
26
|
+
failures = {}
|
|
26
27
|
muted_errors = []
|
|
27
28
|
|
|
28
29
|
errors.each do |error|
|
|
@@ -33,11 +34,18 @@ module RailsErrorDashboard
|
|
|
33
34
|
muted_by: @muted_by,
|
|
34
35
|
muted_reason: @reason
|
|
35
36
|
)
|
|
37
|
+
# The comment a single mute writes, so the reason shows in the thread
|
|
38
|
+
if @reason.present?
|
|
39
|
+
error.comments.create!(author_name: @muted_by || "System", body: "Muted notifications: #{@reason}")
|
|
40
|
+
end
|
|
36
41
|
muted_count += 1
|
|
37
42
|
muted_errors << error
|
|
38
43
|
rescue => e
|
|
39
44
|
failed_ids << error.id
|
|
40
|
-
|
|
45
|
+
failures[error.id] = "#{e.class}: #{e.message}"
|
|
46
|
+
# Rails.logger, not RED's logger: log_level defaults to :silent, and
|
|
47
|
+
# this is the only record of why the row failed
|
|
48
|
+
Rails.logger.warn("[RailsErrorDashboard] Failed to mute error #{error.id}: #{e.class}: #{e.message}")
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
|
|
@@ -50,7 +58,8 @@ module RailsErrorDashboard
|
|
|
50
58
|
count: muted_count,
|
|
51
59
|
total: @error_ids.size,
|
|
52
60
|
failed_ids: failed_ids,
|
|
53
|
-
|
|
61
|
+
failures: failures,
|
|
62
|
+
errors: failed_ids.empty? ? [] : [ "#{red_tp("red.commands.batch_failed.mute", count: failed_ids.size)} (#{failed_ids.map { |id| "##{id}" }.join(", ")})" ]
|
|
54
63
|
}
|
|
55
64
|
rescue => e
|
|
56
65
|
RailsErrorDashboard::Logger.error("Batch mute failed: #{e.message}")
|
|
@@ -24,6 +24,7 @@ module RailsErrorDashboard
|
|
|
24
24
|
|
|
25
25
|
resolved_count = 0
|
|
26
26
|
failed_ids = []
|
|
27
|
+
failures = {}
|
|
27
28
|
|
|
28
29
|
resolved_errors = []
|
|
29
30
|
|
|
@@ -40,7 +41,10 @@ module RailsErrorDashboard
|
|
|
40
41
|
resolved_errors << error
|
|
41
42
|
rescue => e
|
|
42
43
|
failed_ids << error.id
|
|
43
|
-
|
|
44
|
+
failures[error.id] = "#{e.class}: #{e.message}"
|
|
45
|
+
# Rails.logger, not RED's logger: log_level defaults to :silent, and
|
|
46
|
+
# this is the only record of why the row failed
|
|
47
|
+
Rails.logger.warn("[RailsErrorDashboard] Failed to resolve error #{error.id}: #{e.class}: #{e.message}")
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -62,7 +66,8 @@ module RailsErrorDashboard
|
|
|
62
66
|
count: resolved_count,
|
|
63
67
|
total: @error_ids.size,
|
|
64
68
|
failed_ids: failed_ids,
|
|
65
|
-
|
|
69
|
+
failures: failures,
|
|
70
|
+
errors: failed_ids.empty? ? [] : [ "#{red_tp("red.commands.batch_failed.resolve", count: failed_ids.size)} (#{failed_ids.map { |id| "##{id}" }.join(", ")})" ]
|
|
66
71
|
}
|
|
67
72
|
rescue => e
|
|
68
73
|
RailsErrorDashboard::Logger.error("Batch resolve failed: #{e.message}")
|
|
@@ -21,6 +21,7 @@ module RailsErrorDashboard
|
|
|
21
21
|
|
|
22
22
|
unmuted_count = 0
|
|
23
23
|
failed_ids = []
|
|
24
|
+
failures = {}
|
|
24
25
|
unmuted_errors = []
|
|
25
26
|
|
|
26
27
|
errors.each do |error|
|
|
@@ -35,7 +36,10 @@ module RailsErrorDashboard
|
|
|
35
36
|
unmuted_errors << error
|
|
36
37
|
rescue => e
|
|
37
38
|
failed_ids << error.id
|
|
38
|
-
|
|
39
|
+
failures[error.id] = "#{e.class}: #{e.message}"
|
|
40
|
+
# Rails.logger, not RED's logger: log_level defaults to :silent, and
|
|
41
|
+
# this is the only record of why the row failed
|
|
42
|
+
Rails.logger.warn("[RailsErrorDashboard] Failed to unmute error #{error.id}: #{e.class}: #{e.message}")
|
|
39
43
|
end
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -48,7 +52,8 @@ module RailsErrorDashboard
|
|
|
48
52
|
count: unmuted_count,
|
|
49
53
|
total: @error_ids.size,
|
|
50
54
|
failed_ids: failed_ids,
|
|
51
|
-
|
|
55
|
+
failures: failures,
|
|
56
|
+
errors: failed_ids.empty? ? [] : [ "#{red_tp("red.commands.batch_failed.unmute", count: failed_ids.size)} (#{failed_ids.map { |id| "##{id}" }.join(", ")})" ]
|
|
52
57
|
}
|
|
53
58
|
rescue => e
|
|
54
59
|
RailsErrorDashboard::Logger.error("Batch unmute failed: #{e.message}")
|
|
@@ -230,6 +230,12 @@ module RailsErrorDashboard
|
|
|
230
230
|
return {} unless incoming.include?("\n") || incoming.length > stored.length
|
|
231
231
|
|
|
232
232
|
upgrade = { backtrace: @attributes[:backtrace] }
|
|
233
|
+
# The source goes with the backtrace it describes: a client's backtrace
|
|
234
|
+
# replacing a server one must make the row client, and the reverse. A
|
|
235
|
+
# caller that names none is RED's own server-side code.
|
|
236
|
+
if ErrorLog.column_names.include?("backtrace_source")
|
|
237
|
+
upgrade[:backtrace_source] = @attributes[:backtrace_source].presence || "server"
|
|
238
|
+
end
|
|
233
239
|
# The row is no longer a reconstructed exemplar: it now carries a real
|
|
234
240
|
# stack from a real capture, so it must stop describing itself as
|
|
235
241
|
# "minimal".
|
|
@@ -91,10 +91,8 @@ module RailsErrorDashboard
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def current_application_id
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Application.find_by(name: app_name)&.id
|
|
94
|
+
# The name capture files errors under, so these rows join that app
|
|
95
|
+
Application.find_by(name: RailsErrorDashboard.resolved_application_name)&.id
|
|
98
96
|
rescue => e
|
|
99
97
|
nil
|
|
100
98
|
end
|
|
@@ -83,10 +83,8 @@ module RailsErrorDashboard
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def current_application_id
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Application.find_by(name: app_name)&.id
|
|
86
|
+
# The name capture files errors under, so these rows join that app
|
|
87
|
+
Application.find_by(name: RailsErrorDashboard.resolved_application_name)&.id
|
|
90
88
|
rescue => e
|
|
91
89
|
nil
|
|
92
90
|
end
|
|
@@ -391,6 +391,12 @@ module RailsErrorDashboard
|
|
|
391
391
|
if ErrorLog.column_names.include?("context_captured_at")
|
|
392
392
|
create_attrs[:context_captured_at] = create_attrs[:occurred_at]
|
|
393
393
|
end
|
|
394
|
+
# The backtrace above is the exemplar's first frame, so it is a
|
|
395
|
+
# client's when the counted event was a client report. An entry from
|
|
396
|
+
# a release that did not record the source reads as server.
|
|
397
|
+
if ErrorLog.column_names.include?("backtrace_source")
|
|
398
|
+
create_attrs[:backtrace_source] = entry["capture_source"] == "client" ? "client" : "server"
|
|
399
|
+
end
|
|
394
400
|
begin
|
|
395
401
|
# requires_new opens a SAVEPOINT: on PostgreSQL a failed INSERT aborts
|
|
396
402
|
# its transaction, and every later statement -- including the recovery
|
|
@@ -498,12 +504,7 @@ module RailsErrorDashboard
|
|
|
498
504
|
end
|
|
499
505
|
|
|
500
506
|
def resolve_application
|
|
501
|
-
|
|
502
|
-
app_name = RailsErrorDashboard.configuration.application_name ||
|
|
503
|
-
ENV["APPLICATION_NAME"] ||
|
|
504
|
-
(defined?(Rails) && Rails.application.class.module_parent_name) ||
|
|
505
|
-
"Rails Application"
|
|
506
|
-
Application.find_or_create_by_name(app_name)
|
|
507
|
+
Application.find_or_create_by_name(RailsErrorDashboard.resolved_application_name)
|
|
507
508
|
end
|
|
508
509
|
|
|
509
510
|
# Record the interval whose per-event timing was lost.
|
|
@@ -90,10 +90,8 @@ module RailsErrorDashboard
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def current_application_id
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Application.find_by(name: app_name)&.id
|
|
93
|
+
# The name capture files errors under, so these rows join that app
|
|
94
|
+
Application.find_by(name: RailsErrorDashboard.resolved_application_name)&.id
|
|
97
95
|
rescue => e
|
|
98
96
|
nil
|
|
99
97
|
end
|
|
@@ -513,6 +513,13 @@ module RailsErrorDashboard
|
|
|
513
513
|
attributes[:environment] = resolve_environment
|
|
514
514
|
end
|
|
515
515
|
|
|
516
|
+
# Where the backtrace this row is created with came from. Not part of
|
|
517
|
+
# the fingerprint: a client report and a server capture of one error
|
|
518
|
+
# stay one group.
|
|
519
|
+
if ErrorLog.column_names.include?("backtrace_source")
|
|
520
|
+
attributes[:backtrace_source] = capture_source
|
|
521
|
+
end
|
|
522
|
+
|
|
516
523
|
# Neutralise invalid bytes BEFORE filtering: the filter runs regexes,
|
|
517
524
|
# which raise on an invalid string, and PostgreSQL rejects the INSERT.
|
|
518
525
|
attributes = Services::EncodingSanitizer.scrub_deep(attributes)
|
|
@@ -654,8 +661,12 @@ module RailsErrorDashboard
|
|
|
654
661
|
occurrence_columns = ErrorOccurrence.column_names
|
|
655
662
|
occurrence_attrs[:app_version] = attributes[:app_version] if occurrence_columns.include?("app_version")
|
|
656
663
|
occurrence_attrs[:git_sha] = attributes[:git_sha] if occurrence_columns.include?("git_sha")
|
|
664
|
+
occurrence_attrs[:capture_source] = capture_source if occurrence_columns.include?("capture_source")
|
|
657
665
|
occurrence_attrs = Services::EncodingSanitizer.scrub_deep(occurrence_attrs)
|
|
658
|
-
ErrorOccurrence.create(ErrorOccurrence.clamp_string_attributes(occurrence_attrs))
|
|
666
|
+
occurrence = ErrorOccurrence.create(ErrorOccurrence.clamp_string_attributes(occurrence_attrs))
|
|
667
|
+
# This occurrence's own copy of the context the row just got. Never
|
|
668
|
+
# raises, so a failure here is never logged as the occurrence's.
|
|
669
|
+
RecordOccurrenceContext.call(occurrence, attributes) if occurrence.persisted?
|
|
659
670
|
rescue => e
|
|
660
671
|
RailsErrorDashboard::Logger.error("Failed to create error occurrence: #{e.message}")
|
|
661
672
|
end
|
|
@@ -799,12 +810,7 @@ module RailsErrorDashboard
|
|
|
799
810
|
end
|
|
800
811
|
|
|
801
812
|
def find_or_create_application
|
|
802
|
-
|
|
803
|
-
ENV["APPLICATION_NAME"] ||
|
|
804
|
-
(defined?(Rails) && Rails.application.class.module_parent_name) ||
|
|
805
|
-
"Rails Application"
|
|
806
|
-
|
|
807
|
-
Application.find_or_create_by_name(app_name)
|
|
813
|
+
Application.find_or_create_by_name(RailsErrorDashboard.resolved_application_name)
|
|
808
814
|
rescue => e
|
|
809
815
|
RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Failed to find/create application: #{e.message}")
|
|
810
816
|
# Fallback: try to find any application or create default
|
|
@@ -1006,6 +1012,12 @@ module RailsErrorDashboard
|
|
|
1006
1012
|
|
|
1007
1013
|
# Symbol or string key: the async job round-trips the context through the
|
|
1008
1014
|
# queue serializer, which turns symbol keys into strings.
|
|
1015
|
+
# "client" for a ManualErrorReporter report, "server" for every other
|
|
1016
|
+
# capture. Only the exact marker ManualErrorReporter sets counts.
|
|
1017
|
+
def capture_source
|
|
1018
|
+
context_value(:_capture_source) == "client" ? "client" : "server"
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1009
1021
|
def context_value(key)
|
|
1010
1022
|
return nil unless @context.is_a?(Hash)
|
|
1011
1023
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Commands
|
|
5
|
+
# Keeps one occurrence's moment-of-failure context, then trims the error's
|
|
6
|
+
# stored contexts to the newest config.occurrence_context_limit.
|
|
7
|
+
#
|
|
8
|
+
# The values are the JSON strings LogError has just written to the error
|
|
9
|
+
# row (breadcrumbs, system health, local and instance variables), so an
|
|
10
|
+
# occurrence holds exactly what the row would, filtered the same way. A
|
|
11
|
+
# capture with none of them (a storm-protection :lite capture, or every
|
|
12
|
+
# feature off) stores nothing.
|
|
13
|
+
#
|
|
14
|
+
# Runs inside the capture: it never raises.
|
|
15
|
+
class RecordOccurrenceContext
|
|
16
|
+
PIECES = %i[breadcrumbs system_health local_variables instance_variables].freeze
|
|
17
|
+
|
|
18
|
+
# @param occurrence [ErrorOccurrence] the occurrence just created
|
|
19
|
+
# @param attributes [Hash] the attributes LogError wrote to the error row
|
|
20
|
+
def self.call(occurrence, attributes)
|
|
21
|
+
new(occurrence, attributes).call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(occurrence, attributes)
|
|
25
|
+
@occurrence = occurrence
|
|
26
|
+
@attributes = attributes
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call
|
|
30
|
+
limit = RailsErrorDashboard.configuration.occurrence_context_limit.to_i
|
|
31
|
+
return if limit <= 0
|
|
32
|
+
|
|
33
|
+
values = PIECES.index_with { |piece| @attributes[piece].presence }.compact
|
|
34
|
+
return if values.empty?
|
|
35
|
+
|
|
36
|
+
# An app that updated the gem but has not migrated yet
|
|
37
|
+
return unless ErrorOccurrenceContext.table_exists?
|
|
38
|
+
|
|
39
|
+
ErrorOccurrenceContext.create!(
|
|
40
|
+
values.merge(error_occurrence_id: @occurrence.id, error_log_id: @occurrence.error_log_id, created_at: Time.current)
|
|
41
|
+
)
|
|
42
|
+
trim(limit)
|
|
43
|
+
rescue => e
|
|
44
|
+
RailsErrorDashboard::Logger.error(
|
|
45
|
+
"[RailsErrorDashboard] Failed to record occurrence context: #{e.class}: #{e.message}"
|
|
46
|
+
)
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# Everything past the newest `limit`, normally the one the insert pushed out
|
|
53
|
+
def trim(limit)
|
|
54
|
+
stale = ErrorOccurrenceContext.where(error_log_id: @occurrence.error_log_id)
|
|
55
|
+
.order(id: :desc).offset(limit).pluck(:id)
|
|
56
|
+
ErrorOccurrenceContext.where(id: stale).delete_all if stale.any?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|