rails_error_dashboard 0.14.3 → 0.15.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -17
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +28 -4
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +68 -2
  5. data/app/controllers/rails_error_dashboard/health_controller.rb +38 -0
  6. data/app/helpers/rails_error_dashboard/application_helper.rb +10 -2
  7. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +56 -2
  8. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +20 -2
  9. data/app/jobs/rails_error_dashboard/cascade_detection_job.rb +30 -0
  10. data/app/jobs/rails_error_dashboard/concerns/plain_channel_message.rb +33 -6
  11. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +1 -1
  12. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +1 -1
  13. data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +26 -0
  14. data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +6 -1
  15. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +1 -1
  16. data/app/jobs/rails_error_dashboard/telegram_error_notification_job.rb +25 -0
  17. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +2 -18
  18. data/app/models/rails_error_dashboard/error_log.rb +43 -12
  19. data/app/models/rails_error_dashboard/missing_translation.rb +43 -0
  20. data/app/views/layouts/rails_error_dashboard.html.erb +24 -1
  21. data/app/views/rails_error_dashboard/errors/_error_info.html.erb +3 -1
  22. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +3 -1
  23. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +3 -9
  24. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -0
  25. data/app/views/rails_error_dashboard/errors/boot_errors.html.erb +130 -0
  26. data/app/views/rails_error_dashboard/errors/index.html.erb +1 -1
  27. data/app/views/rails_error_dashboard/errors/missing_translations.html.erb +143 -0
  28. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +7 -4
  29. data/app/views/rails_error_dashboard/errors/settings.html.erb +19 -5
  30. data/config/locales/de.yml +72 -5
  31. data/config/locales/en.yml +83 -4
  32. data/config/locales/es.yml +72 -5
  33. data/config/locales/fr.yml +72 -5
  34. data/config/locales/it.yml +72 -5
  35. data/config/locales/ja.yml +72 -4
  36. data/config/locales/pl.yml +72 -5
  37. data/config/locales/pt-BR.yml +72 -5
  38. data/config/locales/ru.yml +72 -5
  39. data/config/locales/uk.yml +72 -5
  40. data/config/locales/zh-CN.yml +72 -4
  41. data/config/routes.rb +6 -0
  42. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +10 -2
  43. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +9 -0
  44. data/db/migrate/20261010000001_create_missing_translations.rb +50 -0
  45. data/lib/generators/rails_error_dashboard/install/install_generator.rb +40 -38
  46. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +31 -3
  47. data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +11 -2
  48. data/lib/rails_error_dashboard/commands/assign_error.rb +24 -1
  49. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +9 -1
  50. data/lib/rails_error_dashboard/commands/flush_missing_translations.rb +103 -0
  51. data/lib/rails_error_dashboard/commands/log_error.rb +6 -3
  52. data/lib/rails_error_dashboard/commands/resolve_error.rb +2 -15
  53. data/lib/rails_error_dashboard/commands/update_error_status.rb +7 -0
  54. data/lib/rails_error_dashboard/configuration.rb +75 -2
  55. data/lib/rails_error_dashboard/engine.rb +52 -5
  56. data/lib/rails_error_dashboard/middleware/rate_limiter.rb +21 -43
  57. data/lib/rails_error_dashboard/plugin.rb +49 -3
  58. data/lib/rails_error_dashboard/plugin_registry.rb +61 -12
  59. data/lib/rails_error_dashboard/queries/boot_errors.rb +76 -0
  60. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +3 -11
  61. data/lib/rails_error_dashboard/queries/errors_list.rb +11 -17
  62. data/lib/rails_error_dashboard/queries/health_status.rb +191 -0
  63. data/lib/rails_error_dashboard/queries/missing_translation_summary.rb +82 -0
  64. data/lib/rails_error_dashboard/services/active_storage_probe.rb +91 -0
  65. data/lib/rails_error_dashboard/services/backtrace_parser.rb +13 -7
  66. data/lib/rails_error_dashboard/services/backtrace_processor.rb +15 -0
  67. data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +30 -0
  68. data/lib/rails_error_dashboard/services/crash_capture.rb +85 -2
  69. data/lib/rails_error_dashboard/services/digest_builder.rb +9 -6
  70. data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +6 -0
  71. data/lib/rails_error_dashboard/services/exception_filter.rb +4 -1
  72. data/lib/rails_error_dashboard/services/github_link_generator.rb +25 -9
  73. data/lib/rails_error_dashboard/services/missing_translation_handler.rb +152 -0
  74. data/lib/rails_error_dashboard/services/missing_translation_tracker.rb +316 -0
  75. data/lib/rails_error_dashboard/services/resolution_callbacks.rb +44 -0
  76. data/lib/rails_error_dashboard/services/severity_classifier.rb +52 -0
  77. data/lib/rails_error_dashboard/services/solid_queue_config_check.rb +47 -0
  78. data/lib/rails_error_dashboard/services/source_code_reader.rb +23 -3
  79. data/lib/rails_error_dashboard/services/telegram_delivery.rb +134 -0
  80. data/lib/rails_error_dashboard/services/telegram_payload_builder.rb +105 -0
  81. data/lib/rails_error_dashboard/services/webhook_delivery.rb +63 -0
  82. data/lib/rails_error_dashboard/services/webhook_signer.rb +80 -0
  83. data/lib/rails_error_dashboard/version.rb +1 -1
  84. data/lib/rails_error_dashboard.rb +12 -0
  85. data/lib/tasks/error_dashboard.rake +43 -2
  86. metadata +21 -2
@@ -60,6 +60,8 @@ zh-CN:
60
60
  llm: LLM
61
61
  n_plus_one: N+1 查询
62
62
  deprecations: Deprecations
63
+ missing_translations: "翻译"
64
+ boot_errors: "启动错误"
63
65
  swallowed: 被吞异常
64
66
  diagnostic_dumps: 诊断
65
67
  storms: 风暴
@@ -85,6 +87,10 @@ zh-CN:
85
87
  body_html: 你正在使用默认凭据(gandalf/youshallnotpass)。请设置环境变量 <code>ERROR_DASHBOARD_USER</code>
86
88
  和 <code>ERROR_DASHBOARD_PASSWORD</code>,或在初始化文件中配置 <code>authenticate_with</code>。
87
89
  dismiss: 本次会话内不再提示
90
+ solid_queue:
91
+ label: "Solid Queue 不会运行 RED 的任务。"
92
+ body_html: "在修复 <code>config/queue.yml</code> 之前,后台捕获的错误和通知会一直在队列中等待。运行 <code>bin/rails error_dashboard:verify</code> 查看修复方法,然后重启应用。"
93
+ guide: "RED 对 Solid Queue 的要求"
88
94
  storm:
89
95
  active_title: 错误风暴进行中
90
96
  recent_title: 近期触发过风暴保护
@@ -749,6 +755,53 @@ zh-CN:
749
755
  column_ratio: 比例
750
756
  view_unrescued: 查看未被捕获、已进入 error_log 的 %{exception} 错误
751
757
  footer_tip: 被吞异常先被抛出,随后被静默捕获,可能掩盖真正的缺陷。
758
+ missing_translations_page:
759
+ title: "缺失的翻译"
760
+ empty_title: "未记录到缺失的翻译"
761
+ empty_body: "最近 %{days} 天内未记录到缺失的翻译键。"
762
+ how_title: "缺失翻译的跟踪方式:"
763
+ how_enabled_html: "必须启用跟踪(%{option})"
764
+ how_hooked: "RED 会包装 I18n.exception_handler,并把调用交给应用原有的处理器;I18n 配置的其他部分不会改变"
765
+ how_recorded: "每一次到达处理器的查找——无论来自 I18n.t 还是视图助手 t()——都会按语言区域和键计数,并记录首次观测到的调用位置"
766
+ how_not_seen: "提供了 :default 的查找永远不会缺失;使用 raise_on_missing_translations 的查找会在任何处理器运行前抛出异常,因此两者都不会在此计数"
767
+ how_buffered: "缺失在内存中计数,并在请求或任务结束时写入,因此显示会有短暂延迟"
768
+ missing_keys: "缺失的键"
769
+ total_misses: "缺失总数"
770
+ locales_affected: "受影响的语言区域"
771
+ all_title: "缺失的翻译键"
772
+ column_key: "键"
773
+ column_locale: "语言区域"
774
+ column_source: "首次出现于"
775
+ column_misses: "缺失次数"
776
+ column_first_seen: "首次出现"
777
+ overflow_notice: "由于跟踪器的缓冲区(每线程 %{limit} 个键)已满,有 %{count} 次缺失无法归到具体的键。"
778
+ footer_tip: "把该键加入语言文件,或传入 :default。用户实际遇到的键排在最前。"
779
+ docs: "Rails i18n 指南"
780
+
781
+ boot_errors_page:
782
+ title: "启动错误"
783
+ empty_title: "未记录到启动错误"
784
+ empty_body: "最近 %{days} 天内没有进程在启动时崩溃。"
785
+ how_title: "启动错误的捕获方式:"
786
+ how_enabled_html: "必须启用崩溃捕获(%{option})"
787
+ how_hooked: "RED 在你的 initializer 运行之后、Rails 预加载之前注册 at_exit 钩子,因此即使应用从未完成启动,启动期间的崩溃也会被捕获"
788
+ how_captured_html: "文件定义了错误常量导致的 %{zeitwerk}、模型中的 %{syntax}、抛出异常的 initializer:任何在处理请求之前就终止进程的问题"
789
+ how_imported: "此时还没有数据库,所以崩溃先写入磁盘,并在下一次成功启动时导入"
790
+ how_never: "被 SIGKILL 或段错误终止的进程,任何工具的 at_exit 都看不到"
791
+ unresolved: "未解决"
792
+ boot_crashes: "启动崩溃"
793
+ failed_boots: "失败的启动"
794
+ all_title: "按错误分组的启动崩溃"
795
+ column_error: "错误"
796
+ column_file: "文件"
797
+ column_constant: "常量"
798
+ column_boots: "启动次数"
799
+ column_first_seen: "首次出现"
800
+ column_status: "状态"
801
+ status_open: "未处理"
802
+ status_resolved: "已解决"
803
+ footer_tip: "启动崩溃意味着进程从未处理过请求。请在下次部署前修复;该行会像其他错误一样被标记为已解决。"
804
+
752
805
  rack_attack_page:
753
806
  title: 限流事件
754
807
  missing_title: 尚未安装 Rack Attack
@@ -963,7 +1016,7 @@ zh-CN:
963
1016
  unable_to_detect: 无法检测
964
1017
  no_user_model: 未检测到 User 模型
965
1018
  unable_to_query_count: 无法查询数量
966
- manual_cleanup_required: 需要手动清理
1019
+ retention_enforced_by_job: "由 RetentionCleanupJob 在您的调度器运行它时删除"
967
1020
  keep_forever: 永久保留
968
1021
  no_automatic_deletion: 不自动删除
969
1022
  run_command: 执行:
@@ -971,7 +1024,7 @@ zh-CN:
971
1024
  options:
972
1025
  enable_middleware: 捕获未处理的异常
973
1026
  enable_error_subscriber: 订阅 Rails 错误事件
974
- retention_days: 仅手动清理(执行:rails error_dashboard:cleanup_resolved DAYS=90)
1027
+ retention_days: "删除这么多天内未再出现的错误。由 RetentionCleanupJob 执行,gem 本身不会安排它运行"
975
1028
  max_backtrace_lines: 堆栈跟踪深度上限
976
1029
  sampling_rate: 错误捕获的采样比例
977
1030
  application_name: 本应用的名称(未设置时自动检测)
@@ -985,7 +1038,7 @@ zh-CN:
985
1038
  total_users_for_impact: 用于计算影响百分比(未设置时自动检测)
986
1039
  async_logging: 在后台任务中处理
987
1040
  async_adapter: 任务队列适配器
988
- enable_rate_limiting: 对 API 请求限流
1041
+ enable_rate_limiting: "按 IP 和路径限制仪表盘请求"
989
1042
  rate_limit_per_minute: 每个 IP 每分钟的请求数
990
1043
  enable_slack_notifications: 实时通知
991
1044
  slack_webhook_url: Slack webhook 地址
@@ -994,10 +1047,15 @@ zh-CN:
994
1047
  notification_email_from: 发件人地址
995
1048
  enable_discord_notifications: 通过 Discord webhook 通知
996
1049
  discord_webhook_url: Discord webhook 地址
1050
+ enable_telegram_notifications: "Telegram Bot API 通知"
1051
+ telegram_bot_token: "来自 @BotFather 的机器人令牌(设置环境变量 TELEGRAM_BOT_TOKEN)"
1052
+ telegram_chat_id: "机器人发送消息的聊天、群组或频道"
1053
+ telegram_api_base_url: "Bot API 服务器(仅在使用代理或自建服务器时更改)"
997
1054
  enable_pagerduty_notifications: 严重错误升级
998
1055
  pagerduty_integration_key: PagerDuty API 密钥
999
1056
  enable_webhook_notifications: 通用 webhook 地址
1000
1057
  webhook_urls: 自定义 webhook 地址
1058
+ webhook_signing_secret: 使用 HMAC-SHA256 签名发出的 webhook(设置环境变量 WEBHOOK_SIGNING_SECRET)
1001
1059
  enable_scheduled_digests: 每日/每周摘要邮件。可通过 SolidQueue、Sidekiq 或 cron 调度
1002
1060
  digest_frequency: 摘要邮件的发送频率
1003
1061
  digest_recipients: 邮件地址(默认使用通知收件人)
@@ -1015,7 +1073,7 @@ zh-CN:
1015
1073
  source_code_context_lines: 目标行前后显示的行数
1016
1074
  enable_git_blame: 显示 git blame 信息
1017
1075
  source_code_cache_ttl: 源代码缓存时长
1018
- only_show_app_code_source: 出于安全考虑隐藏 gems/stdlib
1076
+ only_show_app_code_source: "在源码查看器中隐藏 gem 和 vendor 代码"
1019
1077
  git_branch_strategy: 分支解析策略
1020
1078
  enable_breadcrumbs: 记录请求的活动轨迹
1021
1079
  breadcrumb_buffer_size: 每个请求最多记录的 breadcrumbs 数
@@ -1039,6 +1097,7 @@ zh-CN:
1039
1097
  enable_diagnostic_dump: 按需生成系统状态快照
1040
1098
  enable_crash_capture: 用 at_exit 钩子捕获未处理的崩溃
1041
1099
  enable_coverage_tracking: 诊断模式 — 在面板中启用后,可在源码视图中看到已执行的行(Ruby 3.2+)
1100
+ enable_missing_translation_tracking: "按语言区域和键统计应用的 I18n 缺失(包装 I18n.exception_handler)"
1042
1101
  enable_rack_attack_tracking: 将 throttle/blocklist 事件记录到独立数据表
1043
1102
  enable_actioncable_tracking: 将 WebSocket 频道事件记录为 breadcrumbs
1044
1103
  enable_activestorage_tracking: 将上传/下载/删除记录为 breadcrumbs
@@ -1084,6 +1143,8 @@ zh-CN:
1084
1143
  breadcrumbs: Breadcrumbs
1085
1144
  system_health: 系统状态
1086
1145
  swallowed_exceptions: 被吞异常检测
1146
+ missing_translations: "缺失翻译跟踪"
1147
+ boot_errors: "启动错误捕获"
1087
1148
  rack_attack: Rack Attack 跟踪
1088
1149
  actioncable: ActionCable 跟踪
1089
1150
  activestorage: ActiveStorage 跟踪
@@ -1446,6 +1507,13 @@ zh-CN:
1446
1507
  guide_link: ActionCable 指南
1447
1508
  activestorage:
1448
1509
  title: ActiveStorage 状态
1510
+ probe:
1511
+ title: "服务可达性"
1512
+ reachable: "可达"
1513
+ unreachable: "不可达"
1514
+ service: "服务:%{name}"
1515
+ latency: "往返 %{ms} ms"
1516
+ how: "加载本页时对一个永远不会存储的键做一次存在性检查。GET /health 执行同样的探测。"
1449
1517
  empty:
1450
1518
  title: 未找到 ActiveStorage 事件
1451
1519
  message: 最近 %{days} 天的错误 breadcrumbs 中未检测到 ActiveStorage 的服务操作。
data/config/routes.rb CHANGED
@@ -7,6 +7,10 @@ RailsErrorDashboard::Engine.routes.draw do
7
7
  # Settings page
8
8
  get "settings", to: "errors#settings", as: :settings
9
9
 
10
+ # Health check (JSON): error database, queue, storm breaker, gem version.
11
+ # 200 when the dashboard can do its job, 503 when the error DB is unreachable.
12
+ get "health", to: "health#show", as: :health
13
+
10
14
  # Per-user dashboard language, persisted in the session (P5-T1).
11
15
  # POST only: changing it is a state change, and a GET would let any link or
12
16
  # prefetch alter the user's language.
@@ -41,6 +45,8 @@ RailsErrorDashboard::Engine.routes.draw do
41
45
  get :database_health_summary
42
46
  get :swallowed_exceptions
43
47
  get :rack_attack_summary
48
+ get :missing_translations
49
+ get :boot_errors
44
50
  get :actioncable_health_summary
45
51
  get :activestorage_health_summary
46
52
  get :llm_health_summary
@@ -26,7 +26,7 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
26
26
  t.string :error_type, null: false
27
27
  t.text :message, null: false
28
28
  t.text :backtrace
29
- t.integer :user_id
29
+ t.column :user_id, user_id_type
30
30
  t.text :request_url
31
31
  t.text :request_params
32
32
  t.text :user_agent
@@ -157,7 +157,7 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
157
157
  create_table :rails_error_dashboard_error_occurrences do |t|
158
158
  t.bigint :error_log_id, null: false
159
159
  t.datetime :occurred_at, null: false
160
- t.integer :user_id
160
+ t.column :user_id, user_id_type
161
161
  t.string :request_id
162
162
  t.string :session_id
163
163
  t.timestamps
@@ -275,4 +275,12 @@ class CreateRailsErrorDashboardCompleteSchema < ActiveRecord::Migration[7.0]
275
275
  drop_table :rails_error_dashboard_error_logs, if_exists: true
276
276
  drop_table :rails_error_dashboard_applications, if_exists: true
277
277
  end
278
+
279
+ private
280
+
281
+ # Same type as the app's primary keys, as ActiveStorage does for record_id.
282
+ def user_id_type
283
+ config = Rails.configuration.generators
284
+ config.options[config.orm][:primary_key_type] || :integer
285
+ end
278
286
  end
@@ -3,6 +3,15 @@ class RemoveEnvironmentFromErrorLogs < ActiveRecord::Migration[7.0]
3
3
  # Skip if squashed migration ran (column never existed) or already removed
4
4
  return unless column_exists?(:rails_error_dashboard_error_logs, :environment)
5
5
 
6
+ # Only the v0.1.x column is removed: it was NOT NULL. The column
7
+ # AddEnvironmentToErrorLogs adds back (0.11.0) is nullable and holds real
8
+ # data. This migration replays over it whenever RED's migrations run
9
+ # against tables that already exist (an app joining a shared error
10
+ # database runs its own renumbered copy of every migration), and dropping
11
+ # it there wiped every error's environment.
12
+ environment = columns(:rails_error_dashboard_error_logs).find { |column| column.name == "environment" }
13
+ return if environment.nil? || environment.null
14
+
6
15
  # Remove composite index first
7
16
  remove_index :rails_error_dashboard_error_logs,
8
17
  name: 'index_error_logs_on_environment_and_occurred_at',
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One row per (locale, key, application) the host app failed to translate.
4
+ #
5
+ # A missing translation is not an error: Rails renders "translation missing"
6
+ # (or the humanized key) and the request succeeds, so it never reaches
7
+ # error_logs and nothing in the dashboard would otherwise show it. The
8
+ # tracker counts misses in memory and upserts them here at the end of the
9
+ # request or job that produced them -- a loop rendering a missing key a
10
+ # thousand times is one UPDATE, not a thousand INSERTs.
11
+ class CreateMissingTranslations < ActiveRecord::Migration[7.0]
12
+ def change
13
+ # Guard against the squashed schema migration having already created this
14
+ # table -- without it, every later migration is silently cancelled.
15
+ return if table_exists?(:rails_error_dashboard_missing_translations)
16
+
17
+ create_table :rails_error_dashboard_missing_translations do |t|
18
+ # BCP 47 tags ("pt-BR", "zh-Hans-CN", "en-US-x-twain") fit in 35.
19
+ t.string :locale, null: false, limit: 35
20
+ # The full dotted key as I18n normalised it ("users.show.greeting").
21
+ # Capped at 191 so the unique upsert index stays inside MySQL's
22
+ # 3072-byte utf8mb4 limit: (35 + 191) * 4 + application_id. The tracker
23
+ # truncates before the value reaches the index.
24
+ t.string :translation_key, null: false, limit: 191
25
+ # First host call site seen for this key ("app/views/users/show.html.erb:12").
26
+ # First-write-wins; not part of the row's identity.
27
+ t.string :source, limit: 250
28
+ t.bigint :miss_count, null: false, default: 0
29
+ t.datetime :first_seen_at, null: false
30
+ t.datetime :last_seen_at, null: false
31
+ # Nullable: a miss recorded before application scoping, or with no
32
+ # application_name configured, applies to the whole install.
33
+ t.bigint :application_id
34
+ t.timestamps
35
+ end
36
+
37
+ # Named explicitly: an auto-generated name would exceed PostgreSQL's
38
+ # 63-character limit and fail during the HOST app's deploy.
39
+ add_index :rails_error_dashboard_missing_translations,
40
+ [ :locale, :translation_key, :application_id ],
41
+ unique: true,
42
+ name: "index_red_missing_translations_upsert_key"
43
+
44
+ # The dashboard reads "misses seen in the last N days" and retention
45
+ # prunes by the same column.
46
+ add_index :rails_error_dashboard_missing_translations,
47
+ :last_seen_at,
48
+ name: "index_red_missing_translations_on_last_seen_at"
49
+ end
50
+ end
@@ -12,6 +12,7 @@ module RailsErrorDashboard
12
12
  class_option :slack, type: :boolean, default: false, desc: "Enable Slack notifications"
13
13
  class_option :email, type: :boolean, default: false, desc: "Enable email notifications"
14
14
  class_option :discord, type: :boolean, default: false, desc: "Enable Discord notifications"
15
+ class_option :telegram, type: :boolean, default: false, desc: "Enable Telegram notifications"
15
16
  class_option :pagerduty, type: :boolean, default: false, desc: "Enable PagerDuty notifications"
16
17
  class_option :webhooks, type: :boolean, default: false, desc: "Enable webhook notifications"
17
18
  # Performance options
@@ -58,7 +59,7 @@ module RailsErrorDashboard
58
59
 
59
60
  if quick_mode?
60
61
  @selected_features = build_quick_defaults
61
- say " Using sensible defaults (analytics ON, notifications OFF, breadcrumbs OFF)", :green
62
+ say " Using sensible defaults (analytics, breadcrumbs ON, system health ON, 50% sampling; notifications OFF)", :green
62
63
  return
63
64
  end
64
65
 
@@ -71,13 +72,13 @@ module RailsErrorDashboard
71
72
  @selected_features = {}
72
73
 
73
74
  # =====================================================================
74
- # QUESTION 1: Notifications (gated — one y/N opens 5 sub-questions)
75
+ # QUESTION 1: Notifications (gated — one y/N opens 6 sub-questions)
75
76
  # =====================================================================
76
- notification_keys = %i[slack email discord pagerduty webhooks]
77
+ notification_keys = %i[slack email discord telegram pagerduty webhooks]
77
78
  any_notification_cli_flag = notification_keys.any? { |k| options[k] }
78
79
 
79
80
  say "[1/3] Notifications [background/dashboard only — zero request overhead]", :cyan
80
- say " Alert your team via Slack, email, Discord, PagerDuty, or webhooks.", :white
81
+ say " Alert your team via Slack, email, Discord, Telegram, PagerDuty, or webhooks.", :white
81
82
 
82
83
  if any_notification_cli_flag
83
84
  # Individual CLI flags passed — respect them, skip the gate prompt
@@ -90,6 +91,7 @@ module RailsErrorDashboard
90
91
  { key: :slack, name: "Slack", hint: "SLACK_WEBHOOK_URL" },
91
92
  { key: :email, name: "Email", hint: "ERROR_NOTIFICATION_EMAILS" },
92
93
  { key: :discord, name: "Discord", hint: "DISCORD_WEBHOOK_URL" },
94
+ { key: :telegram, name: "Telegram", hint: "TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID" },
93
95
  { key: :pagerduty, name: "PagerDuty", hint: "PAGERDUTY_INTEGRATION_KEY" },
94
96
  { key: :webhooks, name: "Webhooks", hint: "WEBHOOK_URLS" }
95
97
  ]
@@ -131,7 +133,10 @@ module RailsErrorDashboard
131
133
  advanced_keys = %i[async_logging error_sampling breadcrumbs system_health
132
134
  source_code_integration git_blame swallowed_exceptions
133
135
  crash_capture diagnostic_dump]
134
- any_advanced_cli_flag = advanced_keys.any? { |k| options[k] }
136
+ # A flag the user passed, not an option left at its default: async_logging
137
+ # defaults to true, so "is any of these set?" was always yes and this
138
+ # question was never asked.
139
+ any_advanced_cli_flag = advanced_keys.any? { |k| options[k] != self.class.class_options[k]&.default }
135
140
 
136
141
  say "\n[3/3] Advanced Options (performance tuning & diagnostics)", :cyan
137
142
  say " Async logging, error sampling, breadcrumbs, system health,", :white
@@ -169,8 +174,10 @@ module RailsErrorDashboard
169
174
  advanced_features.each do |f|
170
175
  say "\n #{f[:name]}", :cyan
171
176
  say " #{f[:desc]}", :white
172
- r = ask(" Enable? (y/N):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
173
- @selected_features[f[:key]] = r.downcase == "y"
177
+ # Async logging is on unless declined, as it is without this question.
178
+ default_on = f[:key] == :async_logging
179
+ r = ask(" Enable? (#{default_on ? 'Y/n' : 'y/N'}):", :yellow, limited_to: [ "y", "Y", "n", "N", "" ])
180
+ @selected_features[f[:key]] = default_on ? r.downcase != "n" : r.downcase == "y"
174
181
  if @selected_features[f[:key]] && f[:key] == :swallowed_exceptions && RUBY_VERSION < "3.3"
175
182
  say " ⚠ Requires Ruby 3.3+ (you have #{RUBY_VERSION}) — will activate after upgrade", :yellow
176
183
  end
@@ -289,11 +296,14 @@ module RailsErrorDashboard
289
296
  @enable_slack = @selected_features&.dig(:slack) || options[:slack]
290
297
  @enable_email = @selected_features&.dig(:email) || options[:email]
291
298
  @enable_discord = @selected_features&.dig(:discord) || options[:discord]
299
+ @enable_telegram = @selected_features&.dig(:telegram) || options[:telegram]
292
300
  @enable_pagerduty = @selected_features&.dig(:pagerduty) || options[:pagerduty]
293
301
  @enable_webhooks = @selected_features&.dig(:webhooks) || options[:webhooks]
294
302
 
295
303
  # Performance
296
- @enable_async_logging = @selected_features&.dig(:async_logging) || options[:async_logging]
304
+ # An answer, when there was one, wins: `answer || default` turned a "no"
305
+ # into the default's "yes".
306
+ @enable_async_logging = @selected_features&.key?(:async_logging) ? @selected_features[:async_logging] : options[:async_logging]
297
307
  @enable_error_sampling = @selected_features&.dig(:error_sampling) || options[:error_sampling]
298
308
 
299
309
  # Database mode (set by select_database_mode or CLI flags)
@@ -410,6 +420,7 @@ module RailsErrorDashboard
410
420
  notification_features << "Slack" if @enable_slack
411
421
  notification_features << "Email" if @enable_email
412
422
  notification_features << "Discord" if @enable_discord
423
+ notification_features << "Telegram" if @enable_telegram
413
424
  notification_features << "PagerDuty" if @enable_pagerduty
414
425
  notification_features << "Webhooks" if @enable_webhooks
415
426
 
@@ -474,6 +485,7 @@ module RailsErrorDashboard
474
485
  say " → Set SLACK_WEBHOOK_URL in .env", :yellow if @enable_slack
475
486
  say " → Set ERROR_NOTIFICATION_EMAILS in .env", :yellow if @enable_email
476
487
  say " → Set DISCORD_WEBHOOK_URL in .env", :yellow if @enable_discord
488
+ say " → Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env", :yellow if @enable_telegram
477
489
  say " → Set PAGERDUTY_INTEGRATION_KEY in .env", :yellow if @enable_pagerduty
478
490
  say " → Set WEBHOOK_URLS in .env", :yellow if @enable_webhooks
479
491
  if @enable_async_logging
@@ -493,12 +505,12 @@ module RailsErrorDashboard
493
505
  say "Next Steps:", :cyan
494
506
  if @enable_separate_database
495
507
  say " 1. Add the database.yml entry shown above"
496
- say " 2. Run: rails db:create:error_dashboard"
508
+ say " 2. Run: rails db:create:#{@database_name}"
497
509
  if @enable_multi_app
498
510
  say " 3. Run migrations (only needed on the FIRST app):"
499
- say " rails db:migrate:error_dashboard"
511
+ say " rails db:migrate:#{@database_name}"
500
512
  else
501
- say " 3. Run: rails db:migrate:error_dashboard"
513
+ say " 3. Run: rails db:migrate:#{@database_name}"
502
514
  end
503
515
  say " 4. Before deploying: set ERROR_DASHBOARD_USER and ERROR_DASHBOARD_PASSWORD there"
504
516
  say " 5. Restart your Rails server"
@@ -562,7 +574,7 @@ module RailsErrorDashboard
562
574
  system_health: true, # ~1ms per error — GC, memory, threads at exact error moment
563
575
  error_sampling: true, # 50% sampling on non-critical errors — halves storage, still shows patterns
564
576
  # OFF — require credentials, config, or niche use case
565
- slack: false, email: false, discord: false, pagerduty: false, webhooks: false,
577
+ slack: false, email: false, discord: false, telegram: false, pagerduty: false, webhooks: false,
566
578
  source_code_integration: false,
567
579
  git_blame: false,
568
580
  swallowed_exceptions: false,
@@ -595,33 +607,23 @@ module RailsErrorDashboard
595
607
  say " # Separate error database", :white
596
608
  end
597
609
 
598
- say "\n development:", :cyan
599
- say " primary:", :white
600
- say " <<: *default", :white
601
- say " database: #{app_name_snake}_development", :white
602
- say " error_dashboard:", :white
603
- say " <<: *default", :white
604
- if @enable_multi_app
605
- shared_db_base = @shared_db_name || "shared_errors"
606
- say " database: #{shared_db_base}_development", :white
607
- else
608
- say " database: #{app_name_snake}_errors_development", :white
609
- end
610
- say " migrations_paths: db/error_dashboard_migrate", :white
611
-
612
- say "\n production:", :cyan
613
- say " primary:", :white
614
- say " <<: *default", :white
615
- say " database: #{app_name_snake}_production", :white
616
- say " error_dashboard:", :white
617
- say " <<: *default", :white
618
- if @enable_multi_app
619
- shared_db_base = @shared_db_name || "shared_errors"
620
- say " database: #{shared_db_base}_production", :white
621
- else
622
- say " database: #{app_name_snake}_errors_production", :white
610
+ # Every environment the app boots in needs the entry, test included:
611
+ # without it RED falls back to the main database and records nothing.
612
+ %w[development test production].each do |env|
613
+ say "\n #{env}:", :cyan
614
+ say " primary:", :white
615
+ say " <<: *default", :white
616
+ say " database: #{app_name_snake}_#{env}", :white
617
+ say " #{@database_name}:", :white
618
+ say " <<: *default", :white
619
+ if @enable_multi_app
620
+ shared_db_base = @shared_db_name || "shared_errors"
621
+ say " database: #{shared_db_base}_#{env}", :white
622
+ else
623
+ say " database: #{app_name_snake}_errors_#{env}", :white
624
+ end
625
+ say " migrations_paths: db/error_dashboard_migrate", :white
623
626
  end
624
- say " migrations_paths: db/error_dashboard_migrate", :white
625
627
  say "\n"
626
628
 
627
629
  if @enable_multi_app
@@ -127,6 +127,21 @@ RailsErrorDashboard.configure do |config|
127
127
  config.enable_discord_notifications = false
128
128
  # config.discord_webhook_url = ENV["DISCORD_WEBHOOK_URL"]
129
129
 
130
+ <% end -%>
131
+ <% if @enable_telegram -%>
132
+ # Telegram Notifications - ENABLED
133
+ config.enable_telegram_notifications = true
134
+ config.telegram_bot_token = ENV["TELEGRAM_BOT_TOKEN"]
135
+ config.telegram_chat_id = ENV["TELEGRAM_CHAT_ID"]
136
+ # To disable: Set config.enable_telegram_notifications = false
137
+
138
+ <% else -%>
139
+ # Telegram Notifications - DISABLED
140
+ # To enable: Set config.enable_telegram_notifications = true and configure a bot token and chat id
141
+ config.enable_telegram_notifications = false
142
+ # config.telegram_bot_token = ENV["TELEGRAM_BOT_TOKEN"]
143
+ # config.telegram_chat_id = ENV["TELEGRAM_CHAT_ID"]
144
+
130
145
  <% end -%>
131
146
  <% if @enable_pagerduty -%>
132
147
  # PagerDuty Integration - ENABLED (critical errors only)
@@ -145,6 +160,9 @@ RailsErrorDashboard.configure do |config|
145
160
  # Generic Webhook Notifications - ENABLED
146
161
  config.enable_webhook_notifications = true
147
162
  config.webhook_urls = ENV.fetch("WEBHOOK_URLS", "").split(",").map(&:strip).reject(&:empty?)
163
+ # Sign every webhook (HMAC-SHA256 over timestamp + body) so receivers can
164
+ # verify it came from this app. Generate with: ruby -rsecurerandom -e 'puts SecureRandom.hex(32)'
165
+ # config.webhook_signing_secret = ENV["WEBHOOK_SIGNING_SECRET"]
148
166
  # To disable: Set config.enable_webhook_notifications = false
149
167
 
150
168
  <% else -%>
@@ -152,6 +170,7 @@ RailsErrorDashboard.configure do |config|
152
170
  # To enable: Set config.enable_webhook_notifications = true and configure webhook URLs
153
171
  config.enable_webhook_notifications = false
154
172
  # config.webhook_urls = ENV.fetch("WEBHOOK_URLS", "").split(",").map(&:strip).reject(&:empty?)
173
+ # config.webhook_signing_secret = ENV["WEBHOOK_SIGNING_SECRET"] # HMAC-SHA256 signature headers
155
174
 
156
175
  <% end -%>
157
176
  # Dashboard base URL (used in notification links)
@@ -186,7 +205,8 @@ RailsErrorDashboard.configure do |config|
186
205
  <% if @enable_error_sampling -%>
187
206
  # Error Sampling - ENABLED
188
207
  # Samples non-critical errors to reduce storage volume.
189
- # Critical and high severity errors are ALWAYS logged at 100% regardless of this setting.
208
+ # Critical errors are always logged, regardless of this setting. So is the first
209
+ # occurrence of each error in each process.
190
210
  # 0.5 = log 50% of non-critical occurrences — halves storage while keeping
191
211
  # occurrence counts meaningful and error patterns visible.
192
212
  # Tune lower (e.g. 0.1) if one error is flooding the DB; set to 1.0 to log everything.
@@ -244,7 +264,7 @@ RailsErrorDashboard.configure do |config|
244
264
  # Automatically detect when error rates exceed normal patterns
245
265
  config.enable_baseline_alerts = true
246
266
  config.baseline_alert_threshold_std_devs = 2.0 # Alert when > 2 std devs above baseline
247
- config.baseline_alert_severities = [ :critical, :high ]
267
+ config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
248
268
  config.baseline_alert_cooldown_minutes = 120 # 2 hours between alerts
249
269
  # To disable: Set config.enable_baseline_alerts = false
250
270
 
@@ -253,7 +273,7 @@ RailsErrorDashboard.configure do |config|
253
273
  # To enable: Set config.enable_baseline_alerts = true
254
274
  config.enable_baseline_alerts = false
255
275
  # config.baseline_alert_threshold_std_devs = 2.0
256
- # config.baseline_alert_severities = [ :critical, :high ]
276
+ # config.baseline_alert_severities = [ :critical, :high ] # Only errors of these severities alert
257
277
  # config.baseline_alert_cooldown_minutes = 120
258
278
 
259
279
  <% end -%>
@@ -407,6 +427,14 @@ RailsErrorDashboard.configure do |config|
407
427
  # config.swallowed_exception_threshold = 0.95
408
428
 
409
429
  <% end -%>
430
+ # Missing-Translation Tracking - DISABLED
431
+ # Counts every I18n key your app looked up and did not have, by locale, with
432
+ # the first call site, on its own dashboard page (/errors/missing_translations).
433
+ # Wraps I18n.exception_handler and delegates to the handler you already had —
434
+ # the one place RED touches global I18n state, which is why it is opt-in.
435
+ # To enable: Set config.enable_missing_translation_tracking = true
436
+ config.enable_missing_translation_tracking = false
437
+
410
438
  <% if @enable_diagnostic_dump -%>
411
439
  # Diagnostic Dump - ENABLED
412
440
  # On-demand system state snapshot via rake task or dashboard button
@@ -236,13 +236,22 @@ module RailsErrorDashboard
236
236
  say "Remaining manual steps:", :cyan
237
237
  say "\n"
238
238
 
239
- say "1. Remove from Gemfile:", :yellow
239
+ unless options[:keep_data]
240
+ say "1. Regenerate the schema file:", :yellow
241
+ say " Run: bin/rails db:schema:dump (otherwise db:schema:load recreates the tables)"
242
+ say "\n"
243
+
244
+ say "2. Separate database only: remove its entry from config/database.yml", :yellow
245
+ say "\n"
246
+ end
247
+
248
+ say "#{options[:keep_data] ? 1 : 3}. Remove from Gemfile:", :yellow
240
249
  say " Open: Gemfile"
241
250
  say " Remove: gem 'rails_error_dashboard'"
242
251
  say " Run: bundle install"
243
252
  say "\n"
244
253
 
245
- say "2. Restart your application:", :yellow
254
+ say "#{options[:keep_data] ? 2 : 4}. Restart your application:", :yellow
246
255
  say " Run: rails restart"
247
256
  say " Or: kill and restart your server process"
248
257
  say "\n"
@@ -29,10 +29,33 @@ module RailsErrorDashboard
29
29
  error.update!(
30
30
  assigned_to: assignee.truncate(MAX_ASSIGNEE_LENGTH, omission: ""),
31
31
  assigned_at: Time.current,
32
- status: "in_progress" # Auto-transition to in_progress when assigned
32
+ **status_attributes(error)
33
33
  )
34
+ # The stat cards are cached; a reopened error must show up at once.
35
+ Services::AnalyticsCacheManager.clear if error.saved_change_to_resolved?
36
+
34
37
  { success: true, error: error }
35
38
  end
39
+
40
+ private
41
+
42
+ # Assigning means someone is working on it, so the error moves to
43
+ # in_progress. Doing that to a resolved error used to leave
44
+ # resolved: true and resolved_at behind: a row that was in_progress to
45
+ # the eye, resolved to the scopes, and matched by neither the unresolved
46
+ # nor the resolved lookup, so its next recurrence opened a new row.
47
+ # Assigning a resolved error reopens it. wont_fix is sticky (H-6): the
48
+ # assignee is recorded and the status stays.
49
+ def status_attributes(error)
50
+ return {} if error.status == "wont_fix"
51
+
52
+ attrs = { status: "in_progress" }
53
+ if error.resolved? || error.status == "resolved"
54
+ attrs[:resolved] = false
55
+ attrs[:resolved_at] = nil
56
+ end
57
+ attrs
58
+ end
36
59
  end
37
60
  end
38
61
  end
@@ -44,11 +44,19 @@ module RailsErrorDashboard
44
44
  end
45
45
  end
46
46
 
47
- # Dispatch plugin event for batch resolved errors
48
47
  Services::AnalyticsCacheManager.clear if resolved_count.positive?
49
48
 
49
+ # Dispatch plugin event for batch resolved errors
50
50
  PluginRegistry.dispatch(:on_errors_batch_resolved, resolved_errors) if resolved_errors.any?
51
51
 
52
+ # Each error resolved here is as resolved as one resolved on its own
53
+ # page: the host's error_resolved callbacks (the issue tracker closes
54
+ # the linked issue from one) and the instrumentation event fire per
55
+ # error. Only the plugin event is batch-shaped, by design.
56
+ resolved_errors.each do |error|
57
+ Services::ResolutionCallbacks.call(error, resolved_by: @resolved_by_name)
58
+ end
59
+
52
60
  {
53
61
  success: failed_ids.empty?,
54
62
  count: resolved_count,