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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Services
|
|
5
|
+
# strftime with the word-producing directives resolved from RED's own
|
|
6
|
+
# dictionary.
|
|
7
|
+
#
|
|
8
|
+
# WHY THIS EXISTS
|
|
9
|
+
#
|
|
10
|
+
# Ruby's strftime is not locale-aware: %B is "August" whatever RED's locale
|
|
11
|
+
# is. On the dashboard that does not matter, because a timestamp is
|
|
12
|
+
# rendered as a <span data-format> and the browser re-renders it through
|
|
13
|
+
# formatDateTime() using the red.js.* month/day/meridian vocabulary P3-T2
|
|
14
|
+
# added. Anywhere there is no browser — email (P4-T2), Slack and Discord
|
|
15
|
+
# payloads (P4-T3) — the server has to do that substitution itself.
|
|
16
|
+
#
|
|
17
|
+
# Sharing one implementation is the point: an email, a Slack message and
|
|
18
|
+
# the dashboard should not disagree about what a date looks like.
|
|
19
|
+
#
|
|
20
|
+
# Takes the locale as an argument rather than reading Current, because
|
|
21
|
+
# every caller runs inside a job where Current is nil or belongs to an
|
|
22
|
+
# unrelated request.
|
|
23
|
+
class LocalizedTimeFormatter
|
|
24
|
+
# The directives whose output is a *word*, and therefore locale-dependent:
|
|
25
|
+
# %B %b (month), %A %a (weekday), %p %P (meridian). Everything else
|
|
26
|
+
# (%Y, %m, %d, %H, %M, %S, %Z …) is digits or a timezone abbreviation and
|
|
27
|
+
# is left to strftime.
|
|
28
|
+
LOCALIZED_DIRECTIVES = /%[BbAaPp]/
|
|
29
|
+
|
|
30
|
+
# Stand-in for a localized word while strftime runs. Contains no percent,
|
|
31
|
+
# so strftime cannot read it as a directive, and no space, so it cannot
|
|
32
|
+
# alter the spacing the pattern asked for. The index keeps two
|
|
33
|
+
# occurrences of the same directive independent.
|
|
34
|
+
PLACEHOLDER = "\u0000RED%<index>d\u0000"
|
|
35
|
+
|
|
36
|
+
# @param time [Time]
|
|
37
|
+
# @param pattern [String] a strftime pattern
|
|
38
|
+
# @param locale [String]
|
|
39
|
+
# @return [String]
|
|
40
|
+
def self.call(time, pattern:, locale:)
|
|
41
|
+
new(locale).render(time, pattern)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(locale)
|
|
45
|
+
@locale = locale
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Two-pass, via placeholders: substituting a translated month name
|
|
49
|
+
# directly would put arbitrary text into the pattern, and while a French
|
|
50
|
+
# "%A" -> "mardi" is harmless, a translation containing a literal percent
|
|
51
|
+
# sign would corrupt every directive after it. The placeholder carries no
|
|
52
|
+
# percent, so strftime cannot misread it.
|
|
53
|
+
def render(time, pattern)
|
|
54
|
+
replacements = []
|
|
55
|
+
|
|
56
|
+
staged = pattern.gsub(LOCALIZED_DIRECTIVES) do |directive|
|
|
57
|
+
word = localized_word(directive, time)
|
|
58
|
+
next directive if word.nil?
|
|
59
|
+
|
|
60
|
+
replacements << word
|
|
61
|
+
format(PLACEHOLDER, index: replacements.length - 1)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
rendered = time.strftime(staged)
|
|
65
|
+
|
|
66
|
+
replacements.each_with_index do |word, index|
|
|
67
|
+
rendered = rendered.sub(format(PLACEHOLDER, index: index), word)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
rendered
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :locale
|
|
76
|
+
|
|
77
|
+
# @return [String, nil] nil means "leave it to strftime"
|
|
78
|
+
def localized_word(directive, time)
|
|
79
|
+
case directive
|
|
80
|
+
when "%B" then name_array(:months, 12)[time.month - 1]
|
|
81
|
+
when "%b" then name_array(:months_short, 12)[time.month - 1]
|
|
82
|
+
when "%A" then name_array(:days, 7)[time.wday]
|
|
83
|
+
when "%a" then name_array(:days_short, 7)[time.wday]
|
|
84
|
+
when "%p" then meridian_for(time)
|
|
85
|
+
when "%P" then meridian_for(time)&.downcase
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# I18nStore.subtree returns SYMBOL keys — the backend loads the YAML
|
|
90
|
+
# symbolized. Indexing with a String silently yields nil here, and
|
|
91
|
+
# because a nil array falls back to English the bug looks exactly like
|
|
92
|
+
# "this locale has no translation" rather than like a defect. Caught in
|
|
93
|
+
# P4-T2 by a spec that asserted a translated month name and got English.
|
|
94
|
+
#
|
|
95
|
+
# A locale that translates only some of these must not produce a mix of
|
|
96
|
+
# translated and English names, so the whole array falls back together —
|
|
97
|
+
# the same rule I18nStore.subtree applies for the JS payload.
|
|
98
|
+
def name_array(key, expected_length)
|
|
99
|
+
names = I18nStore.subtree("red.js", locale: locale)[key]
|
|
100
|
+
return names if names.is_a?(Array) && names.length == expected_length
|
|
101
|
+
|
|
102
|
+
fallback = I18nStore.subtree("red.js", locale: I18nStore::DEFAULT_LOCALE)[key]
|
|
103
|
+
fallback.is_a?(Array) ? fallback : []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A 24-hour locale may legitimately translate these to "", which is why
|
|
107
|
+
# this checks for a String rather than for presence.
|
|
108
|
+
def meridian_for(time)
|
|
109
|
+
meridian = I18nStore.subtree("red.js.meridian", locale: locale)
|
|
110
|
+
value = time.hour >= 12 ? meridian[:pm] : meridian[:am]
|
|
111
|
+
return value if value.is_a?(String)
|
|
112
|
+
|
|
113
|
+
time.hour >= 12 ? "PM" : "AM"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -9,6 +9,81 @@ module RailsErrorDashboard
|
|
|
9
9
|
module NotificationHelpers
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
# Translate for a notification payload.
|
|
13
|
+
#
|
|
14
|
+
# Positional locale rather than the ambient one: a payload is built
|
|
15
|
+
# inside a job, where Current.locale is nil at best and an unrelated
|
|
16
|
+
# request's value at worst (P4-T1). Every builder threads the locale it
|
|
17
|
+
# was handed.
|
|
18
|
+
#
|
|
19
|
+
# No escaping — these values go into JSON, not HTML.
|
|
20
|
+
#
|
|
21
|
+
# @return [String] never nil, never raises
|
|
22
|
+
def t(key, locale, **options)
|
|
23
|
+
I18nStore.translate(key, locale: locale, **options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A "*Label:*\nvalue" Slack mrkdwn field.
|
|
27
|
+
#
|
|
28
|
+
# The bold-label-newline-value shape is Slack markup, not language, so it
|
|
29
|
+
# stays here rather than being baked into each translation — a translator
|
|
30
|
+
# editing "*%{label}:*" could break the formatting for every field at
|
|
31
|
+
# once. Only the label itself is translated.
|
|
32
|
+
#
|
|
33
|
+
# @param label [Symbol] a key under red.notifications.error_alert.labels
|
|
34
|
+
def field(label, value, locale)
|
|
35
|
+
"*#{t("red.notifications.error_alert.labels.#{label}", locale)}:*\n#{value}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One label from red.notifications.error_alert.labels.
|
|
39
|
+
#
|
|
40
|
+
# Discord wants a bare field name, Slack wants it wrapped in its mrkdwn
|
|
41
|
+
# bold-and-newline shape — hence this and #field rather than one method.
|
|
42
|
+
#
|
|
43
|
+
# @param label [Symbol]
|
|
44
|
+
def label(label, locale)
|
|
45
|
+
t("red.notifications.error_alert.labels.#{label}", locale)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [String] the localized "Unknown" placeholder
|
|
49
|
+
def unknown(locale)
|
|
50
|
+
t("red.notifications.shared.unknown", locale)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String] the localized "N/A" placeholder
|
|
54
|
+
def not_available(locale)
|
|
55
|
+
t("red.notifications.shared.not_available", locale)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A human-readable timestamp for a notification body.
|
|
59
|
+
#
|
|
60
|
+
# Uses the locale's date format and month names rather than strftime's,
|
|
61
|
+
# which are always English (see MailerI18nHelper for the same problem).
|
|
62
|
+
# Falls back to the machine format if anything goes wrong — a wrong-looking
|
|
63
|
+
# timestamp is better than a lost notification.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] "" for nil
|
|
66
|
+
def format_datetime(time, locale)
|
|
67
|
+
return "" if time.nil?
|
|
68
|
+
|
|
69
|
+
utc = time.respond_to?(:utc) ? time.utc : time
|
|
70
|
+
pattern = I18nStore.translate("red.time.formats.full", locale: locale)
|
|
71
|
+
# A miss returns humanized key text, which would be a nonsense strftime
|
|
72
|
+
# pattern. Detect it the same way red_time_format does.
|
|
73
|
+
pattern = I18nStore.translate("red.time.formats.full", locale: I18nStore::DEFAULT_LOCALE) unless pattern.include?("%")
|
|
74
|
+
|
|
75
|
+
LocalizedTimeFormatter.call(utc, pattern: pattern, locale: locale)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
format_time(time)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A localized timestamp, or the localized "N/A" for nil.
|
|
81
|
+
def format_datetime_or_na(time, locale)
|
|
82
|
+
return not_available(locale) if time.nil?
|
|
83
|
+
|
|
84
|
+
format_datetime(time, locale)
|
|
85
|
+
end
|
|
86
|
+
|
|
12
87
|
# Generate dashboard URL for an error
|
|
13
88
|
# @param error_log [ErrorLog] The error
|
|
14
89
|
# @return [String] Full URL to the error detail page
|
|
@@ -46,16 +121,24 @@ module RailsErrorDashboard
|
|
|
46
121
|
end
|
|
47
122
|
|
|
48
123
|
# Extract first backtrace line (truncated)
|
|
124
|
+
#
|
|
125
|
+
# The line itself is diagnostic output and is never translated — only the
|
|
126
|
+
# "N/A" shown in its absence is, and only when a locale is supplied.
|
|
127
|
+
# Callers that pass no locale keep the literal, so existing non-i18n
|
|
128
|
+
# callers are unaffected.
|
|
129
|
+
#
|
|
49
130
|
# @param backtrace [String, Array, nil] Raw backtrace
|
|
50
131
|
# @param length [Integer] Maximum length (default 100)
|
|
51
|
-
# @
|
|
52
|
-
|
|
53
|
-
|
|
132
|
+
# @param locale [String, nil] locale for the placeholder
|
|
133
|
+
# @return [String] First line, or the "N/A" placeholder
|
|
134
|
+
def extract_first_backtrace_line(backtrace, length = 100, locale: nil)
|
|
135
|
+
placeholder = locale ? not_available(locale) : "N/A"
|
|
136
|
+
return placeholder if backtrace.nil?
|
|
54
137
|
|
|
55
138
|
lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
|
|
56
139
|
first_line = lines.first&.strip
|
|
57
140
|
|
|
58
|
-
return
|
|
141
|
+
return placeholder if first_line.nil?
|
|
59
142
|
first_line.length > length ? "#{first_line[0...length]}..." : first_line
|
|
60
143
|
end
|
|
61
144
|
|
|
@@ -12,16 +12,32 @@ module RailsErrorDashboard
|
|
|
12
12
|
class PagerdutyPayloadBuilder
|
|
13
13
|
# @param error_log [ErrorLog] The error to build a payload for
|
|
14
14
|
# @param routing_key [String] PagerDuty integration key
|
|
15
|
+
# @param locale [String] resolved by the enqueueing thread (P4-T1)
|
|
15
16
|
# @return [Hash] PagerDuty Events API v2 payload
|
|
16
|
-
|
|
17
|
+
#
|
|
18
|
+
# ONLY "summary" AND THE LINK TEXT ARE LOCALIZED.
|
|
19
|
+
#
|
|
20
|
+
# event_action ("trigger") and severity ("critical") are PagerDuty API
|
|
21
|
+
# enums, not adjectives — PagerDuty rejects any other value, so a
|
|
22
|
+
# translated "critical" would drop the incident rather than localize it.
|
|
23
|
+
# The custom_details keys are consumed by whatever the operator has wired
|
|
24
|
+
# downstream, and "client" is RED's product name.
|
|
25
|
+
def self.call(error_log, routing_key:, locale: I18nStore::DEFAULT_LOCALE)
|
|
17
26
|
{
|
|
18
27
|
routing_key: routing_key,
|
|
19
28
|
event_action: "trigger",
|
|
20
29
|
payload: {
|
|
21
|
-
summary:
|
|
30
|
+
summary: NotificationHelpers.t(
|
|
31
|
+
"red.notifications.error_alert.pagerduty_summary",
|
|
32
|
+
locale,
|
|
33
|
+
application: NotificationHelpers.app_name(error_log),
|
|
34
|
+
error_type: error_log.error_type,
|
|
35
|
+
platform: error_log.platform
|
|
36
|
+
),
|
|
37
|
+
# API enum, not an adjective. Never translated.
|
|
22
38
|
severity: "critical",
|
|
23
39
|
source: NotificationHelpers.error_source(error_log),
|
|
24
|
-
component: error_log.controller_name ||
|
|
40
|
+
component: error_log.controller_name || NotificationHelpers.unknown(locale),
|
|
25
41
|
group: error_log.error_type,
|
|
26
42
|
class: error_log.error_type,
|
|
27
43
|
custom_details: {
|
|
@@ -41,10 +57,11 @@ module RailsErrorDashboard
|
|
|
41
57
|
links: [
|
|
42
58
|
{
|
|
43
59
|
href: NotificationHelpers.dashboard_url(error_log),
|
|
44
|
-
text: "
|
|
60
|
+
text: NotificationHelpers.t("red.notifications.error_alert.view_in_dashboard", locale)
|
|
45
61
|
}
|
|
46
62
|
],
|
|
47
|
-
|
|
63
|
+
# Product name — not translated.
|
|
64
|
+
client: NotificationHelpers.t("red.notifications.shared.product_name", locale),
|
|
48
65
|
client_url: NotificationHelpers.dashboard_url(error_log)
|
|
49
66
|
}
|
|
50
67
|
end
|
|
@@ -11,100 +11,120 @@ module RailsErrorDashboard
|
|
|
11
11
|
# # => { text: "...", blocks: [...] }
|
|
12
12
|
class SlackPayloadBuilder
|
|
13
13
|
# @param error_log [ErrorLog] The error to build a payload for
|
|
14
|
+
# @param locale [String] resolved by the enqueueing thread (P4-T1)
|
|
14
15
|
# @return [Hash] Slack Block Kit payload
|
|
15
|
-
|
|
16
|
+
#
|
|
17
|
+
# Only the human-readable *values* are localized. Every "type" and
|
|
18
|
+
# "style" below is Slack Block Kit vocabulary that Slack parses —
|
|
19
|
+
# translating one would produce an invalid payload, not a localized one.
|
|
20
|
+
def self.call(error_log, locale: I18nStore::DEFAULT_LOCALE)
|
|
16
21
|
{
|
|
17
|
-
text: "
|
|
22
|
+
text: NotificationHelpers.t("red.notifications.error_alert.fallback", locale),
|
|
18
23
|
blocks: [
|
|
19
|
-
header_block,
|
|
20
|
-
fields_block(error_log),
|
|
21
|
-
message_block(error_log),
|
|
22
|
-
user_block(error_log),
|
|
23
|
-
request_block(error_log),
|
|
24
|
-
actions_block(error_log),
|
|
25
|
-
context_block(error_log)
|
|
24
|
+
header_block(locale),
|
|
25
|
+
fields_block(error_log, locale),
|
|
26
|
+
message_block(error_log, locale),
|
|
27
|
+
user_block(error_log, locale),
|
|
28
|
+
request_block(error_log, locale),
|
|
29
|
+
actions_block(error_log, locale),
|
|
30
|
+
context_block(error_log, locale)
|
|
26
31
|
].compact
|
|
27
32
|
}
|
|
28
33
|
end
|
|
29
34
|
|
|
30
|
-
def self.header_block
|
|
35
|
+
def self.header_block(locale)
|
|
31
36
|
{
|
|
32
37
|
type: "header",
|
|
33
38
|
text: {
|
|
34
39
|
type: "plain_text",
|
|
35
|
-
text: "
|
|
40
|
+
text: NotificationHelpers.t("red.notifications.error_alert.heading", locale),
|
|
36
41
|
emoji: true
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
def self.fields_block(error_log)
|
|
46
|
+
def self.fields_block(error_log, locale)
|
|
42
47
|
{
|
|
43
48
|
type: "section",
|
|
44
49
|
fields: [
|
|
45
50
|
{
|
|
46
51
|
type: "mrkdwn",
|
|
47
|
-
text:
|
|
52
|
+
text: NotificationHelpers.field(:application, NotificationHelpers.app_name(error_log), locale)
|
|
48
53
|
},
|
|
49
54
|
{
|
|
50
55
|
type: "mrkdwn",
|
|
51
|
-
text: "
|
|
56
|
+
text: NotificationHelpers.field(:error_type, "`#{error_log.error_type}`", locale)
|
|
52
57
|
},
|
|
53
58
|
{
|
|
54
59
|
type: "mrkdwn",
|
|
55
|
-
text:
|
|
60
|
+
text: NotificationHelpers.field(
|
|
61
|
+
:platform,
|
|
62
|
+
"#{NotificationHelpers.platform_emoji(error_log.platform)} " \
|
|
63
|
+
"#{error_log.platform || NotificationHelpers.unknown(locale)}",
|
|
64
|
+
locale
|
|
65
|
+
)
|
|
56
66
|
},
|
|
57
67
|
{
|
|
58
68
|
type: "mrkdwn",
|
|
59
|
-
text:
|
|
69
|
+
text: NotificationHelpers.field(
|
|
70
|
+
:occurred, NotificationHelpers.format_datetime(error_log.occurred_at, locale), locale
|
|
71
|
+
)
|
|
60
72
|
}
|
|
61
73
|
]
|
|
62
74
|
}
|
|
63
75
|
end
|
|
64
76
|
|
|
65
|
-
def self.message_block(error_log)
|
|
77
|
+
def self.message_block(error_log, locale)
|
|
66
78
|
{
|
|
67
79
|
type: "section",
|
|
68
80
|
text: {
|
|
69
81
|
type: "mrkdwn",
|
|
70
|
-
|
|
82
|
+
# Message content is diagnostic output and is never translated.
|
|
83
|
+
text: NotificationHelpers.field(
|
|
84
|
+
:message, "```#{NotificationHelpers.truncate_message(error_log.message)}```", locale
|
|
85
|
+
)
|
|
71
86
|
}
|
|
72
87
|
}
|
|
73
88
|
end
|
|
74
89
|
|
|
75
|
-
def self.user_block(error_log)
|
|
90
|
+
def self.user_block(error_log, locale)
|
|
76
91
|
return nil unless error_log.user_id.present?
|
|
77
92
|
|
|
78
|
-
user_email = error_log.user&.email ||
|
|
93
|
+
user_email = error_log.user&.email ||
|
|
94
|
+
NotificationHelpers.t("red.notifications.error_alert.user_fallback", locale, id: error_log.user_id)
|
|
79
95
|
|
|
80
96
|
{
|
|
81
97
|
type: "section",
|
|
82
98
|
fields: [
|
|
83
99
|
{
|
|
84
100
|
type: "mrkdwn",
|
|
85
|
-
text:
|
|
101
|
+
text: NotificationHelpers.field(:user, user_email, locale)
|
|
86
102
|
},
|
|
87
103
|
{
|
|
88
104
|
type: "mrkdwn",
|
|
89
|
-
text:
|
|
105
|
+
text: NotificationHelpers.field(
|
|
106
|
+
:ip_address, error_log.ip_address || NotificationHelpers.not_available(locale), locale
|
|
107
|
+
)
|
|
90
108
|
}
|
|
91
109
|
]
|
|
92
110
|
}
|
|
93
111
|
end
|
|
94
112
|
|
|
95
|
-
def self.request_block(error_log)
|
|
113
|
+
def self.request_block(error_log, locale)
|
|
96
114
|
return nil unless error_log.request_url.present?
|
|
97
115
|
|
|
98
116
|
{
|
|
99
117
|
type: "section",
|
|
100
118
|
text: {
|
|
101
119
|
type: "mrkdwn",
|
|
102
|
-
text:
|
|
120
|
+
text: NotificationHelpers.field(
|
|
121
|
+
:request_url, "`#{NotificationHelpers.truncate_message(error_log.request_url, 200)}`", locale
|
|
122
|
+
)
|
|
103
123
|
}
|
|
104
124
|
}
|
|
105
125
|
end
|
|
106
126
|
|
|
107
|
-
def self.actions_block(error_log)
|
|
127
|
+
def self.actions_block(error_log, locale)
|
|
108
128
|
{
|
|
109
129
|
type: "actions",
|
|
110
130
|
elements: [
|
|
@@ -112,7 +132,7 @@ module RailsErrorDashboard
|
|
|
112
132
|
type: "button",
|
|
113
133
|
text: {
|
|
114
134
|
type: "plain_text",
|
|
115
|
-
text: "
|
|
135
|
+
text: NotificationHelpers.t("red.notifications.error_alert.view_details", locale),
|
|
116
136
|
emoji: true
|
|
117
137
|
},
|
|
118
138
|
url: NotificationHelpers.dashboard_url(error_log),
|
|
@@ -122,13 +142,13 @@ module RailsErrorDashboard
|
|
|
122
142
|
}
|
|
123
143
|
end
|
|
124
144
|
|
|
125
|
-
def self.context_block(error_log)
|
|
145
|
+
def self.context_block(error_log, locale)
|
|
126
146
|
{
|
|
127
147
|
type: "context",
|
|
128
148
|
elements: [
|
|
129
149
|
{
|
|
130
150
|
type: "mrkdwn",
|
|
131
|
-
text: "
|
|
151
|
+
text: NotificationHelpers.t("red.notifications.error_alert.error_id", locale, id: error_log.id)
|
|
132
152
|
}
|
|
133
153
|
]
|
|
134
154
|
}
|
|
@@ -240,7 +240,8 @@ module RailsErrorDashboard
|
|
|
240
240
|
@storm_notified_episode = episode[:started_at]
|
|
241
241
|
StormNotificationJob.perform_later(
|
|
242
242
|
started_at: episode[:started_at].iso8601,
|
|
243
|
-
state: state.to_s
|
|
243
|
+
state: state.to_s,
|
|
244
|
+
locale: ApplicationJob.enqueue_locale
|
|
244
245
|
)
|
|
245
246
|
rescue => e
|
|
246
247
|
RailsErrorDashboard::Logger.error(
|
|
@@ -11,8 +11,22 @@ module RailsErrorDashboard
|
|
|
11
11
|
# # => { event: "error.created", timestamp: "...", error: { ... } }
|
|
12
12
|
class WebhookPayloadBuilder
|
|
13
13
|
# @param error_log [ErrorLog] The error to build a payload for
|
|
14
|
+
# @param locale [String] accepted so the signature matches its siblings
|
|
14
15
|
# @return [Hash] Webhook payload
|
|
15
|
-
|
|
16
|
+
#
|
|
17
|
+
# DELIBERATELY NOT LOCALIZED (P4-T3 REQ-2, REQ-6).
|
|
18
|
+
#
|
|
19
|
+
# Every key here, and every value, is consumed by a program the operator
|
|
20
|
+
# wrote — not read by a person. "error.created", the severity string, the
|
|
21
|
+
# ISO8601 timestamps and the field names are an API contract; translating
|
|
22
|
+
# any of them silently breaks whatever is parsing the payload. There is
|
|
23
|
+
# no human-readable chrome in this payload to translate.
|
|
24
|
+
#
|
|
25
|
+
# The locale parameter exists so every builder has the same signature and
|
|
26
|
+
# the dispatcher does not need to special-case this one.
|
|
27
|
+
def self.call(error_log, locale: I18nStore::DEFAULT_LOCALE)
|
|
28
|
+
_locale = locale
|
|
29
|
+
|
|
16
30
|
{
|
|
17
31
|
event: "error.created",
|
|
18
32
|
timestamp: Time.current.iso8601,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Translation for code that is not a view.
|
|
5
|
+
#
|
|
6
|
+
# I18nHelper#red_t is a view helper: it html-escapes its output, because that
|
|
7
|
+
# is what a template needs. Controllers and commands produce flash messages
|
|
8
|
+
# and result strings that Rails escapes when the view renders them, so
|
|
9
|
+
# escaping here would double-escape every apostrophe.
|
|
10
|
+
#
|
|
11
|
+
# Both paths resolve the locale the same way — Current.locale_or_default —
|
|
12
|
+
# so a flash message set during a request renders in the same locale as the
|
|
13
|
+
# page that shows it.
|
|
14
|
+
#
|
|
15
|
+
# Like everything else in the i18n path, these never raise. A controller
|
|
16
|
+
# cannot be the place a translation lookup takes down the dashboard.
|
|
17
|
+
module Translation
|
|
18
|
+
# @param key [String, Symbol] e.g. "red.flash.issue.created"
|
|
19
|
+
# @return [String] never nil, never raises, never html-escaped
|
|
20
|
+
def red_t(key, **options)
|
|
21
|
+
I18nStore.translate(key, locale: red_locale, **options)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Pluralized translation. Selects the form from +count+ using the locale's
|
|
27
|
+
# CLDR rules rather than an English binary plural.
|
|
28
|
+
def red_tp(key, count:, **options)
|
|
29
|
+
red_t(key, count: count, **options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String] a locale RED ships
|
|
33
|
+
def red_locale
|
|
34
|
+
Current.locale_or_default
|
|
35
|
+
rescue StandardError
|
|
36
|
+
I18nStore::DEFAULT_LOCALE
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
require "rails_error_dashboard/version"
|
|
2
2
|
require "rails_error_dashboard/engine"
|
|
3
|
+
require "rails_error_dashboard/i18n_store"
|
|
4
|
+
require "rails_error_dashboard/current"
|
|
5
|
+
require "rails_error_dashboard/translation"
|
|
3
6
|
require "rails_error_dashboard/configuration_error"
|
|
4
7
|
require "rails_error_dashboard/test_error"
|
|
5
8
|
require "rails_error_dashboard/configuration"
|
|
@@ -37,6 +40,7 @@ require "rails_error_dashboard/services/error_normalizer"
|
|
|
37
40
|
require "rails_error_dashboard/services/exception_filter"
|
|
38
41
|
require "rails_error_dashboard/services/error_hash_generator"
|
|
39
42
|
require "rails_error_dashboard/services/error_notification_dispatcher"
|
|
43
|
+
require "rails_error_dashboard/services/localized_time_formatter"
|
|
40
44
|
require "rails_error_dashboard/services/notification_helpers"
|
|
41
45
|
require "rails_error_dashboard/services/slack_payload_builder"
|
|
42
46
|
require "rails_error_dashboard/services/discord_payload_builder"
|
|
@@ -545,7 +545,11 @@ namespace :error_dashboard do
|
|
|
545
545
|
end
|
|
546
546
|
|
|
547
547
|
puts "Sending #{period} error digest to #{recipients.join(', ')}..."
|
|
548
|
-
RailsErrorDashboard::ScheduledDigestJob.perform_later(
|
|
548
|
+
RailsErrorDashboard::ScheduledDigestJob.perform_later(
|
|
549
|
+
period: period,
|
|
550
|
+
application_id: app_id,
|
|
551
|
+
locale: RailsErrorDashboard::ApplicationJob.enqueue_locale
|
|
552
|
+
)
|
|
549
553
|
puts "Digest job enqueued."
|
|
550
554
|
end
|
|
551
555
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -169,14 +169,14 @@ dependencies:
|
|
|
169
169
|
requirements:
|
|
170
170
|
- - "~>"
|
|
171
171
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: '
|
|
172
|
+
version: '1.1'
|
|
173
173
|
type: :development
|
|
174
174
|
prerelease: false
|
|
175
175
|
version_requirements: !ruby/object:Gem::Requirement
|
|
176
176
|
requirements:
|
|
177
177
|
- - "~>"
|
|
178
178
|
- !ruby/object:Gem::Version
|
|
179
|
-
version: '
|
|
179
|
+
version: '1.1'
|
|
180
180
|
- !ruby/object:Gem::Dependency
|
|
181
181
|
name: appraisal
|
|
182
182
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -246,9 +246,12 @@ files:
|
|
|
246
246
|
- Rakefile
|
|
247
247
|
- app/controllers/rails_error_dashboard/application_controller.rb
|
|
248
248
|
- app/controllers/rails_error_dashboard/errors_controller.rb
|
|
249
|
+
- app/controllers/rails_error_dashboard/locales_controller.rb
|
|
249
250
|
- app/controllers/rails_error_dashboard/webhooks_controller.rb
|
|
250
251
|
- app/helpers/rails_error_dashboard/application_helper.rb
|
|
251
252
|
- app/helpers/rails_error_dashboard/backtrace_helper.rb
|
|
253
|
+
- app/helpers/rails_error_dashboard/i18n_helper.rb
|
|
254
|
+
- app/helpers/rails_error_dashboard/mailer_i18n_helper.rb
|
|
252
255
|
- app/helpers/rails_error_dashboard/overview_helper.rb
|
|
253
256
|
- app/helpers/rails_error_dashboard/user_agent_helper.rb
|
|
254
257
|
- app/jobs/rails_error_dashboard/add_issue_recurrence_comment_job.rb
|
|
@@ -256,6 +259,7 @@ files:
|
|
|
256
259
|
- app/jobs/rails_error_dashboard/async_error_logging_job.rb
|
|
257
260
|
- app/jobs/rails_error_dashboard/baseline_alert_job.rb
|
|
258
261
|
- app/jobs/rails_error_dashboard/close_linked_issue_job.rb
|
|
262
|
+
- app/jobs/rails_error_dashboard/concerns/localized_job.rb
|
|
259
263
|
- app/jobs/rails_error_dashboard/create_issue_job.rb
|
|
260
264
|
- app/jobs/rails_error_dashboard/discord_error_notification_job.rb
|
|
261
265
|
- app/jobs/rails_error_dashboard/email_error_notification_job.rb
|
|
@@ -331,6 +335,17 @@ files:
|
|
|
331
335
|
- app/views/rails_error_dashboard/errors/storms.html.erb
|
|
332
336
|
- app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb
|
|
333
337
|
- app/views/rails_error_dashboard/errors/user_impact.html.erb
|
|
338
|
+
- config/locales/de.yml
|
|
339
|
+
- config/locales/en.yml
|
|
340
|
+
- config/locales/es.yml
|
|
341
|
+
- config/locales/fr.yml
|
|
342
|
+
- config/locales/it.yml
|
|
343
|
+
- config/locales/ja.yml
|
|
344
|
+
- config/locales/pl.yml
|
|
345
|
+
- config/locales/pt-BR.yml
|
|
346
|
+
- config/locales/ru.yml
|
|
347
|
+
- config/locales/uk.yml
|
|
348
|
+
- config/locales/zh-CN.yml
|
|
334
349
|
- config/routes.rb
|
|
335
350
|
- db/development.sqlite3
|
|
336
351
|
- db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb
|
|
@@ -404,9 +419,11 @@ files:
|
|
|
404
419
|
- lib/rails_error_dashboard/commands/upsert_cascade_pattern.rb
|
|
405
420
|
- lib/rails_error_dashboard/configuration.rb
|
|
406
421
|
- lib/rails_error_dashboard/configuration_error.rb
|
|
422
|
+
- lib/rails_error_dashboard/current.rb
|
|
407
423
|
- lib/rails_error_dashboard/engine.rb
|
|
408
424
|
- lib/rails_error_dashboard/error_reporter.rb
|
|
409
425
|
- lib/rails_error_dashboard/helpers/user_model_detector.rb
|
|
426
|
+
- lib/rails_error_dashboard/i18n_store.rb
|
|
410
427
|
- lib/rails_error_dashboard/integrations/llm_middleware.rb
|
|
411
428
|
- lib/rails_error_dashboard/integrations/llm_span_processor.rb
|
|
412
429
|
- lib/rails_error_dashboard/integrations/o_tel.rb
|
|
@@ -420,6 +437,7 @@ files:
|
|
|
420
437
|
- lib/rails_error_dashboard/plugins/audit_log_plugin.rb
|
|
421
438
|
- lib/rails_error_dashboard/plugins/jira_integration_plugin.rb
|
|
422
439
|
- lib/rails_error_dashboard/plugins/metrics_plugin.rb
|
|
440
|
+
- lib/rails_error_dashboard/private_backend.rb
|
|
423
441
|
- lib/rails_error_dashboard/queries/action_cable_summary.rb
|
|
424
442
|
- lib/rails_error_dashboard/queries/active_storage_summary.rb
|
|
425
443
|
- lib/rails_error_dashboard/queries/analytics_stats.rb
|
|
@@ -481,6 +499,7 @@ files:
|
|
|
481
499
|
- lib/rails_error_dashboard/services/llm_cost_estimator.rb
|
|
482
500
|
- lib/rails_error_dashboard/services/llm_summary.rb
|
|
483
501
|
- lib/rails_error_dashboard/services/local_variable_capturer.rb
|
|
502
|
+
- lib/rails_error_dashboard/services/localized_time_formatter.rb
|
|
484
503
|
- lib/rails_error_dashboard/services/markdown_error_formatter.rb
|
|
485
504
|
- lib/rails_error_dashboard/services/n_plus_one_detector.rb
|
|
486
505
|
- lib/rails_error_dashboard/services/notification_helpers.rb
|
|
@@ -513,6 +532,7 @@ files:
|
|
|
513
532
|
- lib/rails_error_dashboard/subscribers/llm_call_subscriber.rb
|
|
514
533
|
- lib/rails_error_dashboard/subscribers/rack_attack_subscriber.rb
|
|
515
534
|
- lib/rails_error_dashboard/test_error.rb
|
|
535
|
+
- lib/rails_error_dashboard/translation.rb
|
|
516
536
|
- lib/rails_error_dashboard/value_objects/error_context.rb
|
|
517
537
|
- lib/rails_error_dashboard/value_objects/llm_call_event.rb
|
|
518
538
|
- lib/rails_error_dashboard/version.rb
|
|
@@ -530,7 +550,7 @@ metadata:
|
|
|
530
550
|
funding_uri: https://github.com/sponsors/AnjanJ
|
|
531
551
|
post_install_message: |
|
|
532
552
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
533
|
-
RED (Rails Error Dashboard) v0.
|
|
553
|
+
RED (Rails Error Dashboard) v0.9.0
|
|
534
554
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
535
555
|
|
|
536
556
|
First install:
|