rails_error_dashboard 0.1.3 → 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.
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Generators
5
+ class UninstallGenerator < Rails::Generators::Base
6
+ desc "Uninstalls Rails Error Dashboard and removes all associated files and data"
7
+
8
+ class_option :keep_data, type: :boolean, default: false, desc: "Keep error data in database (don't drop tables)"
9
+ class_option :skip_confirmation, type: :boolean, default: false, desc: "Skip confirmation prompts"
10
+ class_option :manual_only, type: :boolean, default: false, desc: "Show manual instructions only, don't perform automated uninstall"
11
+
12
+ def welcome_message
13
+ say "\n"
14
+ say "=" * 80
15
+ say " 🗑️ Rails Error Dashboard - Uninstall", :red
16
+ say "=" * 80
17
+ say "\n"
18
+ say "This will remove Rails Error Dashboard from your application.", :yellow
19
+ say "\n"
20
+ end
21
+
22
+ def detect_installed_components
23
+ @components = {
24
+ initializer: File.exist?("config/initializers/rails_error_dashboard.rb"),
25
+ route: route_mounted?,
26
+ migrations: migrations_exist?,
27
+ tables: tables_exist?,
28
+ gemfile: gemfile_includes_gem?
29
+ }
30
+
31
+ say "Detected components:", :cyan
32
+ say " #{status_icon(@components[:gemfile])} Gemfile entry"
33
+ say " #{status_icon(@components[:initializer])} Initializer (config/initializers/rails_error_dashboard.rb)"
34
+ say " #{status_icon(@components[:route])} Route (mount RailsErrorDashboard::Engine)"
35
+ say " #{status_icon(@components[:migrations])} Migrations (#{migration_count} files)"
36
+ say " #{status_icon(@components[:tables])} Database tables (#{table_count} tables)"
37
+ say "\n"
38
+ end
39
+
40
+ def show_manual_instructions
41
+ say "=" * 80
42
+ say " 📖 Manual Uninstall Instructions", :cyan
43
+ say "=" * 80
44
+ say "\n"
45
+
46
+ say "Step 1: Remove from Gemfile", :yellow
47
+ say " Open: Gemfile"
48
+ say " Remove: gem 'rails_error_dashboard'"
49
+ say " Run: bundle install"
50
+ say "\n"
51
+
52
+ if @components[:initializer]
53
+ say "Step 2: Remove initializer", :yellow
54
+ say " Delete: config/initializers/rails_error_dashboard.rb"
55
+ say "\n"
56
+ end
57
+
58
+ if @components[:route]
59
+ say "Step 3: Remove route", :yellow
60
+ say " Open: config/routes.rb"
61
+ say " Remove: mount RailsErrorDashboard::Engine => '/error_dashboard'"
62
+ say "\n"
63
+ end
64
+
65
+ if @components[:migrations]
66
+ say "Step 4: Remove migrations", :yellow
67
+ say " Delete migration files from db/migrate/:"
68
+ migration_files.each do |file|
69
+ say " - #{File.basename(file)}", :light_black
70
+ end
71
+ say "\n"
72
+ end
73
+
74
+ if @components[:tables]
75
+ say "Step 5: Drop database tables (⚠️ DESTRUCTIVE - will delete all error data)", :yellow
76
+ say " Run: rails rails_error_dashboard:db:drop"
77
+ say " Or manually in rails console:"
78
+ say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_logs')", :light_black
79
+ say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_occurrences')", :light_black
80
+ say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_cascade_patterns')", :light_black
81
+ say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_baselines')", :light_black
82
+ say " ActiveRecord::Base.connection.execute('DROP TABLE rails_error_dashboard_error_comments')", :light_black
83
+ say " ActiveRecord::Migration.drop_table(:rails_error_dashboard_error_logs) rescue nil", :light_black
84
+ say "\n"
85
+ end
86
+
87
+ say "Step 6: Clean up environment variables (optional)", :yellow
88
+ say " Remove from .env or environment:"
89
+ say " - ERROR_DASHBOARD_USER"
90
+ say " - ERROR_DASHBOARD_PASSWORD"
91
+ say " - SLACK_WEBHOOK_URL"
92
+ say " - ERROR_NOTIFICATION_EMAILS"
93
+ say " - DISCORD_WEBHOOK_URL"
94
+ say " - PAGERDUTY_INTEGRATION_KEY"
95
+ say " - WEBHOOK_URLS"
96
+ say " - DASHBOARD_BASE_URL"
97
+ say "\n"
98
+
99
+ say "Step 7: Restart your application", :yellow
100
+ say " Run: rails restart (or restart your server)"
101
+ say "\n"
102
+
103
+ say "=" * 80
104
+ say "\n"
105
+ end
106
+
107
+ def confirm_automated_uninstall
108
+ return if options[:manual_only]
109
+ return if options[:skip_confirmation]
110
+
111
+ say "Would you like to run the automated uninstall? (recommended)", :cyan
112
+ say "This will:", :yellow
113
+ say " ✓ Remove initializer file"
114
+ say " ✓ Remove route from config/routes.rb"
115
+ say " ✓ Remove migration files"
116
+ if options[:keep_data]
117
+ say " ✗ Keep database tables and data (--keep-data flag set)", :green
118
+ else
119
+ say " ⚠️ Drop all database tables (deletes all error data!)", :red
120
+ end
121
+ say "\n"
122
+
123
+ response = ask("Proceed with automated uninstall? (yes/no):", :yellow, limited_to: [ "yes", "no", "y", "n" ])
124
+
125
+ if response.downcase == "no" || response.downcase == "n"
126
+ say "\n"
127
+ say "Automated uninstall cancelled.", :yellow
128
+ say "Follow the manual instructions above to uninstall.", :cyan
129
+ say "\n"
130
+ exit 0
131
+ end
132
+
133
+ say "\n"
134
+ end
135
+
136
+ def final_data_warning
137
+ return if options[:manual_only]
138
+ return if options[:keep_data]
139
+ return unless @components[:tables]
140
+
141
+ say "=" * 80
142
+ say " ⚠️ FINAL WARNING - Data Deletion", :red
143
+ say "=" * 80
144
+ say "\n"
145
+ say "You are about to PERMANENTLY DELETE all error tracking data!", :red
146
+ say "\n"
147
+ say "Database tables to be dropped:", :yellow
148
+ table_names.each do |table|
149
+ say " • #{table}", :light_black
150
+ end
151
+ say "\n"
152
+ say "This action CANNOT be undone!", :red
153
+ say "\n"
154
+
155
+ response = ask("Type 'DELETE ALL DATA' to confirm:", :red)
156
+
157
+ if response != "DELETE ALL DATA"
158
+ say "\n"
159
+ say "Data deletion cancelled. Database tables will be kept.", :green
160
+ say "Use --keep-data flag to skip this warning in the future.", :cyan
161
+ @components[:tables] = false # Don't drop tables
162
+ say "\n"
163
+ end
164
+
165
+ say "\n"
166
+ end
167
+
168
+ def remove_initializer
169
+ return if options[:manual_only]
170
+ return unless @components[:initializer]
171
+
172
+ remove_file "config/initializers/rails_error_dashboard.rb"
173
+ say " ✓ Removed initializer", :green
174
+ end
175
+
176
+ def remove_route
177
+ return if options[:manual_only]
178
+ return unless @components[:route]
179
+
180
+ begin
181
+ gsub_file "config/routes.rb", /mount RailsErrorDashboard::Engine.*\n/, ""
182
+ say " ✓ Removed route", :green
183
+ rescue => e
184
+ say " ⚠️ Could not automatically remove route: #{e.message}", :yellow
185
+ say " Please manually remove: mount RailsErrorDashboard::Engine => '/error_dashboard'", :yellow
186
+ end
187
+ end
188
+
189
+ def remove_migrations
190
+ return if options[:manual_only]
191
+ return unless @components[:migrations]
192
+
193
+ migration_files.each do |file|
194
+ remove_file file
195
+ end
196
+ say " ✓ Removed #{migration_count} migration file(s)", :green
197
+ end
198
+
199
+ def drop_database_tables
200
+ return if options[:manual_only]
201
+ return if options[:keep_data]
202
+ return unless @components[:tables]
203
+
204
+ say " Dropping database tables...", :yellow
205
+
206
+ # Drop tables in reverse order (to respect foreign keys)
207
+ tables_to_drop = [
208
+ "rails_error_dashboard_error_comments",
209
+ "rails_error_dashboard_error_occurrences",
210
+ "rails_error_dashboard_cascade_patterns",
211
+ "rails_error_dashboard_error_baselines",
212
+ "rails_error_dashboard_error_logs"
213
+ ]
214
+
215
+ dropped_count = 0
216
+ tables_to_drop.each do |table|
217
+ if ActiveRecord::Base.connection.table_exists?(table)
218
+ ActiveRecord::Base.connection.drop_table(table, if_exists: true)
219
+ dropped_count += 1
220
+ end
221
+ rescue => e
222
+ say " ⚠️ Could not drop table #{table}: #{e.message}", :yellow
223
+ end
224
+
225
+ say " ✓ Dropped #{dropped_count} database table(s)", :green
226
+ end
227
+
228
+ def show_completion_message
229
+ return if options[:manual_only]
230
+
231
+ say "\n"
232
+ say "=" * 80
233
+ say " ✅ Uninstall Complete!", :green
234
+ say "=" * 80
235
+ say "\n"
236
+
237
+ say "Remaining manual steps:", :cyan
238
+ say "\n"
239
+
240
+ say "1. Remove from Gemfile:", :yellow
241
+ say " Open: Gemfile"
242
+ say " Remove: gem 'rails_error_dashboard'"
243
+ say " Run: bundle install"
244
+ say "\n"
245
+
246
+ say "2. Restart your application:", :yellow
247
+ say " Run: rails restart"
248
+ say " Or: kill and restart your server process"
249
+ say "\n"
250
+
251
+ if options[:keep_data]
252
+ say "3. Database tables were kept (--keep-data flag)", :green
253
+ say " To remove data later, run:", :yellow
254
+ say " rails generate rails_error_dashboard:uninstall", :yellow
255
+ say "\n"
256
+ end
257
+
258
+ say "Clean up environment variables (optional):", :yellow
259
+ say " • ERROR_DASHBOARD_USER, ERROR_DASHBOARD_PASSWORD"
260
+ say " • SLACK_WEBHOOK_URL, ERROR_NOTIFICATION_EMAILS"
261
+ say " • DISCORD_WEBHOOK_URL, PAGERDUTY_INTEGRATION_KEY"
262
+ say " • WEBHOOK_URLS, DASHBOARD_BASE_URL"
263
+ say "\n"
264
+
265
+ say "Thank you for using Rails Error Dashboard! 👋", :cyan
266
+ say "\n"
267
+ end
268
+
269
+ private
270
+
271
+ def status_icon(present)
272
+ present ? "✓" : "✗"
273
+ end
274
+
275
+ def route_mounted?
276
+ return false unless File.exist?("config/routes.rb")
277
+ File.read("config/routes.rb").include?("RailsErrorDashboard::Engine")
278
+ end
279
+
280
+ def migrations_exist?
281
+ migration_files.any?
282
+ end
283
+
284
+ def migration_files
285
+ Dir.glob("db/migrate/*rails_error_dashboard*.rb")
286
+ end
287
+
288
+ def migration_count
289
+ migration_files.count
290
+ end
291
+
292
+ def tables_exist?
293
+ return false unless defined?(ActiveRecord::Base)
294
+ table_names.any? { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
295
+ end
296
+
297
+ def table_names
298
+ [
299
+ "rails_error_dashboard_error_logs",
300
+ "rails_error_dashboard_error_occurrences",
301
+ "rails_error_dashboard_cascade_patterns",
302
+ "rails_error_dashboard_error_baselines",
303
+ "rails_error_dashboard_error_comments"
304
+ ]
305
+ end
306
+
307
+ def table_count
308
+ table_names.count { |table| ActiveRecord::Base.connection.table_exists?(table) rescue false }
309
+ end
310
+
311
+ def gemfile_includes_gem?
312
+ return false unless File.exist?("Gemfile")
313
+ File.read("Gemfile").match?(/gem\s+['"]rails_error_dashboard['"]/)
314
+ end
315
+ end
316
+ end
317
+ 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,8 @@ 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)
32
35
  # Phase 3: Workflow filters
33
36
  query = filter_by_status(query)
34
37
  query = filter_by_assignment(query)
@@ -121,14 +124,14 @@ module RailsErrorDashboard
121
124
 
122
125
  def filter_by_status(query)
123
126
  return query unless @filters[:status].present?
124
- return query unless query.model.column_names.include?("status")
127
+ return query unless ErrorLog.column_names.include?("status")
125
128
 
126
129
  query.by_status(@filters[:status])
127
130
  end
128
131
 
129
132
  def filter_by_assignment(query)
130
133
  return query unless @filters[:assigned_to].present?
131
- return query unless query.model.column_names.include?("assigned_to")
134
+ return query unless ErrorLog.column_names.include?("assigned_to")
132
135
 
133
136
  case @filters[:assigned_to]
134
137
  when "__unassigned__"
@@ -142,13 +145,13 @@ module RailsErrorDashboard
142
145
 
143
146
  def filter_by_priority(query)
144
147
  return query unless @filters[:priority_level].present?
145
- return query unless query.model.column_names.include?("priority_level")
148
+ return query unless ErrorLog.column_names.include?("priority_level")
146
149
 
147
150
  query.by_priority(@filters[:priority_level])
148
151
  end
149
152
 
150
153
  def filter_by_snoozed(query)
151
- return query unless query.model.column_names.include?("snoozed_until")
154
+ return query unless ErrorLog.column_names.include?("snoozed_until")
152
155
 
153
156
  # If hide_snoozed is checked, exclude snoozed errors
154
157
  if @filters[:hide_snoozed] == "1" || @filters[:hide_snoozed] == true
@@ -157,6 +160,75 @@ module RailsErrorDashboard
157
160
  query
158
161
  end
159
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
160
232
  end
161
233
  end
162
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
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
@@ -8,6 +8,7 @@ require "pagy"
8
8
  require "browser"
9
9
  require "groupdate"
10
10
  require "httparty"
11
+ require "chartkick"
11
12
 
12
13
  # Core library files
13
14
  require "rails_error_dashboard/value_objects/error_context"
@@ -32,6 +33,8 @@ require "rails_error_dashboard/queries/dashboard_stats"
32
33
  require "rails_error_dashboard/queries/analytics_stats"
33
34
  require "rails_error_dashboard/queries/filter_options"
34
35
  require "rails_error_dashboard/queries/similar_errors"
36
+ require "rails_error_dashboard/queries/recurring_issues"
37
+ require "rails_error_dashboard/queries/mttr_stats"
35
38
  require "rails_error_dashboard/error_reporter"
36
39
  require "rails_error_dashboard/middleware/error_catcher"
37
40