rails_error_dashboard 0.8.3 → 0.9.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/README.md +35 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +156 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +95 -73
- data/app/controllers/rails_error_dashboard/locales_controller.rb +87 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +20 -14
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +15 -6
- data/app/helpers/rails_error_dashboard/i18n_helper.rb +154 -0
- data/app/helpers/rails_error_dashboard/mailer_i18n_helper.rb +69 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +18 -6
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +25 -11
- data/app/jobs/rails_error_dashboard/application_job.rb +5 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +18 -16
- data/app/jobs/rails_error_dashboard/concerns/localized_job.rb +83 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +4 -2
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +6 -2
- data/app/jobs/rails_error_dashboard/scheduled_digest_job.rb +8 -3
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +6 -4
- data/app/jobs/rails_error_dashboard/storm_notification_job.rb +26 -8
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +4 -2
- data/app/mailers/rails_error_dashboard/application_mailer.rb +14 -0
- data/app/mailers/rails_error_dashboard/digest_mailer.rb +18 -2
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +19 -2
- data/app/views/layouts/rails_error_dashboard.html.erb +255 -92
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.html.erb +23 -22
- data/app/views/rails_error_dashboard/digest_mailer/digest_summary.text.erb +30 -19
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +18 -16
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +21 -14
- data/app/views/rails_error_dashboard/errors/_ai_help_panel.html.erb +11 -7
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +45 -38
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +17 -14
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +27 -9
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +25 -21
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +25 -22
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +24 -12
- data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +12 -11
- data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +19 -13
- data/app/views/rails_error_dashboard/errors/_llm_summary.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +8 -8
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +60 -52
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +59 -49
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +23 -20
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +14 -14
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +118 -93
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +18 -12
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +20 -5
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +5 -5
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +7 -7
- data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +15 -12
- data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +25 -25
- data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +28 -28
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +82 -82
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +27 -27
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +61 -57
- data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +64 -65
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +23 -23
- data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +48 -36
- data/app/views/rails_error_dashboard/errors/index.html.erb +121 -70
- data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +30 -30
- data/app/views/rails_error_dashboard/errors/llm_health_summary.html.erb +38 -39
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +28 -25
- data/app/views/rails_error_dashboard/errors/overview.html.erb +45 -39
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +45 -44
- data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +39 -28
- data/app/views/rails_error_dashboard/errors/releases.html.erb +46 -47
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +48 -44
- data/app/views/rails_error_dashboard/errors/settings.html.erb +122 -112
- data/app/views/rails_error_dashboard/errors/show.html.erb +44 -37
- data/app/views/rails_error_dashboard/errors/storms.html.erb +30 -24
- data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +26 -26
- data/app/views/rails_error_dashboard/errors/user_impact.html.erb +31 -30
- data/config/locales/de.yml +2007 -0
- data/config/locales/en.yml +2719 -0
- data/config/locales/es.yml +2079 -0
- data/config/locales/fr.yml +2082 -0
- data/config/locales/it.yml +2060 -0
- data/config/locales/ja.yml +1802 -0
- data/config/locales/pl.yml +2108 -0
- data/config/locales/pt-BR.yml +2064 -0
- data/config/locales/ru.yml +2109 -0
- data/config/locales/uk.yml +2106 -0
- data/config/locales/zh-CN.yml +1791 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20260325000001_fix_swallowed_exceptions_index_for_mysql.rb +9 -3
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +3 -1
- data/lib/rails_error_dashboard/commands/batch_mute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/batch_unmute_errors.rb +4 -2
- data/lib/rails_error_dashboard/commands/create_issue.rb +5 -3
- data/lib/rails_error_dashboard/commands/link_existing_issue.rb +4 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +1 -1
- data/lib/rails_error_dashboard/configuration.rb +23 -0
- data/lib/rails_error_dashboard/current.rb +56 -0
- data/lib/rails_error_dashboard/i18n_store.rb +293 -0
- data/lib/rails_error_dashboard/private_backend.rb +213 -0
- data/lib/rails_error_dashboard/services/baseline_alert_payload_builder.rb +89 -22
- data/lib/rails_error_dashboard/services/digest_builder.rb +21 -7
- data/lib/rails_error_dashboard/services/discord_payload_builder.rb +27 -15
- data/lib/rails_error_dashboard/services/error_notification_dispatcher.rb +10 -5
- data/lib/rails_error_dashboard/services/localized_time_formatter.rb +117 -0
- data/lib/rails_error_dashboard/services/notification_helpers.rb +87 -4
- data/lib/rails_error_dashboard/services/pagerduty_payload_builder.rb +22 -5
- data/lib/rails_error_dashboard/services/slack_payload_builder.rb +48 -28
- data/lib/rails_error_dashboard/services/storm_protection/gate.rb +2 -1
- data/lib/rails_error_dashboard/services/webhook_payload_builder.rb +15 -1
- data/lib/rails_error_dashboard/translation.rb +39 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +4 -0
- data/lib/tasks/error_dashboard.rake +5 -1
- metadata +24 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Concerns
|
|
5
|
+
# Gives a job an explicit, serialized locale instead of an inherited one.
|
|
6
|
+
#
|
|
7
|
+
# WHY JOBS CANNOT JUST READ Current.locale
|
|
8
|
+
#
|
|
9
|
+
# Mailers and notification jobs render outside the dashboard's
|
|
10
|
+
# around_action. There is no request, so there is no request locale.
|
|
11
|
+
# Reading Current.locale in a job returns nil at best — and at worst, on a
|
|
12
|
+
# reused Puma or job-runner thread, whatever an unrelated request left
|
|
13
|
+
# behind. That is the same bug class as #143 and #148: state outliving the
|
|
14
|
+
# request that set it.
|
|
15
|
+
#
|
|
16
|
+
# So the locale is resolved at ENQUEUE time (see .enqueue_locale) and
|
|
17
|
+
# travels in the job's arguments. The job then reads only its own argument.
|
|
18
|
+
#
|
|
19
|
+
# WHY ENQUEUE TIME RESOLVES TO CONFIG, NOT TO A USER
|
|
20
|
+
#
|
|
21
|
+
# Notifications are enqueued from the capture path — log_error, the storm
|
|
22
|
+
# gate — which runs inside a *host app* request, not a dashboard one. There
|
|
23
|
+
# is no dashboard user in scope and Current.locale is correctly nil there.
|
|
24
|
+
# The meaningful answer is config.dashboard_locale, which is what
|
|
25
|
+
# Current.locale_or_default returns when Current.locale is unset. Resolving
|
|
26
|
+
# through that one path keeps a single precedence chain for the whole gem
|
|
27
|
+
# and lets a dashboard-initiated enqueue (a future "send test notification"
|
|
28
|
+
# button) pick up the acting user's locale for free.
|
|
29
|
+
#
|
|
30
|
+
# BACKWARD COMPATIBILITY IS NOT OPTIONAL HERE
|
|
31
|
+
#
|
|
32
|
+
# Queues drain across a deploy. Jobs enqueued by the previous version have
|
|
33
|
+
# no locale argument at all, and they must still run — a notification is
|
|
34
|
+
# never worth losing over a translation detail. Every consumer therefore
|
|
35
|
+
# defaults the argument, and #job_locale hardens whatever arrives.
|
|
36
|
+
module LocalizedJob
|
|
37
|
+
extend ActiveSupport::Concern
|
|
38
|
+
|
|
39
|
+
class_methods do
|
|
40
|
+
# The locale to serialize into a job's arguments, resolved in the
|
|
41
|
+
# enqueueing thread while its context is still valid.
|
|
42
|
+
#
|
|
43
|
+
# Returns a String, never a Symbol: ActiveJob serializes Symbols
|
|
44
|
+
# inconsistently across adapters, and Sidekiq's JSON round-trip turns
|
|
45
|
+
# one into a String anyway. Being explicit avoids a locale that is a
|
|
46
|
+
# Symbol on the inline adapter and a String in production.
|
|
47
|
+
#
|
|
48
|
+
# @return [String] a locale RED ships. Never nil, never raises.
|
|
49
|
+
def enqueue_locale
|
|
50
|
+
Current.locale_or_default
|
|
51
|
+
rescue StandardError
|
|
52
|
+
I18nStore::DEFAULT_LOCALE
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# The locale this job should render in, from its own argument.
|
|
59
|
+
#
|
|
60
|
+
# Total by design. A nil (pre-upgrade payload), a garbage string, a
|
|
61
|
+
# Symbol, or a locale RED does not ship all degrade to
|
|
62
|
+
# config.dashboard_locale and then to English. REQ-4: a notification must
|
|
63
|
+
# never be lost because of a locale problem.
|
|
64
|
+
#
|
|
65
|
+
# @param value [Object] whatever arrived in the job arguments
|
|
66
|
+
# @return [String] a locale RED ships
|
|
67
|
+
def job_locale(value)
|
|
68
|
+
candidate = value.to_s.strip
|
|
69
|
+
return Current.locale_or_default if candidate.empty?
|
|
70
|
+
|
|
71
|
+
# resolve/1 already degrades an unknown tag to English, but an unknown
|
|
72
|
+
# tag should first get a chance at the configured locale — a queued job
|
|
73
|
+
# carrying a stale tag is closer to "no opinion" than to "wants
|
|
74
|
+
# English".
|
|
75
|
+
return I18nStore.resolve(candidate) if I18nStore.available?(candidate)
|
|
76
|
+
|
|
77
|
+
Current.locale_or_default
|
|
78
|
+
rescue StandardError
|
|
79
|
+
I18nStore::DEFAULT_LOCALE
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -5,13 +5,15 @@ module RailsErrorDashboard
|
|
|
5
5
|
class DiscordErrorNotificationJob < ApplicationJob
|
|
6
6
|
queue_as :default
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
9
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
10
|
+
def perform(error_log_id, locale = nil)
|
|
9
11
|
error_log = ErrorLog.find(error_log_id)
|
|
10
12
|
webhook_url = RailsErrorDashboard.configuration.discord_webhook_url
|
|
11
13
|
|
|
12
14
|
return unless webhook_url.present?
|
|
13
15
|
|
|
14
|
-
payload = Services::DiscordPayloadBuilder.call(error_log)
|
|
16
|
+
payload = Services::DiscordPayloadBuilder.call(error_log, locale: job_locale(locale))
|
|
15
17
|
post_json(webhook_url, payload)
|
|
16
18
|
rescue StandardError => e
|
|
17
19
|
Rails.logger.error("[RailsErrorDashboard] Failed to send Discord notification: #{e.message}")
|
|
@@ -4,14 +4,16 @@ module RailsErrorDashboard
|
|
|
4
4
|
class EmailErrorNotificationJob < ApplicationJob
|
|
5
5
|
queue_as :error_notifications
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
8
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
9
|
+
def perform(error_log_id, locale = nil)
|
|
8
10
|
error_log = ErrorLog.find_by(id: error_log_id)
|
|
9
11
|
return unless error_log
|
|
10
12
|
|
|
11
13
|
recipients = RailsErrorDashboard.configuration.notification_email_recipients
|
|
12
14
|
return unless recipients.present?
|
|
13
15
|
|
|
14
|
-
ErrorNotificationMailer.error_alert(error_log, recipients).deliver_now
|
|
16
|
+
ErrorNotificationMailer.error_alert(error_log, recipients, locale: job_locale(locale)).deliver_now
|
|
15
17
|
rescue => e
|
|
16
18
|
Rails.logger.error("[RailsErrorDashboard] Failed to send email notification: #{e.message}")
|
|
17
19
|
Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
|
|
@@ -8,7 +8,9 @@ module RailsErrorDashboard
|
|
|
8
8
|
|
|
9
9
|
PAGERDUTY_EVENTS_API = "https://events.pagerduty.com/v2/enqueue"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
12
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
13
|
+
def perform(error_log_id, locale = nil)
|
|
12
14
|
error_log = ErrorLog.find(error_log_id)
|
|
13
15
|
|
|
14
16
|
# Only trigger PagerDuty for critical errors
|
|
@@ -17,7 +19,9 @@ module RailsErrorDashboard
|
|
|
17
19
|
routing_key = RailsErrorDashboard.configuration.pagerduty_integration_key
|
|
18
20
|
return unless routing_key.present?
|
|
19
21
|
|
|
20
|
-
payload = Services::PagerdutyPayloadBuilder.call(
|
|
22
|
+
payload = Services::PagerdutyPayloadBuilder.call(
|
|
23
|
+
error_log, routing_key: routing_key, locale: job_locale(locale)
|
|
24
|
+
)
|
|
21
25
|
response = post_json(PAGERDUTY_EVENTS_API, payload)
|
|
22
26
|
|
|
23
27
|
unless response_success?(response)
|
|
@@ -12,18 +12,23 @@ module RailsErrorDashboard
|
|
|
12
12
|
class ScheduledDigestJob < ApplicationJob
|
|
13
13
|
queue_as :default
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
16
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
17
|
+
def perform(period: "daily", application_id: nil, locale: nil)
|
|
16
18
|
return unless RailsErrorDashboard.configuration.enable_scheduled_digests
|
|
17
19
|
|
|
18
20
|
recipients = effective_recipients
|
|
19
21
|
return if recipients.blank?
|
|
20
22
|
|
|
23
|
+
resolved_locale = job_locale(locale)
|
|
24
|
+
|
|
21
25
|
digest = Services::DigestBuilder.call(
|
|
22
26
|
period: period.to_sym,
|
|
23
|
-
application_id: application_id
|
|
27
|
+
application_id: application_id,
|
|
28
|
+
locale: resolved_locale
|
|
24
29
|
)
|
|
25
30
|
|
|
26
|
-
DigestMailer.digest_summary(digest, recipients).deliver_now
|
|
31
|
+
DigestMailer.digest_summary(digest, recipients, locale: resolved_locale).deliver_now
|
|
27
32
|
rescue => e
|
|
28
33
|
Rails.logger.error("[RailsErrorDashboard] ScheduledDigestJob failed: #{e.class}: #{e.message}")
|
|
29
34
|
end
|
|
@@ -4,21 +4,23 @@ module RailsErrorDashboard
|
|
|
4
4
|
class SlackErrorNotificationJob < ApplicationJob
|
|
5
5
|
queue_as :error_notifications
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
8
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
9
|
+
def perform(error_log_id, locale = nil)
|
|
8
10
|
error_log = ErrorLog.find_by(id: error_log_id)
|
|
9
11
|
return unless error_log
|
|
10
12
|
|
|
11
13
|
webhook_url = RailsErrorDashboard.configuration.slack_webhook_url
|
|
12
14
|
return unless webhook_url.present?
|
|
13
15
|
|
|
14
|
-
send_slack_notification(error_log, webhook_url)
|
|
16
|
+
send_slack_notification(error_log, webhook_url, job_locale(locale))
|
|
15
17
|
rescue => e
|
|
16
18
|
Rails.logger.error("Failed to send Slack notification: #{e.message}")
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
private
|
|
20
22
|
|
|
21
|
-
def send_slack_notification(error_log, webhook_url)
|
|
23
|
+
def send_slack_notification(error_log, webhook_url, locale)
|
|
22
24
|
require "net/http"
|
|
23
25
|
require "json"
|
|
24
26
|
|
|
@@ -31,7 +33,7 @@ module RailsErrorDashboard
|
|
|
31
33
|
http.read_timeout = 10 # 10 seconds to read response
|
|
32
34
|
|
|
33
35
|
request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
|
|
34
|
-
request.body = Services::SlackPayloadBuilder.call(error_log).to_json
|
|
36
|
+
request.body = Services::SlackPayloadBuilder.call(error_log, locale: locale).to_json
|
|
35
37
|
|
|
36
38
|
response = http.request(request)
|
|
37
39
|
|
|
@@ -11,9 +11,11 @@ module RailsErrorDashboard
|
|
|
11
11
|
|
|
12
12
|
# @param started_at [String] ISO8601 episode start
|
|
13
13
|
# @param state [String] breaker state at notification time ("shedding"/"open")
|
|
14
|
-
|
|
14
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
15
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
16
|
+
def perform(started_at:, state: "shedding", locale: nil)
|
|
15
17
|
config = RailsErrorDashboard.configuration
|
|
16
|
-
message = build_message(started_at, state, config)
|
|
18
|
+
message = build_message(started_at, state, config, job_locale(locale))
|
|
17
19
|
|
|
18
20
|
if config.enable_slack_notifications && config.slack_webhook_url.present?
|
|
19
21
|
post_json(config.slack_webhook_url, { text: message })
|
|
@@ -38,14 +40,30 @@ module RailsErrorDashboard
|
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
# Assembled from four keys rather than one, because the dashboard link is
|
|
44
|
+
# conditional. Each key is a whole sentence — never a fragment joined to
|
|
45
|
+
# another, which would bake English word order into every translation.
|
|
46
|
+
#
|
|
47
|
+
# ":warning:" is Slack/Discord emoji shortcode, not text.
|
|
48
|
+
def build_message(started_at, state, config, locale)
|
|
49
|
+
mode_key = state == "open" ? "open" : "shedding"
|
|
50
|
+
mode = t("red.notifications.storm.mode.#{mode_key}", locale)
|
|
51
|
+
|
|
43
52
|
dashboard = (config.dashboard_base_url || "").chomp("/")
|
|
44
|
-
link = dashboard.present?
|
|
53
|
+
link = if dashboard.present?
|
|
54
|
+
" " + t("red.notifications.storm.dashboard", locale, url: "#{dashboard}/errors/storms")
|
|
55
|
+
else
|
|
56
|
+
""
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
":warning: " \
|
|
60
|
+
"#{t("red.notifications.storm.detected", locale, application: app_name(config), started_at: started_at)} " \
|
|
61
|
+
"#{t("red.notifications.storm.engaged", locale, mode: mode)} " \
|
|
62
|
+
"#{t("red.notifications.storm.suppressed", locale)}#{link}"
|
|
63
|
+
end
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"suppressed until the storm subsides; exact counts are preserved.#{link}"
|
|
65
|
+
def t(key, locale, **options)
|
|
66
|
+
RailsErrorDashboard::I18nStore.translate(key, locale: locale, **options)
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
def app_name(config)
|
|
@@ -6,7 +6,9 @@ module RailsErrorDashboard
|
|
|
6
6
|
class WebhookErrorNotificationJob < ApplicationJob
|
|
7
7
|
queue_as :default
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# @param locale [String, nil] resolved at enqueue time. nil for jobs
|
|
10
|
+
# enqueued by a pre-Phase-4 version still draining from the queue.
|
|
11
|
+
def perform(error_log_id, locale = nil)
|
|
10
12
|
error_log = ErrorLog.find(error_log_id)
|
|
11
13
|
webhook_urls = RailsErrorDashboard.configuration.webhook_urls
|
|
12
14
|
|
|
@@ -15,7 +17,7 @@ module RailsErrorDashboard
|
|
|
15
17
|
# Ensure webhook_urls is an array
|
|
16
18
|
urls = Array(webhook_urls)
|
|
17
19
|
|
|
18
|
-
payload = Services::WebhookPayloadBuilder.call(error_log)
|
|
20
|
+
payload = Services::WebhookPayloadBuilder.call(error_log, locale: job_locale(locale))
|
|
19
21
|
|
|
20
22
|
urls.each do |url|
|
|
21
23
|
send_webhook(url, payload, error_log)
|
|
@@ -4,5 +4,19 @@ module RailsErrorDashboard
|
|
|
4
4
|
class ApplicationMailer < ActionMailer::Base
|
|
5
5
|
default from: -> { RailsErrorDashboard.configuration.notification_email_from }
|
|
6
6
|
layout false
|
|
7
|
+
|
|
8
|
+
# Mailers do not pick up engine helpers the way controllers do, so these
|
|
9
|
+
# are included explicitly.
|
|
10
|
+
#
|
|
11
|
+
# I18nHelper gives the .html.erb parts red_t; MailerI18nHelper adds
|
|
12
|
+
# red_mail_t (unescaped, for the .text.erb parts) and red_mail_time (a
|
|
13
|
+
# localized timestamp that needs no JavaScript).
|
|
14
|
+
#
|
|
15
|
+
# The locale arrives as an explicit argument and is assigned to
|
|
16
|
+
# @red_locale, which I18nHelper#red_locale prefers over Current — a mailer
|
|
17
|
+
# renders outside the request that set Current, and on a reused thread
|
|
18
|
+
# Current holds an unrelated request's value (P4-T1).
|
|
19
|
+
helper RailsErrorDashboard::I18nHelper
|
|
20
|
+
helper RailsErrorDashboard::MailerI18nHelper
|
|
7
21
|
end
|
|
8
22
|
end
|
|
@@ -2,18 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsErrorDashboard
|
|
4
4
|
class DigestMailer < ApplicationMailer
|
|
5
|
-
|
|
5
|
+
# @param locale [String] resolved at enqueue time, never read from
|
|
6
|
+
# Current — a mailer renders outside the request that set it.
|
|
7
|
+
def digest_summary(digest, recipients, locale: I18nStore::DEFAULT_LOCALE)
|
|
6
8
|
@digest = digest
|
|
7
9
|
@dashboard_url = dashboard_base_url
|
|
10
|
+
@red_locale = locale
|
|
8
11
|
|
|
9
12
|
mail(
|
|
10
13
|
to: recipients,
|
|
11
|
-
subject:
|
|
14
|
+
subject: subject_for(digest, locale)
|
|
12
15
|
)
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
private
|
|
16
19
|
|
|
20
|
+
# "N new errors" was an English binary plural built by interpolation. It is
|
|
21
|
+
# now a real plural key, selected by the locale's CLDR rules (REQ-1).
|
|
22
|
+
def subject_for(digest, locale)
|
|
23
|
+
count = digest[:stats][:new_errors].to_i
|
|
24
|
+
|
|
25
|
+
I18nStore.translate(
|
|
26
|
+
"red.mailers.digest.subject",
|
|
27
|
+
locale: locale,
|
|
28
|
+
count: count,
|
|
29
|
+
period: digest[:period_label]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
17
33
|
def dashboard_base_url
|
|
18
34
|
base = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
|
|
19
35
|
mount_path = RailsErrorDashboard.configuration.engine_mount_path
|
|
@@ -2,18 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsErrorDashboard
|
|
4
4
|
class ErrorNotificationMailer < ApplicationMailer
|
|
5
|
-
|
|
5
|
+
# @param locale [String] resolved at enqueue time, never read from
|
|
6
|
+
# Current — a mailer renders outside the request that set it.
|
|
7
|
+
def error_alert(error_log, recipients, locale: I18nStore::DEFAULT_LOCALE)
|
|
6
8
|
@error_log = error_log
|
|
7
9
|
@dashboard_url = dashboard_url(error_log)
|
|
10
|
+
@red_locale = locale
|
|
8
11
|
|
|
9
12
|
mail(
|
|
10
13
|
to: recipients,
|
|
11
|
-
subject:
|
|
14
|
+
subject: subject_for(error_log, locale)
|
|
12
15
|
)
|
|
13
16
|
end
|
|
14
17
|
|
|
15
18
|
private
|
|
16
19
|
|
|
20
|
+
# The subject is assembled from a key rather than interpolated inline, so a
|
|
21
|
+
# locale can reorder or drop the emoji. error_type and message are verbatim
|
|
22
|
+
# error content and are never translated.
|
|
23
|
+
def subject_for(error_log, locale)
|
|
24
|
+
I18nStore.translate(
|
|
25
|
+
"red.mailers.error_alert.subject",
|
|
26
|
+
locale: locale,
|
|
27
|
+
application: error_log.application&.name ||
|
|
28
|
+
I18nStore.translate("red.mailers.shared.unknown_application", locale: locale),
|
|
29
|
+
error_type: error_log.error_type,
|
|
30
|
+
message: truncate_subject(error_log.message)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
def dashboard_url(error_log)
|
|
18
35
|
base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
|
|
19
36
|
mount_path = RailsErrorDashboard.configuration.engine_mount_path
|