rails_error_dashboard 0.1.29 → 0.1.30

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -6
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
  5. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
  6. data/app/models/rails_error_dashboard/application.rb +1 -1
  7. data/app/models/rails_error_dashboard/error_log.rb +44 -16
  8. data/app/views/layouts/rails_error_dashboard.html.erb +66 -1237
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
  10. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
  11. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
  12. data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
  13. data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
  14. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
  15. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
  16. data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
  17. data/app/views/rails_error_dashboard/errors/show.html.erb +44 -20
  18. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
  19. data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
  20. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
  21. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
  22. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
  23. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
  24. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
  25. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
  26. data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
  27. data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
  28. data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
  29. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
  30. data/db/migrate/20251226020100_create_error_comments.rb +3 -0
  31. data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
  32. data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
  33. data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
  34. data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
  35. data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
  36. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
  37. data/lib/rails_error_dashboard/configuration.rb +160 -3
  38. data/lib/rails_error_dashboard/configuration_error.rb +24 -0
  39. data/lib/rails_error_dashboard/engine.rb +17 -0
  40. data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
  41. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
  42. data/lib/rails_error_dashboard/queries/errors_list.rb +20 -8
  43. data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
  44. data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
  45. data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
  46. data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
  47. data/lib/rails_error_dashboard/version.rb +1 -1
  48. data/lib/rails_error_dashboard.rb +6 -0
  49. metadata +13 -10
  50. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
  51. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
  52. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
  53. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
  54. data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
  55. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
  56. data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
  57. data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c2f62f1af545cfebb6eb7356950d1ef5044ca767f7f0d0f8d048b9f8cedc16f
4
- data.tar.gz: 95239d1ecadcf74c334b6af8269b3e2fbf2fc27f8b679a91f537f9c81d2ab363
3
+ metadata.gz: d9a0ec7ce2177fab0bbbd7103440111b9d737add4281bd85e651e219b032242a
4
+ data.tar.gz: c7edac468fff5d5318d78f384b460b1b9cf1ddbc50373ad71764c5675e33c67e
5
5
  SHA512:
6
- metadata.gz: 867adf79f0eebb0dd5ab6f4ecc417546af088a5ececc76595326a8d1970b1593e2eaca52023c48f5441ebd788daad167d577223500b0f5adbb07b24ef6d0e581
7
- data.tar.gz: f626ae8fb589b8ddf4a48385c910e52de87d17218f18b62fdf4f29837a37651b8eb103839a8e4a548b6ed1df0ea773e0870ede59550f8ee1b9cf0dfdce958559
6
+ metadata.gz: 166ed8da84a2ef05fc69b1e4352e4f108300b34ec78b75b7685d41622a07e1dfe45113556f27f55756002d0af93d2ae6fd165683fcef9393562ab117fe02aa0d
7
+ data.tar.gz: 79f5193644e2754c9f4f49e6d8f03edd463d91a7e021d23f9b02a02f8e412d748666a9b2cc3e8ff843b821f926f781a4e60887cce5d01b6f9b96c597cea6d317
data/README.md CHANGED
@@ -143,6 +143,32 @@ Detect cyclical patterns (business hours, nighttime, weekend rhythms) and error
143
143
  **Plus: Developer Insights Dashboard** 💡
144
144
  Built-in analytics dashboard with severity detection, platform stability scoring, actionable recommendations, and recent error activity summaries (always available, no configuration needed).
145
145
 
146
+ #### 🔍 Source Code Integration (NEW!)
147
+
148
+ **View actual source code directly in error backtraces** - no need to switch to your editor or GitHub.
149
+
150
+ - **Inline Source Code Viewer** - Click "View Source" on any error frame to see the actual code with ±7 lines of context
151
+ - **Git Blame Integration** - See who last modified the code, when, and the commit message
152
+ - **Repository Links** - Jump directly to GitHub/GitLab/Bitbucket at the exact error line
153
+ - **Smart Caching** - Fast performance with 1-hour cache (configurable)
154
+ - **Security Controls** - Only shows your app code by default (not gems/frameworks)
155
+
156
+ **Perfect for debugging:**
157
+ - Understand the code context without leaving the dashboard
158
+ - Identify code ownership with git blame
159
+ - Quick navigation to your repository
160
+ - See recent changes that might have caused the error
161
+
162
+ ```ruby
163
+ # Enable in config/initializers/rails_error_dashboard.rb
164
+ config.enable_source_code_integration = true
165
+ config.source_code_context_lines = 7
166
+ config.enable_git_blame = true
167
+ config.git_repository_url = "https://github.com/user/repo"
168
+ ```
169
+
170
+ **📖 [Complete documentation →](docs/SOURCE_CODE_INTEGRATION.md)**
171
+
146
172
  #### 🔌 Plugin System
147
173
  Extensible architecture with event hooks (`on_error_logged`, `on_error_resolved`, `on_threshold_exceeded`). Built-in examples for Jira integration, metrics tracking, audit logging. Easy to create custom plugins - just drop a file in `config/initializers/error_dashboard_plugins/`.
148
174
 
@@ -763,15 +789,17 @@ See [Customization Guide](docs/CUSTOMIZATION.md).
763
789
  <details>
764
790
  <summary><strong>How long are errors stored?</strong></summary>
765
791
 
766
- Forever by default. Configure retention policy:
792
+ Forever by default (no automatic deletion). Manual cleanup with rake task:
767
793
 
768
- ```ruby
769
- RailsErrorDashboard.configure do |config|
770
- config.retention_days = 90 # Auto-delete after 90 days
771
- end
794
+ ```bash
795
+ # Delete resolved errors older than 90 days
796
+ rails error_dashboard:cleanup_resolved DAYS=90
797
+
798
+ # Filter by application name
799
+ rails error_dashboard:cleanup_resolved DAYS=30 APP_NAME="My App"
772
800
  ```
773
801
 
774
- Or clean up manually with rake tasks. See [Database Optimization](docs/guides/DATABASE_OPTIMIZATION.md).
802
+ Or schedule with cron/whenever. See [Database Optimization](docs/guides/DATABASE_OPTIMIZATION.md).
775
803
  </details>
776
804
 
777
805
  <details>
@@ -18,6 +18,7 @@ module RailsErrorDashboard
18
18
  frequency
19
19
  status
20
20
  assigned_to
21
+ assignee_name
21
22
  priority_level
22
23
  hide_snoozed
23
24
  sort_by
@@ -38,6 +39,18 @@ module RailsErrorDashboard
38
39
  @platform_scores = {}
39
40
  end
40
41
 
42
+ # Get correlation summary (if enabled, pass application filter)
43
+ if RailsErrorDashboard.configuration.enable_error_correlation
44
+ correlation = Queries::ErrorCorrelation.new(days: 7, application_id: @current_application_id)
45
+ @problematic_releases = correlation.problematic_releases.first(3)
46
+ @time_correlated_errors = correlation.time_correlated_errors.first(3)
47
+ @multi_error_users = correlation.multi_error_users(min_error_types: 2).first(5)
48
+ else
49
+ @problematic_releases = []
50
+ @time_correlated_errors = []
51
+ @multi_error_users = []
52
+ end
53
+
41
54
  # Get critical alerts (critical/high severity errors from last hour)
42
55
  # Filter by priority_level in database instead of loading all records into memory
43
56
  @critical_alerts = ErrorLog
@@ -62,6 +75,15 @@ module RailsErrorDashboard
62
75
  filter_options = Queries::FilterOptions.call(application_id: @current_application_id)
63
76
  @error_types = filter_options[:error_types]
64
77
  @platforms = filter_options[:platforms]
78
+
79
+ # Get all distinct assignees for the assignee filter dropdown
80
+ assignee_query = ErrorLog.where.not(assigned_to: nil)
81
+ # Filter by application if specified
82
+ assignee_query = assignee_query.where(application_id: @current_application_id) if @current_application_id.present?
83
+ @assignees = assignee_query.select(:assigned_to)
84
+ .distinct
85
+ .pluck(:assigned_to)
86
+ .sort
65
87
  end
66
88
 
67
89
  def show
@@ -74,13 +74,6 @@ module RailsErrorDashboard
74
74
  end
75
75
  end
76
76
 
77
- # Returns the current user name for filtering "My Errors"
78
- # Uses configured dashboard username or system username
79
- # @return [String] Current user identifier
80
- def current_user_name
81
- RailsErrorDashboard.configuration.dashboard_username || ENV["USER"] || "unknown"
82
- end
83
-
84
77
  # Returns a sanitized hash of filter params safe for query links
85
78
  # @param extra_keys [Array<Symbol>] Additional permitted keys for specific contexts
86
79
  # @return [Hash] Whitelisted params for building URLs
@@ -202,5 +195,84 @@ module RailsErrorDashboard
202
195
  }
203
196
  )
204
197
  end
198
+
199
+ # Automatically converts URLs in text to clickable links that open in new window
200
+ # Also highlights inline code wrapped in backticks with syntax highlighting
201
+ # Also converts file paths to GitHub links if repository URL is configured
202
+ # Supports http://, https://, and common patterns like github.com/user/repo
203
+ # @param text [String] The text containing URLs, file paths, and inline code
204
+ # @param error [RailsErrorDashboard::ErrorLog, nil] The error for context (to get repo URL)
205
+ # @return [String] HTML safe text with clickable links and styled code
206
+ def auto_link_urls(text, error: nil)
207
+ return "" if text.blank?
208
+
209
+ # Get repository URL from error's application or global config
210
+ repo_url = if error&.application&.repository_url.present?
211
+ error.application.repository_url
212
+ elsif RailsErrorDashboard.configuration.git_repository_url.present?
213
+ RailsErrorDashboard.configuration.git_repository_url
214
+ end
215
+
216
+ # First, protect inline code with backticks by replacing with placeholders
217
+ code_blocks = []
218
+ file_paths = []
219
+ text_with_placeholders = text.gsub(/`([^`]+)`/) do |match|
220
+ code_content = Regexp.last_match(1)
221
+
222
+ # Check if the code block contains a file path pattern
223
+ if repo_url && code_content =~ %r{^(app|lib|config|db|spec|test)/[^\s]+\.(rb|js|jsx|ts|tsx|erb|yml|yaml|json|css|scss)$}
224
+ # It's a file path - save it and mark for GitHub linking
225
+ file_paths << code_content
226
+ "###FILE_PATH_#{file_paths.length - 1}###"
227
+ else
228
+ # Regular code block
229
+ code_blocks << code_content
230
+ "###CODE_BLOCK_#{code_blocks.length - 1}###"
231
+ end
232
+ end
233
+
234
+ # Regex to match URLs (http://, https://, www., and common domains)
235
+ url_regex = %r{
236
+ (
237
+ (?:https?://|www\.) # http://, https://, or www.
238
+ (?:[^\s<>"]+) # Domain and path (no spaces, <, >, or ")
239
+ |
240
+ (?:^|\s) # Start of string or whitespace
241
+ (?:github\.com|gitlab\.com|bitbucket\.org|jira\.[^\s]+)
242
+ /[^\s<>"]+ # Path after domain
243
+ )
244
+ }xi
245
+
246
+ # Replace URLs with clickable links
247
+ linked_text = text_with_placeholders.gsub(url_regex) do |url|
248
+ # Clean up the URL
249
+ clean_url = url.strip
250
+
251
+ # Add protocol if missing
252
+ href = clean_url.start_with?("http://", "https://") ? clean_url : "https://#{clean_url}"
253
+
254
+ # Truncate display text for very long URLs
255
+ display_text = clean_url.length > 60 ? "#{clean_url[0..57]}..." : clean_url
256
+
257
+ "<a href=\"#{ERB::Util.html_escape(href)}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary text-decoration-underline\">#{ERB::Util.html_escape(display_text)}</a>"
258
+ end
259
+
260
+ # Restore file paths with GitHub links (elvish magic! 🧝‍♀️)
261
+ linked_text.gsub!(/###FILE_PATH_(\d+)###/) do
262
+ file_path = file_paths[Regexp.last_match(1).to_i]
263
+ github_url = "#{repo_url.chomp('/')}/blob/main/#{file_path}"
264
+ "<a href=\"#{ERB::Util.html_escape(github_url)}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-decoration-none\" title=\"View on GitHub\">" \
265
+ "<code class=\"inline-code-highlight file-path-link\">#{ERB::Util.html_escape(file_path)}</code></a>"
266
+ end
267
+
268
+ # Restore code blocks with styling
269
+ linked_text.gsub!(/###CODE_BLOCK_(\d+)###/) do
270
+ code_content = code_blocks[Regexp.last_match(1).to_i]
271
+ "<code class=\"inline-code-highlight\">#{ERB::Util.html_escape(code_content)}</code>"
272
+ end
273
+
274
+ # Preserve line breaks and return as HTML safe
275
+ simple_format(linked_text, {}, sanitize: false)
276
+ end
205
277
  end
206
278
  end
@@ -2,11 +2,60 @@
2
2
 
3
3
  module RailsErrorDashboard
4
4
  module BacktraceHelper
5
+ # Language mapping for syntax highlighting
6
+ LANGUAGE_MAP = {
7
+ ".rb" => "ruby",
8
+ ".js" => "javascript",
9
+ ".jsx" => "javascript",
10
+ ".ts" => "typescript",
11
+ ".tsx" => "typescript",
12
+ ".erb" => "erb",
13
+ ".html" => "html",
14
+ ".htm" => "html",
15
+ ".css" => "css",
16
+ ".scss" => "scss",
17
+ ".sass" => "scss",
18
+ ".yml" => "yaml",
19
+ ".yaml" => "yaml",
20
+ ".json" => "json",
21
+ ".sql" => "sql",
22
+ ".xml" => "xml",
23
+ ".py" => "python",
24
+ ".go" => "go",
25
+ ".java" => "java",
26
+ ".c" => "c",
27
+ ".cpp" => "cpp",
28
+ ".h" => "c",
29
+ ".hpp" => "cpp",
30
+ ".sh" => "bash",
31
+ ".bash" => "bash",
32
+ ".zsh" => "bash",
33
+ ".php" => "php",
34
+ ".pl" => "perl",
35
+ ".r" => "r",
36
+ ".rs" => "rust",
37
+ ".swift" => "swift",
38
+ ".kt" => "kotlin",
39
+ ".scala" => "scala",
40
+ ".clj" => "clojure",
41
+ ".ex" => "elixir",
42
+ ".exs" => "elixir"
43
+ }.freeze
44
+
5
45
  # Parse backtrace string into structured frames
6
46
  def parse_backtrace(backtrace_string)
7
47
  Services::BacktraceParser.parse(backtrace_string)
8
48
  end
9
49
 
50
+ # Detect programming language from file path
51
+ # Returns Highlight.js language identifier
52
+ def detect_language_from_path(file_path)
53
+ return "plaintext" unless file_path
54
+
55
+ ext = File.extname(file_path).downcase
56
+ LANGUAGE_MAP[ext] || "plaintext"
57
+ end
58
+
10
59
  # Filter to show only application code
11
60
  def filter_app_code(frames)
12
61
  frames.select { |f| f[:category] == :app }
@@ -87,5 +136,105 @@ module RailsErrorDashboard
87
136
  frames.group_by { |f| f[:category] }
88
137
  .transform_values(&:count)
89
138
  end
139
+
140
+ # Read source code for a backtrace frame
141
+ # Returns hash with { lines:, error: } or nil
142
+ def read_source_code(frame, context: 5)
143
+ return nil unless RailsErrorDashboard.configuration.enable_source_code_integration
144
+ return nil unless frame[:file_path] && frame[:line_number]
145
+
146
+ # Cache key includes file path, line number, and git SHA if available
147
+ cache_key = "source_code/#{frame[:file_path]}/#{frame[:line_number]}"
148
+ cache_ttl = RailsErrorDashboard.configuration.source_code_cache_ttl || 3600
149
+
150
+ Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
151
+ context_lines = RailsErrorDashboard.configuration.source_code_context_lines || 5
152
+ reader = Services::SourceCodeReader.new(frame[:file_path], frame[:line_number])
153
+ lines = reader.read_lines(context: context_lines)
154
+
155
+ if lines
156
+ {
157
+ lines: lines,
158
+ language: detect_language_from_path(frame[:file_path]),
159
+ error: nil
160
+ }
161
+ else
162
+ {
163
+ lines: nil,
164
+ language: nil,
165
+ error: reader.error
166
+ }
167
+ end
168
+ end
169
+ end
170
+
171
+ # Read git blame for a backtrace frame
172
+ # Returns blame data hash or nil
173
+ def read_git_blame(frame)
174
+ return nil unless RailsErrorDashboard.configuration.enable_source_code_integration
175
+ return nil unless RailsErrorDashboard.configuration.enable_git_blame
176
+ return nil unless frame[:file_path] && frame[:line_number]
177
+
178
+ # Cache key includes file path and line number
179
+ cache_key = "git_blame/#{frame[:file_path]}/#{frame[:line_number]}"
180
+ cache_ttl = RailsErrorDashboard.configuration.source_code_cache_ttl || 3600
181
+
182
+ Rails.cache.fetch(cache_key, expires_in: cache_ttl) do
183
+ reader = Services::GitBlameReader.new(frame[:file_path], frame[:line_number])
184
+ reader.read_blame
185
+ end
186
+ end
187
+
188
+ # Generate GitHub/GitLab/Bitbucket link for a frame
189
+ # Returns URL string or nil
190
+ def generate_repository_link(frame, error_log)
191
+ return nil unless RailsErrorDashboard.configuration.enable_source_code_integration
192
+ return nil unless RailsErrorDashboard.configuration.git_repository_url.present?
193
+ return nil unless frame[:file_path] && frame[:line_number]
194
+
195
+ repo_url = RailsErrorDashboard.configuration.git_repository_url
196
+ commit_sha = determine_commit_sha(error_log)
197
+
198
+ generator = Services::GithubLinkGenerator.new(
199
+ repository_url: repo_url,
200
+ file_path: frame[:file_path],
201
+ line_number: frame[:line_number],
202
+ commit_sha: commit_sha
203
+ )
204
+
205
+ generator.generate_link
206
+ end
207
+
208
+ private
209
+
210
+ # Determine which commit SHA to use based on strategy
211
+ def determine_commit_sha(error_log)
212
+ strategy = RailsErrorDashboard.configuration.git_branch_strategy || :commit_sha
213
+
214
+ case strategy
215
+ when :commit_sha
216
+ # Use the SHA from when the error occurred (most accurate)
217
+ error_log.respond_to?(:git_sha) ? error_log.git_sha : nil
218
+ when :current_branch
219
+ # Use current branch HEAD
220
+ get_current_git_sha
221
+ when :main
222
+ # Use main/master branch
223
+ nil # Will default to "main" in GithubLinkGenerator
224
+ else
225
+ nil
226
+ end
227
+ end
228
+
229
+ # Get current git SHA from repository
230
+ def get_current_git_sha
231
+ return @current_git_sha if defined?(@current_git_sha)
232
+
233
+ @current_git_sha = begin
234
+ `git rev-parse HEAD 2>/dev/null`.strip.presence
235
+ rescue StandardError
236
+ nil
237
+ end
238
+ end
90
239
  end
91
240
  end
@@ -1,5 +1,5 @@
1
1
  module RailsErrorDashboard
2
- class Application < ActiveRecord::Base
2
+ class Application < ErrorLogsRecord
3
3
  self.table_name = "rails_error_dashboard_applications"
4
4
 
5
5
  # Associations
@@ -4,6 +4,15 @@ module RailsErrorDashboard
4
4
  class ErrorLog < ErrorLogsRecord
5
5
  self.table_name = "rails_error_dashboard_error_logs"
6
6
 
7
+ # Priority level constants
8
+ # Using industry standard: P0 = Critical (highest), P3 = Low (lowest)
9
+ PRIORITY_LEVELS = {
10
+ 3 => { label: "Critical", short_label: "P0", color: "danger", emoji: "🔴" },
11
+ 2 => { label: "High", short_label: "P1", color: "warning", emoji: "🟠" },
12
+ 1 => { label: "Medium", short_label: "P2", color: "info", emoji: "🟡" },
13
+ 0 => { label: "Low", short_label: "P3", color: "secondary", emoji: "⚪" }
14
+ }.freeze
15
+
7
16
  # Application association
8
17
  belongs_to :application, optional: false
9
18
 
@@ -94,11 +103,17 @@ module RailsErrorDashboard
94
103
  # Includes controller/action/application for better context-aware grouping
95
104
  # Per-app deduplication: same error in App A vs App B creates separate records
96
105
  def generate_error_hash
97
- # Hash based on error class, normalized message, first stack frame, controller, action, and application
106
+ # Use smart normalization to improve error grouping accuracy
107
+ normalized_message = Services::ErrorNormalizer.normalize(message)
108
+
109
+ # Extract significant backtrace frames (skips gem/vendor code)
110
+ significant_frames = Services::ErrorNormalizer.extract_significant_frames(backtrace, count: 3)
111
+
112
+ # Hash based on error class, normalized message, significant frames, controller, action, and application
98
113
  digest_input = [
99
114
  error_type,
100
- message&.gsub(/\d+/, "N")&.gsub(/"[^"]*"/, '""'), # Normalize numbers and strings
101
- backtrace&.lines&.first&.split(":")&.first, # Just the file, not line number
115
+ normalized_message,
116
+ significant_frames,
102
117
  controller_name, # Controller context
103
118
  action_name, # Action context
104
119
  application_id.to_s # Application context (for per-app deduplication)
@@ -283,22 +298,35 @@ module RailsErrorDashboard
283
298
 
284
299
  # Priority methods
285
300
  def priority_label
286
- case priority_level
287
- when 3 then "Critical"
288
- when 2 then "High"
289
- when 1 then "Medium"
290
- when 0 then "Low"
291
- else "Unset"
292
- end
301
+ priority_data = PRIORITY_LEVELS[priority_level]
302
+ return "Unset" unless priority_data
303
+
304
+ "#{priority_data[:label]} (#{priority_data[:short_label]})"
293
305
  end
294
306
 
295
307
  def priority_color
296
- case priority_level
297
- when 3 then "danger" # Critical = red
298
- when 2 then "warning" # High = orange
299
- when 1 then "info" # Medium = blue
300
- when 0 then "secondary" # Low = gray
301
- else "light"
308
+ priority_data = PRIORITY_LEVELS[priority_level]
309
+ return "light" unless priority_data
310
+
311
+ priority_data[:color]
312
+ end
313
+
314
+ def priority_emoji
315
+ priority_data = PRIORITY_LEVELS[priority_level]
316
+ return "" unless priority_data
317
+
318
+ priority_data[:emoji]
319
+ end
320
+
321
+ # Class method to get priority options for select dropdowns
322
+ def self.priority_options(include_emoji: false)
323
+ PRIORITY_LEVELS.sort_by { |level, _| -level }.map do |level, data|
324
+ label = if include_emoji
325
+ "#{data[:emoji]} #{data[:label]} (#{data[:short_label]})"
326
+ else
327
+ "#{data[:label]} (#{data[:short_label]})"
328
+ end
329
+ [ label, level ]
302
330
  end
303
331
  end
304
332