rails_error_dashboard 0.1.1 → 0.1.3

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -21
  3. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
  4. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
  5. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
  6. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
  7. data/app/assets/stylesheets/rails_error_dashboard/application.css +926 -15
  8. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
  9. data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
  10. data/app/controllers/rails_error_dashboard/errors_controller.rb +94 -1
  11. data/app/helpers/rails_error_dashboard/application_helper.rb +42 -4
  12. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
  13. data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
  14. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
  15. data/app/models/rails_error_dashboard/error_comment.rb +27 -0
  16. data/app/models/rails_error_dashboard/error_log.rb +145 -0
  17. data/app/views/layouts/rails_error_dashboard.html.erb +796 -299
  18. data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
  19. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -0
  20. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +4 -4
  21. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
  22. data/app/views/rails_error_dashboard/errors/analytics.html.erb +138 -22
  23. data/app/views/rails_error_dashboard/errors/index.html.erb +83 -4
  24. data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
  25. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +29 -18
  26. data/app/views/rails_error_dashboard/errors/show.html.erb +353 -54
  27. data/config/routes.rb +7 -0
  28. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
  29. data/db/migrate/20251226020100_create_error_comments.rb +18 -0
  30. data/lib/generators/rails_error_dashboard/install/install_generator.rb +8 -2
  31. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +21 -0
  32. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
  33. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
  34. data/lib/rails_error_dashboard/commands/log_error.rb +47 -9
  35. data/lib/rails_error_dashboard/commands/resolve_error.rb +1 -1
  36. data/lib/rails_error_dashboard/configuration.rb +8 -0
  37. data/lib/rails_error_dashboard/error_reporter.rb +4 -4
  38. data/lib/rails_error_dashboard/logger.rb +105 -0
  39. data/lib/rails_error_dashboard/middleware/error_catcher.rb +2 -2
  40. data/lib/rails_error_dashboard/plugin.rb +3 -3
  41. data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
  42. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +1 -1
  43. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -1
  44. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +109 -1
  45. data/lib/rails_error_dashboard/queries/errors_list.rb +61 -6
  46. data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
  47. data/lib/rails_error_dashboard/version.rb +1 -1
  48. data/lib/rails_error_dashboard.rb +2 -0
  49. metadata +18 -2
  50. data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
@@ -71,6 +71,21 @@ module RailsErrorDashboard
71
71
  attributes[:backtrace_signature] = calculate_backtrace_signature_from_backtrace(truncated_backtrace)
72
72
  end
73
73
 
74
+ # Add git/release info if columns exist
75
+ if ErrorLog.column_names.include?("git_sha")
76
+ attributes[:git_sha] = RailsErrorDashboard.configuration.git_sha ||
77
+ ENV["GIT_SHA"] ||
78
+ ENV["HEROKU_SLUG_COMMIT"] ||
79
+ ENV["RENDER_GIT_COMMIT"] ||
80
+ detect_git_sha_from_command
81
+ end
82
+
83
+ if ErrorLog.column_names.include?("app_version")
84
+ attributes[:app_version] = RailsErrorDashboard.configuration.app_version ||
85
+ ENV["APP_VERSION"] ||
86
+ detect_version_from_file
87
+ end
88
+
74
89
  # Find existing error or create new one
75
90
  # This ensures accurate occurrence tracking
76
91
  error_log = ErrorLog.find_or_increment_by_hash(error_hash, attributes.merge(error_hash: error_hash))
@@ -86,7 +101,7 @@ module RailsErrorDashboard
86
101
  session_id: error_context.session_id
87
102
  )
88
103
  rescue => e
89
- Rails.logger.error("Failed to create error occurrence: #{e.message}")
104
+ RailsErrorDashboard::Logger.error("Failed to create error occurrence: #{e.message}")
90
105
  end
91
106
  end
92
107
 
@@ -112,10 +127,14 @@ module RailsErrorDashboard
112
127
  rescue => e
113
128
  # Don't let error logging cause more errors - fail silently
114
129
  # CRITICAL: Log but never propagate exception
130
+ # Log to Rails logger for visibility during development
115
131
  Rails.logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
116
- Rails.logger.error("Original exception: #{@exception.class} - #{@exception.message}") if @exception
117
- Rails.logger.error("Context: #{@context.inspect}") if @context
118
- Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
132
+ Rails.logger.error("Backtrace: #{e.backtrace&.first(10)&.join("\n")}")
133
+
134
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
135
+ RailsErrorDashboard::Logger.error("Original exception: #{@exception.class} - #{@exception.message}") if @exception
136
+ RailsErrorDashboard::Logger.error("Context: #{@context.inspect}") if @context
137
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
119
138
  nil # Explicitly return nil, never raise
120
139
  end
121
140
 
@@ -127,7 +146,7 @@ module RailsErrorDashboard
127
146
  RailsErrorDashboard.configuration.notification_callbacks[:error_logged].each do |callback|
128
147
  callback.call(error_log)
129
148
  rescue => e
130
- Rails.logger.error("Error in error_logged callback: #{e.message}")
149
+ RailsErrorDashboard::Logger.error("Error in error_logged callback: #{e.message}")
131
150
  end
132
151
 
133
152
  # Trigger critical_error callbacks if this is a critical error
@@ -135,7 +154,7 @@ module RailsErrorDashboard
135
154
  RailsErrorDashboard.configuration.notification_callbacks[:critical_error].each do |callback|
136
155
  callback.call(error_log)
137
156
  rescue => e
138
- Rails.logger.error("Error in critical_error callback: #{e.message}")
157
+ RailsErrorDashboard::Logger.error("Error in critical_error callback: #{e.message}")
139
158
  end
140
159
  end
141
160
  end
@@ -210,7 +229,7 @@ module RailsErrorDashboard
210
229
  end
211
230
  rescue NameError
212
231
  # Handle invalid class names in configuration
213
- Rails.logger.warn("Invalid ignored exception class: #{ignored}")
232
+ RailsErrorDashboard::Logger.warn("Invalid ignored exception class: #{ignored}")
214
233
  false
215
234
  end
216
235
  end
@@ -348,13 +367,32 @@ module RailsErrorDashboard
348
367
  # Enqueue alert job (which will handle throttling)
349
368
  BaselineAlertJob.perform_later(error_log.id, anomaly)
350
369
 
351
- Rails.logger.info(
370
+ RailsErrorDashboard::Logger.info(
352
371
  "Baseline alert queued for #{error_log.error_type} on #{error_log.platform}: " \
353
372
  "#{anomaly[:level]} (#{anomaly[:std_devs_above]&.round(1)}σ above baseline)"
354
373
  )
355
374
  rescue => e
356
375
  # Don't let baseline alerting cause errors
357
- Rails.logger.error("Failed to check baseline anomaly: #{e.message}")
376
+ RailsErrorDashboard::Logger.error("Failed to check baseline anomaly: #{e.message}")
377
+ end
378
+
379
+ # Detect git SHA from git command (fallback)
380
+ def detect_git_sha_from_command
381
+ return nil unless File.exist?(Rails.root.join(".git"))
382
+ `git rev-parse --short HEAD 2>/dev/null`.strip.presence
383
+ rescue => e
384
+ RailsErrorDashboard::Logger.debug("Could not detect git SHA: #{e.message}")
385
+ nil
386
+ end
387
+
388
+ # Detect app version from VERSION file (fallback)
389
+ def detect_version_from_file
390
+ version_file = Rails.root.join("VERSION")
391
+ return File.read(version_file).strip if File.exist?(version_file)
392
+ nil
393
+ rescue => e
394
+ RailsErrorDashboard::Logger.debug("Could not detect version: #{e.message}")
395
+ nil
358
396
  end
359
397
  end
360
398
  end
@@ -32,7 +32,7 @@ module RailsErrorDashboard
32
32
  RailsErrorDashboard.configuration.notification_callbacks[:error_resolved].each do |callback|
33
33
  callback.call(error)
34
34
  rescue => e
35
- Rails.logger.error("Error in error_resolved callback: #{e.message}")
35
+ RailsErrorDashboard::Logger.error("Error in error_resolved callback: #{e.message}")
36
36
  end
37
37
 
38
38
  # Emit ActiveSupport::Notifications instrumentation event
@@ -82,6 +82,10 @@ module RailsErrorDashboard
82
82
  # Notification callbacks (managed via helper methods, not set directly)
83
83
  attr_reader :notification_callbacks
84
84
 
85
+ # Internal logging configuration
86
+ attr_accessor :enable_internal_logging
87
+ attr_accessor :log_level
88
+
85
89
  def initialize
86
90
  # Default values
87
91
  @dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
@@ -145,6 +149,10 @@ module RailsErrorDashboard
145
149
  @baseline_alert_severities = [ :critical, :high ] # Alert on critical and high severity anomalies
146
150
  @baseline_alert_cooldown_minutes = ENV.fetch("BASELINE_ALERT_COOLDOWN", "120").to_i
147
151
 
152
+ # Internal logging defaults - SILENT by default
153
+ @enable_internal_logging = false # Opt-in for debugging
154
+ @log_level = :silent # Silent by default, use :debug, :info, :warn, :error, or :silent
155
+
148
156
  @notification_callbacks = {
149
157
  error_logged: [],
150
158
  critical_error: [],
@@ -32,10 +32,10 @@ module RailsErrorDashboard
32
32
  rescue => e
33
33
  # Don't let error logging cause more errors - fail silently
34
34
  # Log failure for debugging but NEVER propagate exception
35
- Rails.logger.error("[RailsErrorDashboard] ErrorReporter failed: #{e.class} - #{e.message}")
36
- Rails.logger.error("Original error: #{error.class} - #{error.message}") if error
37
- Rails.logger.error("Context: #{context.inspect}") if context
38
- Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
35
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] ErrorReporter failed: #{e.class} - #{e.message}")
36
+ RailsErrorDashboard::Logger.error("Original error: #{error.class} - #{error.message}") if error
37
+ RailsErrorDashboard::Logger.error("Context: #{context.inspect}") if context
38
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
39
39
  nil # Explicitly return nil, never raise
40
40
  end
41
41
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ # Internal logger wrapper for Rails Error Dashboard
5
+ #
6
+ # By default, all logging is SILENT to keep production logs clean.
7
+ # Users can opt-in to verbose logging for debugging.
8
+ #
9
+ # @example Enable logging for debugging
10
+ # RailsErrorDashboard.configure do |config|
11
+ # config.enable_internal_logging = true
12
+ # config.log_level = :debug
13
+ # end
14
+ #
15
+ # @example Production troubleshooting (errors only)
16
+ # RailsErrorDashboard.configure do |config|
17
+ # config.enable_internal_logging = true
18
+ # config.log_level = :error
19
+ # end
20
+ module Logger
21
+ LOG_LEVELS = {
22
+ debug: 0,
23
+ info: 1,
24
+ warn: 2,
25
+ error: 3,
26
+ silent: 4
27
+ }.freeze
28
+
29
+ class << self
30
+ # Log debug message (only if internal logging enabled)
31
+ #
32
+ # @param message [String] The message to log
33
+ # @example
34
+ # RailsErrorDashboard::Logger.debug("Processing error #123")
35
+ def debug(message)
36
+ return unless logging_enabled?
37
+ return unless log_level_enabled?(:debug)
38
+
39
+ Rails.logger.debug(formatted_message(message))
40
+ end
41
+
42
+ # Log info message (only if internal logging enabled)
43
+ #
44
+ # @param message [String] The message to log
45
+ # @example
46
+ # RailsErrorDashboard::Logger.info("Registered plugin: MyPlugin")
47
+ def info(message)
48
+ return unless logging_enabled?
49
+ return unless log_level_enabled?(:info)
50
+
51
+ Rails.logger.info(formatted_message(message))
52
+ end
53
+
54
+ # Log warning message (only if internal logging enabled)
55
+ #
56
+ # @param message [String] The message to log
57
+ # @example
58
+ # RailsErrorDashboard::Logger.warn("Plugin already registered")
59
+ def warn(message)
60
+ return unless logging_enabled?
61
+ return unless log_level_enabled?(:warn)
62
+
63
+ Rails.logger.warn(formatted_message(message))
64
+ end
65
+
66
+ # Log error message
67
+ # Errors are logged by default unless log_level is :silent
68
+ #
69
+ # @param message [String] The message to log
70
+ # @example
71
+ # RailsErrorDashboard::Logger.error("Failed to save error log")
72
+ def error(message)
73
+ return unless log_level_enabled?(:error)
74
+
75
+ Rails.logger.error(formatted_message(message))
76
+ end
77
+
78
+ private
79
+
80
+ # Check if internal logging is enabled
81
+ #
82
+ # @return [Boolean]
83
+ def logging_enabled?
84
+ RailsErrorDashboard.configuration.enable_internal_logging
85
+ end
86
+
87
+ # Check if the given log level is enabled
88
+ #
89
+ # @param level [Symbol] The log level to check (:debug, :info, :warn, :error)
90
+ # @return [Boolean]
91
+ def log_level_enabled?(level)
92
+ config_level = RailsErrorDashboard.configuration.log_level || :silent
93
+ LOG_LEVELS[level] >= LOG_LEVELS[config_level]
94
+ end
95
+
96
+ # Format message with gem prefix
97
+ #
98
+ # @param message [String] The message to format
99
+ # @return [String] Formatted message with prefix
100
+ def formatted_message(message)
101
+ "[RailsErrorDashboard] #{message}"
102
+ end
103
+ end
104
+ end
105
+ end
@@ -36,8 +36,8 @@ module RailsErrorDashboard
36
36
  )
37
37
  rescue => e
38
38
  # If error reporting fails, log it but DON'T break the app
39
- Rails.logger.error("[RailsErrorDashboard] Middleware error reporting failed: #{e.class} - #{e.message}")
40
- Rails.logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
39
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Middleware error reporting failed: #{e.class} - #{e.message}")
40
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(5)&.join("\n")) if e.backtrace
41
41
  end
42
42
 
43
43
  # Re-raise original exception to let Rails handle the response
@@ -92,9 +92,9 @@ module RailsErrorDashboard
92
92
  send(method_name, *args)
93
93
  rescue => e
94
94
  # Log plugin failures but never propagate - plugins must not break the app
95
- Rails.logger.error("[RailsErrorDashboard] Plugin '#{name}' failed in #{method_name}: #{e.class} - #{e.message}")
96
- Rails.logger.error("Plugin version: #{version}")
97
- Rails.logger.error(e.backtrace&.first(10)&.join("\n")) if e.backtrace
95
+ RailsErrorDashboard::Logger.error("[RailsErrorDashboard] Plugin '#{name}' failed in #{method_name}: #{e.class} - #{e.message}")
96
+ RailsErrorDashboard::Logger.error("Plugin version: #{version}")
97
+ RailsErrorDashboard::Logger.error(e.backtrace&.first(10)&.join("\n")) if e.backtrace
98
98
  nil # Explicitly return nil, never raise
99
99
  end
100
100
  end
@@ -18,13 +18,13 @@ module RailsErrorDashboard
18
18
  end
19
19
 
20
20
  if plugins.any? { |p| p.name == plugin.name }
21
- Rails.logger.warn("Plugin '#{plugin.name}' is already registered, skipping")
21
+ RailsErrorDashboard::Logger.warn("Plugin '#{plugin.name}' is already registered, skipping")
22
22
  return false
23
23
  end
24
24
 
25
25
  plugins << plugin
26
26
  plugin.on_register
27
- Rails.logger.info("Registered plugin: #{plugin.name} (#{plugin.version})")
27
+ RailsErrorDashboard::Logger.info("Registered plugin: #{plugin.name} (#{plugin.version})")
28
28
  true
29
29
  end
30
30
 
@@ -62,7 +62,7 @@ module RailsErrorDashboard
62
62
  labels: [ "rails-error-dashboard", error_log.platform ].compact
63
63
  }
64
64
 
65
- Rails.logger.info("Would create Jira ticket: #{ticket_data.to_json}")
65
+ RailsErrorDashboard::Logger.info("Would create Jira ticket: #{ticket_data.to_json}")
66
66
 
67
67
  # Actual implementation:
68
68
  # require 'httparty'
@@ -56,7 +56,7 @@ module RailsErrorDashboard
56
56
  # Datadog::Statsd.increment(metric_name, tags: metric_tags(data))
57
57
 
58
58
  # For demonstration, just log
59
- Rails.logger.info("Metrics: #{metric_name} - #{data.is_a?(Hash) ? data : data.class.name}")
59
+ RailsErrorDashboard::Logger.info("Metrics: #{metric_name} - #{data.is_a?(Hash) ? data : data.class.name}")
60
60
  end
61
61
 
62
62
  def metric_tags(data)
@@ -22,7 +22,15 @@ module RailsErrorDashboard
22
22
  errors_trend_7d: errors_trend_7d,
23
23
  errors_by_severity_7d: errors_by_severity_7d,
24
24
  spike_detected: spike_detected?,
25
- spike_info: spike_info
25
+ spike_info: spike_info,
26
+ # New metrics for Overview dashboard
27
+ error_rate: error_rate,
28
+ affected_users_today: affected_users_today,
29
+ affected_users_yesterday: affected_users_yesterday,
30
+ affected_users_change: affected_users_change,
31
+ trend_percentage: trend_percentage,
32
+ trend_direction: trend_direction,
33
+ top_errors_by_impact: top_errors_by_impact
26
34
  }
27
35
  end
28
36
 
@@ -164,6 +172,106 @@ module RailsErrorDashboard
164
172
  :critical
165
173
  end
166
174
  end
175
+
176
+ # Calculate error rate as a percentage
177
+ # Since we don't track total requests, we'll use error count as proxy
178
+ # In the future, this could be: (errors / total_requests) * 100
179
+ def error_rate
180
+ today_errors = ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count
181
+ return 0.0 if today_errors.zero?
182
+
183
+ # For now, use a simple heuristic: errors per hour today
184
+ # Assume we want < 1 error per hour = good (< 1%)
185
+ # 1-5 errors per hour = warning (1-5%)
186
+ # > 5 errors per hour = critical (> 5%)
187
+ hours_today = ((Time.current - Time.current.beginning_of_day) / 1.hour).round(1)
188
+ hours_today = 1.0 if hours_today < 1.0 # Avoid division by zero in early morning
189
+
190
+ errors_per_hour = today_errors / hours_today
191
+ # Convert to percentage scale (0-100)
192
+ # Scale: 0 errors/hr = 0%, 1 error/hr = 1%, 10 errors/hr = 10%, etc.
193
+ [ errors_per_hour, 100.0 ].min.round(1)
194
+ end
195
+
196
+ # Count distinct users affected by errors today
197
+ def affected_users_today
198
+ ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day)
199
+ .where.not(user_id: nil)
200
+ .distinct
201
+ .count(:user_id)
202
+ end
203
+
204
+ # Count distinct users affected by errors yesterday
205
+ def affected_users_yesterday
206
+ ErrorLog.where("occurred_at >= ? AND occurred_at < ?",
207
+ 1.day.ago.beginning_of_day,
208
+ Time.current.beginning_of_day)
209
+ .where.not(user_id: nil)
210
+ .distinct
211
+ .count(:user_id)
212
+ end
213
+
214
+ # Calculate change in affected users (today vs yesterday)
215
+ def affected_users_change
216
+ today = affected_users_today
217
+ yesterday = affected_users_yesterday
218
+
219
+ return 0 if today.zero? && yesterday.zero?
220
+ return today if yesterday.zero?
221
+
222
+ today - yesterday
223
+ end
224
+
225
+ # Calculate percentage change in errors (today vs yesterday)
226
+ def trend_percentage
227
+ today = ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count
228
+ yesterday = ErrorLog.where("occurred_at >= ? AND occurred_at < ?",
229
+ 1.day.ago.beginning_of_day,
230
+ Time.current.beginning_of_day).count
231
+
232
+ return 0.0 if today.zero? && yesterday.zero?
233
+ return 100.0 if yesterday.zero? && today.positive?
234
+
235
+ ((today - yesterday).to_f / yesterday * 100).round(1)
236
+ end
237
+
238
+ # Determine trend direction (increasing, decreasing, stable)
239
+ def trend_direction
240
+ trend = trend_percentage
241
+
242
+ if trend > 10
243
+ :increasing
244
+ elsif trend < -10
245
+ :decreasing
246
+ else
247
+ :stable
248
+ end
249
+ end
250
+
251
+ # Get top 5 errors ranked by impact score
252
+ # Impact = affected_users_count × occurrence_count
253
+ def top_errors_by_impact
254
+ ErrorLog.where("occurred_at >= ?", 7.days.ago)
255
+ .group(:error_type, :id)
256
+ .select("error_type, id, occurrence_count,
257
+ COUNT(DISTINCT user_id) as affected_users,
258
+ COUNT(DISTINCT user_id) * occurrence_count as impact_score")
259
+ .order("impact_score DESC")
260
+ .limit(5)
261
+ .map do |error|
262
+ full_error = ErrorLog.find(error.id)
263
+ {
264
+ id: error.id,
265
+ error_type: error.error_type,
266
+ message: full_error.message&.truncate(80),
267
+ severity: full_error.severity,
268
+ occurrence_count: error.occurrence_count,
269
+ affected_users: error.affected_users.to_i,
270
+ impact_score: error.impact_score.to_i,
271
+ occurred_at: full_error.occurred_at
272
+ }
273
+ end
274
+ end
167
275
  end
168
276
  end
169
277
  end
@@ -29,6 +29,11 @@ module RailsErrorDashboard
29
29
  query = filter_by_platform(query)
30
30
  query = filter_by_search(query)
31
31
  query = filter_by_severity(query)
32
+ # Phase 3: Workflow filters
33
+ query = filter_by_status(query)
34
+ query = filter_by_assignment(query)
35
+ query = filter_by_priority(query)
36
+ query = filter_by_snoozed(query)
32
37
  query
33
38
  end
34
39
 
@@ -39,14 +44,23 @@ module RailsErrorDashboard
39
44
  end
40
45
 
41
46
  def filter_by_resolved(query)
42
- # Default to unresolved only if no explicit filter is set
43
- # If unresolved param is explicitly false (boolean, string, or "0"), show all errors
44
- # Otherwise, default to showing only unresolved errors
45
- if @filters[:unresolved] == false || @filters[:unresolved] == "false" || @filters[:unresolved] == "0"
46
- # Show all errors (resolved and unresolved)
47
+ # Handle unresolved filter with explicit true/false values
48
+ # When checkbox is unchecked: unresolved=false show all errors
49
+ # When checkbox is checked: unresolved=true → show only unresolved errors
50
+ # When no filter: nil default to unresolved only
51
+
52
+ case @filters[:unresolved]
53
+ when false, "false", "0"
54
+ # Explicitly show all errors (resolved and unresolved)
47
55
  query
56
+ when true, "true", "1"
57
+ # Explicitly show only unresolved errors
58
+ query.unresolved
59
+ when nil, ""
60
+ # Default: show only unresolved errors when no filter is set
61
+ query.unresolved
48
62
  else
49
- # Default: show only unresolved errors
63
+ # Fallback: show only unresolved errors
50
64
  query.unresolved
51
65
  end
52
66
  end
@@ -102,6 +116,47 @@ module RailsErrorDashboard
102
116
 
103
117
  query.where(error_type: error_types)
104
118
  end
119
+
120
+ # Phase 3: Workflow filter methods
121
+
122
+ def filter_by_status(query)
123
+ return query unless @filters[:status].present?
124
+ return query unless query.model.column_names.include?("status")
125
+
126
+ query.by_status(@filters[:status])
127
+ end
128
+
129
+ def filter_by_assignment(query)
130
+ return query unless @filters[:assigned_to].present?
131
+ return query unless query.model.column_names.include?("assigned_to")
132
+
133
+ case @filters[:assigned_to]
134
+ when "__unassigned__"
135
+ query.unassigned
136
+ when "__assigned__"
137
+ query.assigned
138
+ else
139
+ query.by_assignee(@filters[:assigned_to])
140
+ end
141
+ end
142
+
143
+ def filter_by_priority(query)
144
+ return query unless @filters[:priority_level].present?
145
+ return query unless query.model.column_names.include?("priority_level")
146
+
147
+ query.by_priority(@filters[:priority_level])
148
+ end
149
+
150
+ def filter_by_snoozed(query)
151
+ return query unless query.model.column_names.include?("snoozed_until")
152
+
153
+ # If hide_snoozed is checked, exclude snoozed errors
154
+ if @filters[:hide_snoozed] == "1" || @filters[:hide_snoozed] == true
155
+ query.active
156
+ else
157
+ query
158
+ end
159
+ end
105
160
  end
106
161
  end
107
162
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Service: Parse and categorize backtrace frames
6
+ # Filters out framework noise to show only relevant application code
7
+ class BacktraceParser
8
+ # Match both formats:
9
+ # /path/file.rb:123:in `method'
10
+ # /path/file.rb:123:in 'ClassName#method'
11
+ FRAME_PATTERN = %r{^(.+):(\d+)(?::in [`'](.+)['`])?$}
12
+
13
+ def self.parse(backtrace_string)
14
+ new(backtrace_string).parse
15
+ end
16
+
17
+ def initialize(backtrace_string)
18
+ @backtrace_string = backtrace_string
19
+ end
20
+
21
+ def parse
22
+ return [] if @backtrace_string.blank?
23
+
24
+ lines = @backtrace_string.split("\n")
25
+ lines.map.with_index do |line, index|
26
+ parse_frame(line.strip, index)
27
+ end.compact
28
+ end
29
+
30
+ private
31
+
32
+ def parse_frame(line, index)
33
+ match = line.match(FRAME_PATTERN)
34
+ return nil unless match
35
+
36
+ file_path = match[1]
37
+ line_number = match[2].to_i
38
+ method_name = match[3] || "(unknown)"
39
+
40
+ {
41
+ index: index,
42
+ file_path: file_path,
43
+ line_number: line_number,
44
+ method_name: method_name,
45
+ category: categorize_frame(file_path),
46
+ full_line: line,
47
+ short_path: shorten_path(file_path)
48
+ }
49
+ end
50
+
51
+ def categorize_frame(file_path)
52
+ # Application code (highest priority)
53
+ return :app if app_code?(file_path)
54
+
55
+ # Gem code (dependencies)
56
+ return :gem if gem_code?(file_path)
57
+
58
+ # Rails framework
59
+ return :framework if rails_code?(file_path)
60
+
61
+ # Ruby core/stdlib
62
+ :ruby_core
63
+ end
64
+
65
+ def app_code?(file_path)
66
+ # Match /app/, /lib/ directories in the application
67
+ file_path.include?("/app/") ||
68
+ (file_path.include?("/lib/") && !file_path.include?("/gems/") && !file_path.include?("/ruby/"))
69
+ end
70
+
71
+ def gem_code?(file_path)
72
+ file_path.include?("/gems/") ||
73
+ file_path.include?("/bundler/gems/") ||
74
+ file_path.include?("/vendor/bundle/")
75
+ end
76
+
77
+ def rails_code?(file_path)
78
+ file_path.include?("/railties-") ||
79
+ file_path.include?("/actionpack-") ||
80
+ file_path.include?("/actionview-") ||
81
+ file_path.include?("/activerecord-") ||
82
+ file_path.include?("/activesupport-") ||
83
+ file_path.include?("/actioncable-") ||
84
+ file_path.include?("/activejob-") ||
85
+ file_path.include?("/actionmailer-") ||
86
+ file_path.include?("/activestorage-") ||
87
+ file_path.include?("/actionmailbox-") ||
88
+ file_path.include?("/actiontext-") ||
89
+ file_path.include?("/rails-")
90
+ end
91
+
92
+ def shorten_path(file_path)
93
+ # Remove gem version numbers and long paths
94
+ # /Users/.../.gem/ruby/3.4.0/gems/activerecord-8.0.4/lib/... → activerecord/.../file.rb
95
+ if file_path.include?("/gems/")
96
+ parts = file_path.split("/gems/").last
97
+ gem_and_path = parts.split("/", 2)
98
+ gem_name = gem_and_path.first.split("-").first # Remove version
99
+ path_in_gem = gem_and_path.last
100
+ "#{gem_name}/#{path_in_gem}"
101
+ # /path/to/app/controllers/... → app/controllers/...
102
+ elsif file_path.include?("/app/")
103
+ file_path.split("/app/").last.prepend("app/")
104
+ elsif file_path.include?("/lib/") && !file_path.include?("/ruby/")
105
+ file_path.split("/lib/").last.prepend("lib/")
106
+ else
107
+ # Just show filename for Ruby core
108
+ File.basename(file_path)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "rails_error_dashboard/version"
2
2
  require "rails_error_dashboard/engine"
3
3
  require "rails_error_dashboard/configuration"
4
+ require "rails_error_dashboard/logger"
4
5
 
5
6
  # External dependencies
6
7
  require "pagy"
@@ -11,6 +12,7 @@ require "httparty"
11
12
  # Core library files
12
13
  require "rails_error_dashboard/value_objects/error_context"
13
14
  require "rails_error_dashboard/services/platform_detector"
15
+ require "rails_error_dashboard/services/backtrace_parser"
14
16
  require "rails_error_dashboard/services/similarity_calculator"
15
17
  require "rails_error_dashboard/services/cascade_detector"
16
18
  require "rails_error_dashboard/services/baseline_calculator"