Module: RailsErrorDashboard::ApplicationHelper

Defined in:
app/helpers/rails_error_dashboard/application_helper.rb

Instance Method Summary collapse

Instance Method Details

#app_contextHash

Returns the current application context param for preserving app selection across navigation. Use this in link helpers: errors_path(app_context) or error_path(error, **app_context)

Returns:

  • (Hash)

    { application_id: X } if an app is selected, empty hash otherwise



117
118
119
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 117

def app_context
  params[:application_id].present? ? { application_id: params[:application_id] } : {}
end

Automatically converts URLs in text to clickable links that open in new window Also highlights inline code wrapped in backticks with syntax highlighting Also converts file paths to GitHub links if repository URL is configured Supports http://, https://, and common patterns like github.com/user/repo

Parameters:

  • text (String)

    The text containing URLs, file paths, and inline code

  • error (RailsErrorDashboard::ErrorLog, nil) (defaults to: nil)

    The error for context (to get repo URL)

Returns:

  • (String)

    HTML safe text with clickable links and styled code



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 286

def auto_link_urls(text, error: nil)
  return "" if text.blank?

  # SECURITY: escape HTML special chars in the input before any further
  # processing. simple_format(..., sanitize: false) at the end means
  # whatever survives this pipeline is rendered as raw HTML; any unescaped
  # `<script>` or `<img onerror=>` in the user's text would XSS.
  # We only intentionally inject our own <a> / <code> tags after this point.
  text = ERB::Util.html_escape(text)

  # Get repository URL from error's application or global config
  repo_url = if error&.application.respond_to?(:repository_url) && error.application.repository_url.present?
    error.application.repository_url
  elsif RailsErrorDashboard.configuration.git_repository_url.present?
    RailsErrorDashboard.configuration.git_repository_url
  end

  # First, protect inline code with backticks by replacing with placeholders
  code_blocks = []
  file_paths = []
  text_with_placeholders = text.gsub(/`([^`]+)`/) do |match|
    code_content = Regexp.last_match(1)

    # Check if the code block contains a file path pattern
    if repo_url && code_content =~ %r{^(app|lib|config|db|spec|test)/[^\s]+\.(rb|js|jsx|ts|tsx|erb|yml|yaml|json|css|scss)$}
      # It's a file path - save it and mark for GitHub linking
      file_paths << code_content
      "###FILE_PATH_#{file_paths.length - 1}###"
    else
      # Regular code block
      code_blocks << code_content
      "###CODE_BLOCK_#{code_blocks.length - 1}###"
    end
  end

  # Regex to match URLs (http://, https://, www., and common domains)
  url_regex = %r{
    (
      (?:https?://|www\.)           # http://, https://, or www.
      (?:[^\s<>"]+)                 # Domain and path (no spaces, <, >, or ")
      |
      (?:^|\s)                      # Start of string or whitespace
      (?:github\.com|gitlab\.com|bitbucket\.org|jira\.[^\s]+)
      /[^\s<>"]+                    # Path after domain
    )
  }xi

  # Replace URLs with clickable links. The url, code_content, and file_path
  # values below are slices of `text` which we already escaped at function
  # entry, so we don't re-escape them here (would double-escape `&amp;`,
  # breaking visual rendering). repo_url is from config so we escape it
  # explicitly before interpolating.
  escaped_repo_url = repo_url ? ERB::Util.html_escape(repo_url.chomp("/")) : nil

  linked_text = text_with_placeholders.gsub(url_regex) do |url|
    clean_url = url.strip

    href = clean_url.start_with?("http://", "https://") ? clean_url : "https://#{clean_url}"

    display_text = clean_url.length > 60 ? "#{clean_url[0..57]}..." : clean_url

    "<a href=\"#{href}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary text-decoration-underline\">#{display_text}</a>"
  end

  # Restore file paths with GitHub links
  linked_text.gsub!(/###FILE_PATH_(\d+)###/) do
    file_path = file_paths[Regexp.last_match(1).to_i]
    "<a href=\"#{escaped_repo_url}/blob/main/#{file_path}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-decoration-none\" title=\"View on GitHub\">" \
    "<code class=\"inline-code-highlight file-path-link\">#{file_path}</code></a>"
  end

  # Restore code blocks with styling
  linked_text.gsub!(/###CODE_BLOCK_(\d+)###/) do
    code_content = code_blocks[Regexp.last_match(1).to_i]
    "<code class=\"inline-code-highlight\">#{code_content}</code>"
  end

  # Preserve line breaks and return as HTML safe
  simple_format(linked_text, {}, sanitize: false)
end

Returns Bootstrap badge color class for breadcrumb category

Parameters:

  • category (String)

    Breadcrumb category (sql, controller, cache, job, mailer, custom)

Returns:

  • (String)

    Bootstrap color class



257
258
259
260
261
262
263
264
265
266
267
268
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 257

def breadcrumb_badge_color(category)
  case category.to_s
  when "sql"        then "primary"
  when "controller" then "success"
  when "cache"      then "info"
  when "job"        then "warning"
  when "mailer"     then "secondary"
  when "custom"     then "dark"
  when "deprecation" then "danger"
  else "light"
  end
end

#extract_table_from_sql(sql) ⇒ String?

Extracts table name from a SQL query string

Parameters:

  • sql (String)

    SQL query (e.g., ‘SELECT “users”.* FROM “users” WHERE …’)

Returns:

  • (String, nil)

    The table name or nil if not extractable



273
274
275
276
277
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 273

def extract_table_from_sql(sql)
  return nil if sql.blank?
  match = sql.match(/FROM\s+["`]?(\w+)["`]?/i)
  match ? match[1] : nil
end

Generates a link to a git commit if repository URL is configured

Parameters:

  • git_sha (String)

    The git commit SHA

  • short (Boolean) (defaults to: true)

    Whether to show short SHA (7 chars) or full SHA

Returns:

  • (String)

    HTML safe link to commit or plain text if no repo configured



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 165

def git_commit_link(git_sha, short: true)
  return "" if git_sha.blank?

  config = RailsErrorDashboard.configuration
  display_sha = short ? git_sha[0..6] : git_sha

  if config.git_repository_url.present?
    # Support GitHub, GitLab, Bitbucket URL formats
    commit_url = "#{config.git_repository_url.chomp("/")}/commit/#{git_sha}"
    link_to display_sha, commit_url, class: "text-decoration-none font-monospace", target: "_blank", rel: "noopener"
  else
    (:code, display_sha, class: "font-monospace")
  end
end

#js_safe_json(value) ⇒ Object

Serialize a value to JSON safely for inlining inside a <script> block. Ruby’s #to_json escapes JSON special chars but does NOT escape “</” — a value containing the literal string “</script>” would break out of the surrounding <script> tag. Replace “</” with “</” (semantically equivalent in JSON and in JavaScript string literals) to neutralize the close tag. Returns html_safe for direct interpolation into a script body.



36
37
38
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 36

def js_safe_json(value)
  value.to_json.gsub("</", '<\/').html_safe
end

#local_time(time, format: :full, fallback: "N/A") ⇒ String

Renders a timestamp that will be automatically converted to user’s local timezone Server sends UTC timestamp, JavaScript converts to local timezone on page load

Parameters:

  • time (Time, DateTime, nil)

    The timestamp to display

  • format (Symbol) (defaults to: :full)

    Format preset (:full, :short, :date_only, :time_only, :datetime)

  • fallback (String) (defaults to: "N/A")

    Text to show if time is nil

Returns:

  • (String)

    HTML safe span with data attributes for JS conversion



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 186

def local_time(time, format: :full, fallback: "N/A")
  return fallback if time.nil?

  # Convert to UTC if not already
  utc_time = time.respond_to?(:utc) ? time.utc : time

  # ISO 8601 format for JavaScript parsing
  iso_time = utc_time.iso8601

  # Format presets for data-format attribute
  format_string = case format
  when :full
    "%B %d, %Y %I:%M:%S %p"  # December 31, 2024 11:59:59 PM
  when :short
    "%m/%d %I:%M%p"          # 12/31 11:59PM
  when :date_only
    "%B %d, %Y"              # December 31, 2024
  when :time_only
    "%I:%M:%S %p"            # 11:59:59 PM
  when :datetime
    "%b %d, %Y %H:%M"        # Dec 31, 2024 23:59
  else
    format.to_s
  end

  (
    :span,
    utc_time.strftime(format_string + " UTC"),  # Fallback for non-JS browsers
    class: "local-time",
    data: {
      utc: iso_time,
      format: format_string
    }
  )
end

#local_time_ago(time, fallback: "N/A") ⇒ String

Renders a relative time (“3 hours ago”) that updates automatically

Parameters:

  • time (Time, DateTime, nil)

    The timestamp to display

  • fallback (String) (defaults to: "N/A")

    Text to show if time is nil

Returns:

  • (String)

    HTML safe span with data attributes for JS conversion



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 237

def local_time_ago(time, fallback: "N/A")
  return fallback if time.nil?

  # Convert to UTC if not already
  utc_time = time.respond_to?(:utc) ? time.utc : time
  iso_time = utc_time.iso8601

  (
    :span,
    time_ago_in_words(time) + " ago",  # Fallback for non-JS browsers
    class: "local-time-ago",
    data: {
      utc: iso_time
    }
  )
end

#parse_pg_timestamp(value) ⇒ Object

Raw connection.select_all returns timestamps as Time in some PG configs and as ISO8601 strings in others — accept both shapes.



224
225
226
227
228
229
230
231
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 224

def parse_pg_timestamp(value)
  return nil if value.blank?
  return value if value.is_a?(Time) || value.is_a?(DateTime)

  Time.parse(value.to_s)
rescue ArgumentError
  nil
end

#permitted_filter_params(extra_keys: []) ⇒ Hash

Returns a sanitized hash of filter params safe for query links

Parameters:

  • extra_keys (Array<Symbol>) (defaults to: [])

    Additional permitted keys for specific contexts

Returns:

  • (Hash)

    Whitelisted params for building URLs



124
125
126
127
128
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 124

def permitted_filter_params(extra_keys: [])
  base_keys = RailsErrorDashboard::ErrorsController::FILTERABLE_PARAMS + %i[page per_page days]
  allowed_keys = base_keys + Array(extra_keys)
  params.permit(*allowed_keys).to_h.symbolize_keys
end

#platform_color_var(platform) ⇒ String

Returns platform-specific color class

Parameters:

  • platform (String)

    Platform name (ios, android, web, api)

Returns:

  • (String)

    CSS color variable



81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 81

def platform_color_var(platform)
  case platform&.downcase
  when "ios"
    "var(--platform-ios)"
  when "android"
    "var(--platform-android)"
  when "web"
    "var(--platform-web)"
  when "api"
    "var(--platform-api)"
  else
    "var(--text-color)"
  end
end

#platform_icon(platform) ⇒ String

Returns platform icon

Parameters:

  • platform (String)

    Platform name (ios, android, web, api)

Returns:

  • (String)

    Bootstrap icon class



99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 99

def platform_icon(platform)
  case platform&.downcase
  when "ios"
    "bi-apple"
  when "android"
    "bi-android2"
  when "web"
    "bi-globe"
  when "api"
    "bi-server"
  else
    "bi-question-circle"
  end
end

#red_csp_nonceObject

Returns the host app’s CSP nonce (if any) so inline <script> tags pass strict CSP. Falls back to nil when the host has no CSP configured — in that case the script tag works without a nonce attribute. Strict CSPs (script-src ‘self’ ‘nonce-…’) require this; without it the script is blocked.



7
8
9
10
11
12
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 7

def red_csp_nonce
  return nil unless respond_to?(:content_security_policy_nonce)
  content_security_policy_nonce
rescue StandardError
  nil
end

#red_javascript_tag(&block) ⇒ Object

Wraps an inline <script> block with the host app’s CSP nonce when available. Use everywhere we have <script>…</script> in our views so they pass strict CSP.

<%= red_javascript_tag do %>
  console.log('hi');
<% end %>


20
21
22
23
24
25
26
27
28
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 20

def red_javascript_tag(&block)
  nonce = red_csp_nonce
  content = capture(&block)
  if nonce
    (:script, content.html_safe, nonce: nonce)
  else
    (:script, content.html_safe)
  end
end

#severity_color(severity) ⇒ String

Returns Bootstrap color class for error severity Uses Catppuccin Mocha colors in dark theme via CSS variables

Parameters:

  • severity (Symbol)

    The severity level (:critical, :high, :medium, :low, :info)

Returns:

  • (String)

    Bootstrap color class (danger, warning, info, secondary)



44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 44

def severity_color(severity)
  case severity&.to_sym
  when :critical
    "danger"   # Maps to --ctp-red in dark mode
  when :high
    "warning"  # Maps to --ctp-peach in dark mode
  when :medium
    "info"     # Maps to --ctp-blue in dark mode
  when :low
    "secondary" # Maps to --ctp-overlay1 in dark mode
  else
    "secondary"
  end
end

#severity_color_var(severity) ⇒ String

Returns CSS variable for severity color (for inline styles) Useful when you need to set background-color or color directly

Parameters:

  • severity (Symbol)

    The severity level

Returns:

  • (String)

    CSS variable reference



63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 63

def severity_color_var(severity)
  case severity&.to_sym
  when :critical
    "var(--status-critical)"
  when :high
    "var(--status-warning)"
  when :medium
    "var(--status-info)"
  when :low
    "var(--text-tertiary)"
  else
    "var(--text-tertiary)"
  end
end

#sortable_header(label, column) ⇒ String

Generates a sortable column header link

Parameters:

  • label (String)

    The column label to display

  • column (String)

    The column name to sort by

Returns:

  • (String)

    HTML safe link with sort indicator



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'app/helpers/rails_error_dashboard/application_helper.rb', line 134

def sortable_header(label, column)
  current_sort = params[:sort_by]
  current_direction = params[:sort_direction] || "desc"

  # Determine new direction: if clicking same column, toggle; otherwise default to desc
  new_direction = if current_sort == column
    current_direction == "asc" ? "desc" : "asc"
  else
    "desc"
  end

  # Choose icon based on current state
  icon = if current_sort == column
    current_direction == "asc" ? "" : ""
  else
    ""  # Unsorted indicator
  end

  # Preserve whitelisted filter params while adding sort params
  link_params = permitted_filter_params.merge(sort_by: column, sort_direction: new_direction)

  link_to errors_path(link_params), class: "text-decoration-none" do
    (:span, "#{label} ", class: current_sort == column ? "fw-bold" : "") +
    (:span, icon, class: "text-muted small")
  end
end