Module: RailsErrorDashboard::Services::NotificationHelpers

Defined in:
lib/rails_error_dashboard/services/notification_helpers.rb

Overview

Shared helper methods for notification payload builders

Pure functions: no side effects, no HTTP calls, no database access. Used by all notification payload builders to avoid duplication.

Class Method Summary collapse

Class Method Details

.app_name(error_log) ⇒ String

Application name for notifications

Parameters:

Returns:

  • (String)

    Application name or "Unknown"



178
179
180
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 178

def app_name(error_log)
  error_log.application&.name || "Unknown"
end

.dashboard_url(error_log) ⇒ String

Generate dashboard URL for an error

Parameters:

Returns:

  • (String)

    Full URL to the error detail page



90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 90

def dashboard_url(error_log)
  base_url = (RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000").chomp("/")
  mount_path = RailsErrorDashboard.configuration.engine_mount_path

  # Avoid doubling the mount path when base_url already includes it.
  # e.g., base_url="https://app.com/red" + mount_path="/red" → don't produce "/red/red"
  if mount_path.present? && base_url.end_with?(mount_path.chomp("/"))
    "#{base_url}/errors/#{error_log.id}"
  else
    "#{base_url}#{mount_path}/errors/#{error_log.id}"
  end
end

.error_source(error_log) ⇒ String

Error source description for PagerDuty

Parameters:

Returns:

  • (String)

    Source description



185
186
187
188
189
190
191
192
193
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 185

def error_source(error_log)
  if error_log.controller_name && error_log.action_name
    "#{error_log.controller_name}##{error_log.action_name}"
  elsif error_log.request_url
    error_log.request_url
  else
    error_log.platform || "Rails Application"
  end
end

.extract_backtrace(backtrace, limit = 20) ⇒ Array<String>

Extract backtrace lines as an array

Parameters:

  • backtrace (String, Array, nil)

    Raw backtrace

  • limit (Integer) (defaults to: 20)

    Maximum lines to extract (default 20)

Returns:

  • (Array<String>)

    Backtrace lines



116
117
118
119
120
121
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 116

def extract_backtrace(backtrace, limit = 20)
  return [] if backtrace.nil?

  lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
  lines.first(limit).map(&:strip)
end

.extract_first_backtrace_line(backtrace, length = 100, locale: nil) ⇒ String

Extract first backtrace line (truncated)

The line itself is diagnostic output and is never translated — only the "N/A" shown in its absence is, and only when a locale is supplied. Callers that pass no locale keep the literal, so existing non-i18n callers are unaffected.

Parameters:

  • backtrace (String, Array, nil)

    Raw backtrace

  • length (Integer) (defaults to: 100)

    Maximum length (default 100)

  • locale (String, nil) (defaults to: nil)

    locale for the placeholder

Returns:

  • (String)

    First line, or the "N/A" placeholder



134
135
136
137
138
139
140
141
142
143
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 134

def extract_first_backtrace_line(backtrace, length = 100, locale: nil)
  placeholder = locale ? not_available(locale) : "N/A"
  return placeholder if backtrace.nil?

  lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
  first_line = lines.first&.strip

  return placeholder if first_line.nil?
  first_line.length > length ? "#{first_line[0...length]}..." : first_line
end

.field(label, value, locale) ⇒ Object

A "Label:\nvalue" Slack mrkdwn field.

The bold-label-newline-value shape is Slack markup, not language, so it stays here rather than being baked into each translation — a translator editing "%label:" could break the formatting for every field at once. Only the label itself is translated.

Parameters:

  • label (Symbol)

    a key under red.notifications.error_alert.labels



34
35
36
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 34

def field(label, value, locale)
  "*#{t("red.notifications.error_alert.labels.#{label}", locale)}:*\n#{value}"
end

.format_datetime(time, locale) ⇒ String

A human-readable timestamp for a notification body.

Uses the locale's date format and month names rather than strftime's, which are always English (see MailerI18nHelper for the same problem). Falls back to the machine format if anything goes wrong — a wrong-looking timestamp is better than a lost notification.

Returns:

  • (String)

    "" for nil



66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 66

def format_datetime(time, locale)
  return "" if time.nil?

  utc = time.respond_to?(:utc) ? time.utc : time
  pattern = I18nStore.translate("red.time.formats.full", locale: locale)
  # A miss returns humanized key text, which would be a nonsense strftime
  # pattern. Detect it the same way red_time_format does.
  pattern = I18nStore.translate("red.time.formats.full", locale: I18nStore::DEFAULT_LOCALE) unless pattern.include?("%")

  LocalizedTimeFormatter.call(utc, pattern: pattern, locale: locale)
rescue StandardError
  format_time(time)
end

.format_datetime_or_na(time, locale) ⇒ Object

A localized timestamp, or the localized "N/A" for nil.



81
82
83
84
85
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 81

def format_datetime_or_na(time, locale)
  return not_available(locale) if time.nil?

  format_datetime(time, locale)
end

.format_time(time) ⇒ String

Format time for display

Parameters:

  • time (Time, nil)

    Time to format

Returns:

  • (String)

    Formatted time or "N/A"



160
161
162
163
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 160

def format_time(time)
  return "N/A" if time.nil?
  time.strftime("%Y-%m-%d %H:%M:%S UTC")
end

.label(label, locale) ⇒ Object

One label from red.notifications.error_alert.labels.

Discord wants a bare field name, Slack wants it wrapped in its mrkdwn bold-and-newline shape — hence this and #field rather than one method.

Parameters:

  • label (Symbol)


44
45
46
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 44

def label(label, locale)
  t("red.notifications.error_alert.labels.#{label}", locale)
end

.not_available(locale) ⇒ String

Returns the localized "N/A" placeholder.

Returns:

  • (String)

    the localized "N/A" placeholder



54
55
56
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 54

def not_available(locale)
  t("red.notifications.shared.not_available", locale)
end

.parse_request_params(params_json) ⇒ Hash

Parse request params JSON safely

Parameters:

  • params_json (String, nil)

    JSON string

Returns:

  • (Hash)

    Parsed params or empty hash



168
169
170
171
172
173
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 168

def parse_request_params(params_json)
  return {} if params_json.nil?
  JSON.parse(params_json)
rescue JSON::ParserError
  {}
end

.platform_emoji(platform) ⇒ String

Platform emoji for Slack/text notifications

Parameters:

  • platform (String, nil)

    Platform name

Returns:

  • (String)

    Emoji



148
149
150
151
152
153
154
155
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 148

def platform_emoji(platform)
  case platform&.downcase
  when "ios" then "📱"
  when "android" then "🤖"
  when "api" then "🔌"
  else "💻"
  end
end

.t(key, locale, **options) ⇒ String

Translate for a notification payload.

Positional locale rather than the ambient one: a payload is built inside a job, where Current.locale is nil at best and an unrelated request's value at worst (P4-T1). Every builder threads the locale it was handed.

No escaping — these values go into JSON, not HTML.

Returns:

  • (String)

    never nil, never raises



22
23
24
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 22

def t(key, locale, **options)
  I18nStore.translate(key, locale: locale, **options)
end

.truncate_message(message, length = 500) ⇒ String

Truncate a message to a maximum length

Parameters:

  • message (String, nil)

    The message to truncate

  • length (Integer) (defaults to: 500)

    Maximum length (default 500)

Returns:

  • (String)

    Truncated message



107
108
109
110
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 107

def truncate_message(message, length = 500)
  return "" unless message
  message.length > length ? "#{message[0...length]}..." : message
end

.unknown(locale) ⇒ String

Returns the localized "Unknown" placeholder.

Returns:

  • (String)

    the localized "Unknown" placeholder



49
50
51
# File 'lib/rails_error_dashboard/services/notification_helpers.rb', line 49

def unknown(locale)
  t("red.notifications.shared.unknown", locale)
end