rails_error_dashboard 0.1.1 → 0.1.4

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -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.map +7 -0
  8. data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
  9. data/app/controllers/rails_error_dashboard/errors_controller.rb +135 -1
  10. data/app/helpers/rails_error_dashboard/application_helper.rb +80 -4
  11. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
  12. data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
  13. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
  14. data/app/models/rails_error_dashboard/error_comment.rb +27 -0
  15. data/app/models/rails_error_dashboard/error_log.rb +159 -0
  16. data/app/views/layouts/rails_error_dashboard/application.html.erb +39 -1
  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 +439 -22
  23. data/app/views/rails_error_dashboard/errors/index.html.erb +127 -11
  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 +11 -1
  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/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +317 -0
  33. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
  34. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
  35. data/lib/rails_error_dashboard/commands/log_error.rb +47 -9
  36. data/lib/rails_error_dashboard/commands/resolve_error.rb +1 -1
  37. data/lib/rails_error_dashboard/configuration.rb +8 -0
  38. data/lib/rails_error_dashboard/error_reporter.rb +4 -4
  39. data/lib/rails_error_dashboard/logger.rb +105 -0
  40. data/lib/rails_error_dashboard/middleware/error_catcher.rb +2 -2
  41. data/lib/rails_error_dashboard/plugin.rb +3 -3
  42. data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
  43. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +1 -1
  44. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -1
  45. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +109 -1
  46. data/lib/rails_error_dashboard/queries/errors_list.rb +134 -7
  47. data/lib/rails_error_dashboard/queries/mttr_stats.rb +111 -0
  48. data/lib/rails_error_dashboard/queries/recurring_issues.rb +97 -0
  49. data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
  50. data/lib/rails_error_dashboard/version.rb +1 -1
  51. data/lib/rails_error_dashboard.rb +5 -0
  52. data/lib/tasks/rails_error_dashboard_tasks.rake +85 -4
  53. metadata +36 -2
@@ -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
@@ -14,10 +14,11 @@ module RailsErrorDashboard
14
14
  end
15
15
 
16
16
  def call
17
- query = ErrorLog.order(occurred_at: :desc)
17
+ query = ErrorLog
18
18
  # Only eager load user if User model exists
19
19
  query = query.includes(:user) if defined?(::User)
20
20
  query = apply_filters(query)
21
+ query = apply_sorting(query)
21
22
  query
22
23
  end
23
24
 
@@ -29,6 +30,13 @@ module RailsErrorDashboard
29
30
  query = filter_by_platform(query)
30
31
  query = filter_by_search(query)
31
32
  query = filter_by_severity(query)
33
+ query = filter_by_timeframe(query)
34
+ query = filter_by_frequency(query)
35
+ # Phase 3: Workflow filters
36
+ query = filter_by_status(query)
37
+ query = filter_by_assignment(query)
38
+ query = filter_by_priority(query)
39
+ query = filter_by_snoozed(query)
32
40
  query
33
41
  end
34
42
 
@@ -39,14 +47,23 @@ module RailsErrorDashboard
39
47
  end
40
48
 
41
49
  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)
50
+ # Handle unresolved filter with explicit true/false values
51
+ # When checkbox is unchecked: unresolved=false show all errors
52
+ # When checkbox is checked: unresolved=true → show only unresolved errors
53
+ # When no filter: nil default to unresolved only
54
+
55
+ case @filters[:unresolved]
56
+ when false, "false", "0"
57
+ # Explicitly show all errors (resolved and unresolved)
47
58
  query
59
+ when true, "true", "1"
60
+ # Explicitly show only unresolved errors
61
+ query.unresolved
62
+ when nil, ""
63
+ # Default: show only unresolved errors when no filter is set
64
+ query.unresolved
48
65
  else
49
- # Default: show only unresolved errors
66
+ # Fallback: show only unresolved errors
50
67
  query.unresolved
51
68
  end
52
69
  end
@@ -102,6 +119,116 @@ module RailsErrorDashboard
102
119
 
103
120
  query.where(error_type: error_types)
104
121
  end
122
+
123
+ # Phase 3: Workflow filter methods
124
+
125
+ def filter_by_status(query)
126
+ return query unless @filters[:status].present?
127
+ return query unless ErrorLog.column_names.include?("status")
128
+
129
+ query.by_status(@filters[:status])
130
+ end
131
+
132
+ def filter_by_assignment(query)
133
+ return query unless @filters[:assigned_to].present?
134
+ return query unless ErrorLog.column_names.include?("assigned_to")
135
+
136
+ case @filters[:assigned_to]
137
+ when "__unassigned__"
138
+ query.unassigned
139
+ when "__assigned__"
140
+ query.assigned
141
+ else
142
+ query.by_assignee(@filters[:assigned_to])
143
+ end
144
+ end
145
+
146
+ def filter_by_priority(query)
147
+ return query unless @filters[:priority_level].present?
148
+ return query unless ErrorLog.column_names.include?("priority_level")
149
+
150
+ query.by_priority(@filters[:priority_level])
151
+ end
152
+
153
+ def filter_by_snoozed(query)
154
+ return query unless ErrorLog.column_names.include?("snoozed_until")
155
+
156
+ # If hide_snoozed is checked, exclude snoozed errors
157
+ if @filters[:hide_snoozed] == "1" || @filters[:hide_snoozed] == true
158
+ query.active
159
+ else
160
+ query
161
+ end
162
+ end
163
+
164
+ def filter_by_timeframe(query)
165
+ return query unless @filters[:timeframe].present?
166
+
167
+ case @filters[:timeframe]
168
+ when "last_hour"
169
+ query.where("occurred_at >= ?", 1.hour.ago)
170
+ when "today"
171
+ query.where("occurred_at >= ?", Time.current.beginning_of_day)
172
+ when "yesterday"
173
+ query.where("occurred_at BETWEEN ? AND ?",
174
+ 1.day.ago.beginning_of_day,
175
+ 1.day.ago.end_of_day)
176
+ when "last_7_days"
177
+ query.where("occurred_at >= ?", 7.days.ago)
178
+ when "last_30_days"
179
+ query.where("occurred_at >= ?", 30.days.ago)
180
+ when "last_90_days"
181
+ query.where("occurred_at >= ?", 90.days.ago)
182
+ else
183
+ query
184
+ end
185
+ end
186
+
187
+ def filter_by_frequency(query)
188
+ return query unless @filters[:frequency].present?
189
+
190
+ case @filters[:frequency]
191
+ when "once"
192
+ query.where(occurrence_count: 1)
193
+ when "few"
194
+ query.where("occurrence_count BETWEEN ? AND ?", 2, 9)
195
+ when "frequent"
196
+ query.where("occurrence_count BETWEEN ? AND ?", 10, 99)
197
+ when "very_frequent"
198
+ query.where("occurrence_count >= ?", 100)
199
+ when "recurring"
200
+ # Errors that occurred multiple times AND are still active
201
+ query.where("occurrence_count > ?", 5)
202
+ .where("last_seen_at > ?", 24.hours.ago)
203
+ else
204
+ query
205
+ end
206
+ end
207
+
208
+ def apply_sorting(query)
209
+ sort_column = @filters[:sort_by].presence || "occurred_at"
210
+ sort_direction = @filters[:sort_direction].presence || "desc"
211
+
212
+ # Validate sort direction
213
+ sort_direction = %w[asc desc].include?(sort_direction) ? sort_direction : "desc"
214
+
215
+ # Map severity to priority for sorting (since severity is an enum/method)
216
+ # We'll use priority_score which factors in severity
217
+ case sort_column
218
+ when "occurred_at", "first_seen_at", "last_seen_at", "created_at", "resolved_at"
219
+ query.order(sort_column => sort_direction)
220
+ when "occurrence_count", "priority_score"
221
+ query.order(sort_column => sort_direction, occurred_at: :desc)
222
+ when "error_type", "platform", "app_version"
223
+ query.order(sort_column => sort_direction, occurred_at: :desc)
224
+ when "severity"
225
+ # Sort by priority_score as proxy for severity (critical=highest score)
226
+ query.order(priority_score: sort_direction, occurred_at: :desc)
227
+ else
228
+ # Default sort
229
+ query.order(occurred_at: :desc)
230
+ end
231
+ end
105
232
  end
106
233
  end
107
234
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Queries
5
+ # Query: Calculate Mean Time to Resolution (MTTR) statistics
6
+ # Provides metrics on how quickly errors are resolved
7
+ class MttrStats
8
+ def self.call(days = 30)
9
+ new(days).call
10
+ end
11
+
12
+ def initialize(days = 30)
13
+ @days = days
14
+ @start_date = days.days.ago
15
+ end
16
+
17
+ def call
18
+ {
19
+ overall_mttr: calculate_overall_mttr,
20
+ mttr_by_platform: mttr_by_platform,
21
+ mttr_by_severity: mttr_by_severity,
22
+ mttr_trend: mttr_trend_by_week,
23
+ fastest_resolution: fastest_resolution_time,
24
+ slowest_resolution: slowest_resolution_time,
25
+ total_resolved: resolved_errors.count
26
+ }
27
+ end
28
+
29
+ private
30
+
31
+ def resolved_errors
32
+ @resolved_errors ||= ErrorLog
33
+ .where.not(resolved_at: nil)
34
+ .where("occurred_at >= ?", @start_date)
35
+ end
36
+
37
+ def calculate_overall_mttr
38
+ return 0 if resolved_errors.empty?
39
+
40
+ total_hours = resolved_errors.sum do |error|
41
+ ((error.resolved_at - error.occurred_at) / 3600.0).round(2)
42
+ end
43
+ (total_hours / resolved_errors.count).round(2)
44
+ end
45
+
46
+ def mttr_by_platform
47
+ platforms = ErrorLog.distinct.pluck(:platform).compact
48
+
49
+ platforms.each_with_object({}) do |platform, result|
50
+ platform_resolved = resolved_errors.where(platform: platform)
51
+ next if platform_resolved.empty?
52
+
53
+ total_hours = platform_resolved.sum { |e| ((e.resolved_at - e.occurred_at) / 3600.0) }
54
+ result[platform] = (total_hours / platform_resolved.count).round(2)
55
+ end
56
+ end
57
+
58
+ def mttr_by_severity
59
+ {
60
+ critical: calculate_mttr_for_severity(:critical),
61
+ high: calculate_mttr_for_severity(:high),
62
+ medium: calculate_mttr_for_severity(:medium),
63
+ low: calculate_mttr_for_severity(:low)
64
+ }.compact
65
+ end
66
+
67
+ def calculate_mttr_for_severity(severity)
68
+ severity_errors = resolved_errors.select { |e| e.severity == severity }
69
+ return nil if severity_errors.empty?
70
+
71
+ total_hours = severity_errors.sum { |e| ((e.resolved_at - e.occurred_at) / 3600.0) }
72
+ (total_hours / severity_errors.count).round(2)
73
+ end
74
+
75
+ def mttr_trend_by_week
76
+ trends = {}
77
+ current_date = @start_date
78
+
79
+ while current_date < Time.current
80
+ week_end = current_date + 1.week
81
+ week_resolved = ErrorLog
82
+ .where.not(resolved_at: nil)
83
+ .where("occurred_at >= ? AND occurred_at < ?", current_date, week_end)
84
+
85
+ if week_resolved.any?
86
+ total_hours = week_resolved.sum { |e| ((e.resolved_at - e.occurred_at) / 3600.0) }
87
+ trends[current_date.to_date.to_s] = (total_hours / week_resolved.count).round(2)
88
+ end
89
+
90
+ current_date = week_end
91
+ end
92
+
93
+ trends
94
+ end
95
+
96
+ def fastest_resolution_time
97
+ return nil if resolved_errors.empty?
98
+
99
+ resolved_errors.min_by { |e| e.resolved_at - e.occurred_at }
100
+ .then { |e| ((e.resolved_at - e.occurred_at) / 60.0).round } # minutes
101
+ end
102
+
103
+ def slowest_resolution_time
104
+ return nil if resolved_errors.empty?
105
+
106
+ resolved_errors.max_by { |e| e.resolved_at - e.occurred_at }
107
+ .then { |e| ((e.resolved_at - e.occurred_at) / 3600.0).round(1) } # hours
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Queries
5
+ # Query: Analyze recurring and persistent errors
6
+ # Returns data about high-frequency errors, persistent issues, and cyclical patterns
7
+ class RecurringIssues
8
+ def self.call(days = 30)
9
+ new(days).call
10
+ end
11
+
12
+ def initialize(days = 30)
13
+ @days = days
14
+ @start_date = days.days.ago
15
+ end
16
+
17
+ def call
18
+ {
19
+ high_frequency_errors: high_frequency_errors,
20
+ persistent_errors: persistent_errors,
21
+ cyclical_patterns: cyclical_patterns
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def base_query
28
+ ErrorLog.where("occurred_at >= ?", @start_date)
29
+ end
30
+
31
+ def high_frequency_errors
32
+ # Errors with high occurrence count
33
+ base_query
34
+ .where("occurrence_count > ?", 10)
35
+ .group(:error_type)
36
+ .select("error_type,
37
+ SUM(occurrence_count) as total_occurrences,
38
+ MIN(first_seen_at) as first_occurrence,
39
+ MAX(last_seen_at) as last_occurrence,
40
+ COUNT(*) as unique_error_count")
41
+ .order("total_occurrences DESC")
42
+ .limit(10)
43
+ .map do |error|
44
+ first_seen = error.first_occurrence.is_a?(Time) ? error.first_occurrence : Time.parse(error.first_occurrence.to_s)
45
+ last_seen = error.last_occurrence.is_a?(Time) ? error.last_occurrence : Time.parse(error.last_occurrence.to_s)
46
+
47
+ {
48
+ error_type: error.error_type,
49
+ total_occurrences: error.total_occurrences,
50
+ first_seen: first_seen,
51
+ last_seen: last_seen,
52
+ duration_days: ((last_seen - first_seen) / 1.day).round,
53
+ still_active: last_seen > 24.hours.ago
54
+ }
55
+ end
56
+ end
57
+
58
+ def persistent_errors
59
+ # Errors that have been unresolved for longest time
60
+ base_query
61
+ .where(resolved: false)
62
+ .where("first_seen_at < ?", 7.days.ago)
63
+ .order("first_seen_at ASC")
64
+ .limit(10)
65
+ .map do |error|
66
+ {
67
+ id: error.id,
68
+ error_type: error.error_type,
69
+ message: error.message.to_s.truncate(100),
70
+ first_seen: error.first_seen_at,
71
+ age_days: ((Time.current - error.first_seen_at) / 1.day).round,
72
+ occurrence_count: error.occurrence_count,
73
+ platform: error.platform
74
+ }
75
+ end
76
+ end
77
+
78
+ def cyclical_patterns
79
+ # Use existing PatternDetector if available
80
+ return {} unless defined?(Services::PatternDetector)
81
+
82
+ top_error_types = base_query.group(:error_type).count.sort_by { |_, count| -count }.first(5).to_h.keys
83
+
84
+ top_error_types.each_with_object({}) do |error_type, result|
85
+ pattern = Services::PatternDetector.analyze_cyclical_pattern(
86
+ error_type: error_type,
87
+ platform: nil,
88
+ days: @days
89
+ )
90
+ result[error_type] = pattern if pattern[:pattern_strength] > 0.6
91
+ end
92
+ rescue NameError
93
+ {} # PatternDetector not available
94
+ end
95
+ end
96
+ end
97
+ 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.4"
3
3
  end
@@ -1,16 +1,19 @@
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"
7
8
  require "browser"
8
9
  require "groupdate"
9
10
  require "httparty"
11
+ require "chartkick"
10
12
 
11
13
  # Core library files
12
14
  require "rails_error_dashboard/value_objects/error_context"
13
15
  require "rails_error_dashboard/services/platform_detector"
16
+ require "rails_error_dashboard/services/backtrace_parser"
14
17
  require "rails_error_dashboard/services/similarity_calculator"
15
18
  require "rails_error_dashboard/services/cascade_detector"
16
19
  require "rails_error_dashboard/services/baseline_calculator"
@@ -30,6 +33,8 @@ require "rails_error_dashboard/queries/dashboard_stats"
30
33
  require "rails_error_dashboard/queries/analytics_stats"
31
34
  require "rails_error_dashboard/queries/filter_options"
32
35
  require "rails_error_dashboard/queries/similar_errors"
36
+ require "rails_error_dashboard/queries/recurring_issues"
37
+ require "rails_error_dashboard/queries/mttr_stats"
33
38
  require "rails_error_dashboard/error_reporter"
34
39
  require "rails_error_dashboard/middleware/error_catcher"
35
40