rails_error_dashboard 0.1.0 → 0.1.1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -700
  3. data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
  4. data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
  5. data/app/helpers/rails_error_dashboard/application_helper.rb +17 -0
  6. data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
  7. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
  8. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
  9. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
  10. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
  11. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
  12. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
  13. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
  14. data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
  15. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
  16. data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
  17. data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
  18. data/app/models/rails_error_dashboard/error_log.rb +326 -3
  19. data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
  20. data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
  21. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
  22. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
  23. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +76 -0
  24. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
  25. data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
  26. data/app/views/rails_error_dashboard/errors/analytics.html.erb +19 -39
  27. data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
  28. data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
  29. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
  30. data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
  31. data/config/routes.rb +2 -0
  32. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
  33. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
  34. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
  35. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
  36. data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
  37. data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
  38. data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
  39. data/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
  40. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -37
  41. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
  42. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
  43. data/lib/rails_error_dashboard/commands/log_error.rb +234 -7
  44. data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
  45. data/lib/rails_error_dashboard/configuration.rb +82 -5
  46. data/lib/rails_error_dashboard/error_reporter.rb +15 -7
  47. data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
  48. data/lib/rails_error_dashboard/plugin.rb +6 -3
  49. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
  50. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
  51. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
  52. data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
  53. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  54. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
  55. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +134 -2
  56. data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
  57. data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
  58. data/lib/rails_error_dashboard/queries/errors_list.rb +52 -11
  59. data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
  60. data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
  61. data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
  62. data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
  63. data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
  64. data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
  65. data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
  66. data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
  67. data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
  68. data/lib/rails_error_dashboard/version.rb +1 -1
  69. data/lib/rails_error_dashboard.rb +55 -7
  70. metadata +52 -9
  71. data/app/models/rails_error_dashboard/application_record.rb +0 -5
  72. data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
  73. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Detects cascade patterns by analyzing error occurrences
6
+ #
7
+ # Runs periodically to find errors that consistently follow other errors,
8
+ # indicating a causal relationship.
9
+ class CascadeDetector
10
+ # Time window to look for cascades (errors within this window may be related)
11
+ DETECTION_WINDOW = 60.seconds
12
+
13
+ # Minimum times a pattern must occur to be considered a cascade
14
+ MIN_CASCADE_FREQUENCY = 3
15
+
16
+ # Minimum probability threshold (% of time parent leads to child)
17
+ MIN_CASCADE_PROBABILITY = 0.7
18
+
19
+ def self.call(lookback_hours: 24)
20
+ new(lookback_hours: lookback_hours).detect_cascades
21
+ end
22
+
23
+ def initialize(lookback_hours: 24)
24
+ @lookback_hours = lookback_hours
25
+ @detected_count = 0
26
+ end
27
+
28
+ def detect_cascades
29
+ return { detected: 0, updated: 0 } unless can_detect?
30
+
31
+ # Get recent error occurrences
32
+ start_time = @lookback_hours.hours.ago
33
+ occurrences = ErrorOccurrence.where("occurred_at >= ?", start_time).order(:occurred_at)
34
+
35
+ # For each error occurrence, find potential children
36
+ patterns_found = Hash.new { |h, k| h[k] = { delays: [], count: 0 } }
37
+
38
+ occurrences.each do |parent_occ|
39
+ # Find occurrences within detection window
40
+ potential_children = ErrorOccurrence
41
+ .where("occurred_at > ? AND occurred_at <= ?",
42
+ parent_occ.occurred_at,
43
+ parent_occ.occurred_at + DETECTION_WINDOW)
44
+ .where.not(error_log_id: parent_occ.error_log_id)
45
+
46
+ potential_children.each do |child_occ|
47
+ key = [ parent_occ.error_log_id, child_occ.error_log_id ]
48
+ delay = (child_occ.occurred_at - parent_occ.occurred_at).to_f
49
+
50
+ patterns_found[key][:delays] << delay
51
+ patterns_found[key][:count] += 1
52
+ end
53
+ end
54
+
55
+ # Filter and save cascade patterns
56
+ updated_count = 0
57
+ patterns_found.each do |(parent_id, child_id), data|
58
+ next if data[:count] < MIN_CASCADE_FREQUENCY
59
+
60
+ # Find or create cascade pattern
61
+ pattern = CascadePattern.find_or_initialize_by(
62
+ parent_error_id: parent_id,
63
+ child_error_id: child_id
64
+ )
65
+
66
+ avg_delay = data[:delays].sum / data[:delays].size
67
+
68
+ if pattern.new_record?
69
+ pattern.frequency = data[:count]
70
+ pattern.avg_delay_seconds = avg_delay
71
+ pattern.last_detected_at = Time.current
72
+ pattern.save
73
+ @detected_count += 1
74
+ else
75
+ # Update existing pattern
76
+ pattern.increment_detection!(avg_delay)
77
+ updated_count += 1
78
+ end
79
+
80
+ # Calculate probability
81
+ pattern.calculate_probability!
82
+ end
83
+
84
+ { detected: @detected_count, updated: updated_count }
85
+ end
86
+
87
+ private
88
+
89
+ def can_detect?
90
+ defined?(CascadePattern) && CascadePattern.table_exists? &&
91
+ defined?(ErrorOccurrence) && ErrorOccurrence.table_exists?
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Service object for detecting occurrence patterns in errors
6
+ #
7
+ # Provides two main pattern detection capabilities:
8
+ # 1. Cyclical patterns - Daily/weekly rhythms (e.g., business hours pattern)
9
+ # 2. Burst detection - Many errors in short time period
10
+ #
11
+ # @example Cyclical pattern
12
+ # pattern = PatternDetector.analyze_cyclical_pattern(
13
+ # error_type: "NoMethodError",
14
+ # platform: "ios",
15
+ # days: 30
16
+ # )
17
+ # # => {
18
+ # # pattern_type: :business_hours,
19
+ # # peak_hours: [9, 10, 11, 14, 15],
20
+ # # hourly_distribution: { 0 => 5, 1 => 3, ... },
21
+ # # pattern_strength: 0.8
22
+ # # }
23
+ #
24
+ # @example Burst detection
25
+ # bursts = PatternDetector.detect_bursts(
26
+ # error_type: "NoMethodError",
27
+ # platform: "ios",
28
+ # days: 7
29
+ # )
30
+ # # => [{
31
+ # # start_time: <Time>,
32
+ # # end_time: <Time>,
33
+ # # duration_seconds: 300,
34
+ # # error_count: 25,
35
+ # # burst_intensity: :high
36
+ # # }]
37
+ class PatternDetector
38
+ # Analyze cyclical patterns in error occurrences
39
+ #
40
+ # Detects:
41
+ # - Business hours pattern (9am-5pm peak)
42
+ # - Night pattern (midnight-6am peak)
43
+ # - Weekend pattern (Sat-Sun peak)
44
+ # - Uniform pattern (no clear pattern)
45
+ #
46
+ # @param error_type [String] The error type to analyze
47
+ # @param platform [String] The platform (iOS, Android, API, etc.)
48
+ # @param days [Integer] Number of days to analyze (default: 30)
49
+ # @return [Hash] Pattern analysis with type, peaks, distribution, and strength
50
+ def self.analyze_cyclical_pattern(error_type:, platform:, days: 30)
51
+ start_date = days.days.ago
52
+
53
+ # Get all error occurrences for this error type/platform
54
+ errors = ErrorLog
55
+ .where(error_type: error_type, platform: platform)
56
+ .where("occurred_at >= ?", start_date)
57
+
58
+ return empty_pattern if errors.empty?
59
+
60
+ # Group by hour of day (0-23)
61
+ hourly_distribution = Hash.new(0)
62
+ weekday_distribution = Hash.new(0)
63
+
64
+ errors.each do |error|
65
+ hour = error.occurred_at.hour
66
+ wday = error.occurred_at.wday # 0 = Sunday, 6 = Saturday
67
+ hourly_distribution[hour] += 1
68
+ weekday_distribution[wday] += 1
69
+ end
70
+
71
+ # Calculate pattern type and peaks
72
+ pattern_type = determine_pattern_type(hourly_distribution, weekday_distribution)
73
+ peak_hours = find_peak_hours(hourly_distribution)
74
+ pattern_strength = calculate_pattern_strength(hourly_distribution)
75
+
76
+ {
77
+ pattern_type: pattern_type,
78
+ peak_hours: peak_hours,
79
+ hourly_distribution: hourly_distribution,
80
+ weekday_distribution: weekday_distribution,
81
+ pattern_strength: pattern_strength,
82
+ total_errors: errors.count,
83
+ analysis_days: days
84
+ }
85
+ end
86
+
87
+ # Detect error bursts (sequences where errors occur rapidly)
88
+ #
89
+ # A burst is defined as a sequence where inter-arrival time < 1 minute
90
+ # Burst intensity:
91
+ # - :high - 20+ errors in burst
92
+ # - :medium - 10-19 errors
93
+ # - :low - 5-9 errors
94
+ #
95
+ # @param error_type [String] The error type to analyze
96
+ # @param platform [String] The platform
97
+ # @param days [Integer] Number of days to analyze (default: 7)
98
+ # @return [Array<Hash>] Array of burst metadata
99
+ def self.detect_bursts(error_type:, platform:, days: 7)
100
+ start_date = days.days.ago
101
+
102
+ # Get all error occurrences sorted by time
103
+ errors = ErrorLog
104
+ .where(error_type: error_type, platform: platform)
105
+ .where("occurred_at >= ?", start_date)
106
+ .order(:occurred_at)
107
+
108
+ return [] if errors.count < 5 # Need at least 5 errors to detect a burst
109
+
110
+ # Get all occurrence timestamps
111
+ timestamps = errors.flat_map do |error|
112
+ # If error has error_occurrences, use those timestamps
113
+ if error.respond_to?(:error_occurrences) && error.error_occurrences.any?
114
+ error.error_occurrences.pluck(:occurred_at)
115
+ else
116
+ # Otherwise use the error's occurred_at repeated by occurrence_count
117
+ Array.new(error.occurrence_count || 1, error.occurred_at)
118
+ end
119
+ end.sort
120
+
121
+ return [] if timestamps.size < 5
122
+
123
+ # Detect bursts: sequences where inter-arrival < 60 seconds
124
+ bursts = []
125
+ current_burst = nil
126
+
127
+ timestamps.each_with_index do |timestamp, i|
128
+ next if i.zero?
129
+
130
+ inter_arrival = timestamp - timestamps[i - 1]
131
+
132
+ if inter_arrival <= 60 # 60 seconds threshold
133
+ # Start new burst or continue existing
134
+ if current_burst.nil?
135
+ current_burst = {
136
+ start_time: timestamps[i - 1],
137
+ timestamps: [ timestamps[i - 1], timestamp ]
138
+ }
139
+ else
140
+ current_burst[:timestamps] << timestamp
141
+ end
142
+ else
143
+ # End current burst if it exists and has enough errors
144
+ if current_burst && current_burst[:timestamps].size >= 5
145
+ bursts << finalize_burst(current_burst)
146
+ end
147
+ current_burst = nil
148
+ end
149
+ end
150
+
151
+ # Don't forget the last burst
152
+ if current_burst && current_burst[:timestamps].size >= 5
153
+ bursts << finalize_burst(current_burst)
154
+ end
155
+
156
+ bursts
157
+ end
158
+
159
+ private
160
+
161
+ # Empty pattern result
162
+ def self.empty_pattern
163
+ {
164
+ pattern_type: :none,
165
+ peak_hours: [],
166
+ hourly_distribution: {},
167
+ weekday_distribution: {},
168
+ pattern_strength: 0.0,
169
+ total_errors: 0,
170
+ analysis_days: 0
171
+ }
172
+ end
173
+
174
+ # Determine the pattern type based on hour and weekday distributions
175
+ def self.determine_pattern_type(hourly_dist, weekday_dist)
176
+ return :none if hourly_dist.empty?
177
+
178
+ # Calculate average errors per hour
179
+ avg_per_hour = hourly_dist.values.sum.to_f / 24
180
+
181
+ # Find peak hours (>2x average)
182
+ peak_hours = hourly_dist.select { |_, count| count > avg_per_hour * 2 }.keys.sort
183
+
184
+ # Business hours pattern: peaks between 9am-5pm
185
+ business_hours = (9..17).to_a
186
+ business_peaks = peak_hours & business_hours
187
+ if business_peaks.size >= 3
188
+ return :business_hours
189
+ end
190
+
191
+ # Night pattern: peaks between midnight-6am
192
+ night_hours = (0..6).to_a
193
+ night_peaks = peak_hours & night_hours
194
+ if night_peaks.size >= 2
195
+ return :night
196
+ end
197
+
198
+ # Weekend pattern: most errors on Sat/Sun
199
+ if weekday_dist.any?
200
+ weekend_count = (weekday_dist[0] || 0) + (weekday_dist[6] || 0) # Sun + Sat
201
+ total_count = weekday_dist.values.sum
202
+ if weekend_count > total_count * 0.5
203
+ return :weekend
204
+ end
205
+ end
206
+
207
+ # No clear pattern
208
+ :uniform
209
+ end
210
+
211
+ # Find peak hours (hours with >2x average)
212
+ def self.find_peak_hours(hourly_dist)
213
+ return [] if hourly_dist.empty?
214
+
215
+ avg = hourly_dist.values.sum.to_f / 24
216
+ hourly_dist.select { |_, count| count > avg * 2 }.keys.sort
217
+ end
218
+
219
+ # Calculate pattern strength (0.0-1.0)
220
+ # Measures how concentrated the errors are in peak hours
221
+ def self.calculate_pattern_strength(hourly_dist)
222
+ return 0.0 if hourly_dist.empty?
223
+
224
+ total = hourly_dist.values.sum
225
+ return 0.0 if total.zero?
226
+
227
+ # Calculate coefficient of variation (std dev / mean)
228
+ # Higher variation = stronger pattern
229
+ values = (0..23).map { |h| hourly_dist[h] || 0 }
230
+ mean = total.to_f / 24
231
+ variance = values.sum { |v| (v - mean)**2 } / 24
232
+ std_dev = Math.sqrt(variance)
233
+
234
+ # Normalize to 0-1 scale (coefficient of variation)
235
+ # Divide by sqrt(mean) to get a rough 0-1 scale
236
+ cv = mean > 0 ? std_dev / mean : 0
237
+ [ cv.round(2), 1.0 ].min
238
+ end
239
+
240
+ # Finalize burst metadata
241
+ def self.finalize_burst(burst_data)
242
+ start_time = burst_data[:start_time]
243
+ end_time = burst_data[:timestamps].last
244
+ duration = end_time - start_time
245
+ count = burst_data[:timestamps].size
246
+
247
+ {
248
+ start_time: start_time,
249
+ end_time: end_time,
250
+ duration_seconds: duration.round(1),
251
+ error_count: count,
252
+ burst_intensity: classify_burst_intensity(count)
253
+ }
254
+ end
255
+
256
+ # Classify burst intensity based on error count
257
+ def self.classify_burst_intensity(count)
258
+ if count >= 20
259
+ :high
260
+ elsif count >= 10
261
+ :medium
262
+ else
263
+ :low
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ module Services
5
+ # Calculates similarity scores between errors using multiple algorithms
6
+ #
7
+ # Combines:
8
+ # - Backtrace similarity (Jaccard index on stack frames) - 70% weight
9
+ # - Message similarity (Levenshtein distance) - 30% weight
10
+ #
11
+ # Returns a similarity score from 0.0 (completely different) to 1.0 (identical)
12
+ class SimilarityCalculator
13
+ # Calculate similarity between two errors
14
+ #
15
+ # @param error1 [ErrorLog] First error to compare
16
+ # @param error2 [ErrorLog] Second error to compare
17
+ # @return [Float] Similarity score from 0.0 to 1.0
18
+ def self.call(error1, error2)
19
+ new(error1, error2).calculate
20
+ end
21
+
22
+ def initialize(error1, error2)
23
+ @error1 = error1
24
+ @error2 = error2
25
+ end
26
+
27
+ def calculate
28
+ # Quick return for same error
29
+ return 1.0 if @error1.id == @error2.id
30
+
31
+ # Quick return for different platforms (per user config - same platform only)
32
+ return 0.0 if different_platforms?
33
+
34
+ backtrace_score = calculate_backtrace_similarity
35
+ message_score = calculate_message_similarity
36
+
37
+ # Weighted combination: backtrace 70%, message 30%
38
+ (backtrace_score * 0.7) + (message_score * 0.3)
39
+ end
40
+
41
+ private
42
+
43
+ def different_platforms?
44
+ return false if @error1.platform.nil? || @error2.platform.nil?
45
+ @error1.platform != @error2.platform
46
+ end
47
+
48
+ # Calculate Jaccard similarity on backtrace frames
49
+ # Jaccard = intersection / union
50
+ def calculate_backtrace_similarity
51
+ frames1 = extract_frames(@error1.backtrace)
52
+ frames2 = extract_frames(@error2.backtrace)
53
+
54
+ return 0.0 if frames1.empty? || frames2.empty?
55
+
56
+ intersection = (frames1 & frames2).size
57
+ union = (frames1 | frames2).size
58
+
59
+ return 0.0 if union.zero?
60
+
61
+ intersection.to_f / union
62
+ end
63
+
64
+ # Calculate normalized Levenshtein distance on messages
65
+ # Returns 1.0 for identical messages, decreasing as they differ
66
+ def calculate_message_similarity
67
+ msg1 = normalize_message(@error1.message)
68
+ msg2 = normalize_message(@error2.message)
69
+
70
+ return 1.0 if msg1 == msg2
71
+ return 0.0 if msg1.empty? || msg2.empty?
72
+
73
+ distance = levenshtein_distance(msg1, msg2)
74
+ max_length = [ msg1.length, msg2.length ].max
75
+
76
+ return 0.0 if max_length.zero?
77
+
78
+ # Convert distance to similarity (1.0 = identical, 0.0 = completely different)
79
+ 1.0 - (distance.to_f / max_length)
80
+ end
81
+
82
+ # Extract meaningful frames from backtrace
83
+ # Format: "file.rb:123:in `method_name`" => "file.rb:method_name"
84
+ def extract_frames(backtrace)
85
+ return [] if backtrace.blank?
86
+
87
+ lines = backtrace.is_a?(String) ? backtrace.split("\n") : backtrace
88
+ lines.first(20).map do |line| # Only consider first 20 frames
89
+ # Extract file path and method name, ignore line numbers
90
+ if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
91
+ "#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
92
+ elsif line =~ %r{([^/]+\.rb)}
93
+ Regexp.last_match(1)
94
+ end
95
+ end.compact.uniq
96
+ end
97
+
98
+ # Normalize message for comparison
99
+ # Already normalized during error logging, but ensure consistency
100
+ def normalize_message(message)
101
+ return "" if message.nil?
102
+
103
+ message
104
+ .gsub(/#<\w+:0x[0-9a-f]+>/i, "__OBJ__") # Object inspections - temp placeholder
105
+ .gsub(/0x[0-9a-f]+/i, "__HEX__") # Hex addresses - temp placeholder
106
+ .gsub(/"[^"]*"/, '""') # Quoted strings to ""
107
+ .gsub(/'[^']*'/, "''") # Single quotes to ''
108
+ .downcase # Convert to lowercase
109
+ .gsub(/\d+/, "n") # Numbers to n (lowercase)
110
+ .gsub(/__hex__/, "0xhex") # Replace placeholder with final value
111
+ .gsub(/__obj__/, "#<obj>") # Replace placeholder with final value
112
+ .strip
113
+ end
114
+
115
+ # Calculate Levenshtein distance (edit distance) between two strings
116
+ # Classic dynamic programming algorithm
117
+ def levenshtein_distance(str1, str2)
118
+ return str2.length if str1.empty?
119
+ return str1.length if str2.empty?
120
+
121
+ # Create matrix
122
+ matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1, 0) }
123
+
124
+ # Initialize first row and column
125
+ (0..str1.length).each { |i| matrix[i][0] = i }
126
+ (0..str2.length).each { |j| matrix[0][j] = j }
127
+
128
+ # Fill matrix
129
+ (1..str1.length).each do |i|
130
+ (1..str2.length).each do |j|
131
+ cost = str1[i - 1] == str2[j - 1] ? 0 : 1
132
+ matrix[i][j] = [
133
+ matrix[i - 1][j] + 1, # deletion
134
+ matrix[i][j - 1] + 1, # insertion
135
+ matrix[i - 1][j - 1] + cost # substitution
136
+ ].min
137
+ end
138
+ end
139
+
140
+ matrix[str1.length][str2.length]
141
+ end
142
+ end
143
+ end
144
+ end
@@ -6,7 +6,7 @@ module RailsErrorDashboard
6
6
  # Extracts and normalizes context information from various sources
7
7
  class ErrorContext
8
8
  attr_reader :user_id, :request_url, :request_params, :user_agent, :ip_address, :platform,
9
- :controller_name, :action_name
9
+ :controller_name, :action_name, :request_id, :session_id
10
10
 
11
11
  def initialize(context, source = nil)
12
12
  @context = context
@@ -20,6 +20,8 @@ module RailsErrorDashboard
20
20
  @platform = detect_platform
21
21
  @controller_name = extract_controller_name
22
22
  @action_name = extract_action_name
23
+ @request_id = extract_request_id
24
+ @session_id = extract_session_id
23
25
  end
24
26
 
25
27
  def to_h
@@ -143,6 +145,30 @@ module RailsErrorDashboard
143
145
 
144
146
  nil
145
147
  end
148
+
149
+ def extract_request_id
150
+ # From Rails request
151
+ return @context[:request]&.request_id if @context[:request]&.respond_to?(:request_id)
152
+
153
+ # From explicit context
154
+ return @context[:request_id] if @context[:request_id]
155
+
156
+ # From job ID (for background jobs)
157
+ return @context[:job]&.job_id if @context[:job]
158
+ return @context[:jid] if @context[:jid]
159
+
160
+ nil
161
+ end
162
+
163
+ def extract_session_id
164
+ # From Rails session
165
+ return @context[:request]&.session&.id if @context[:request]&.session
166
+
167
+ # From explicit context
168
+ return @context[:session_id] if @context[:session_id]
169
+
170
+ nil
171
+ end
146
172
  end
147
173
  end
148
174
  end
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -11,6 +11,16 @@ require "httparty"
11
11
  # Core library files
12
12
  require "rails_error_dashboard/value_objects/error_context"
13
13
  require "rails_error_dashboard/services/platform_detector"
14
+ require "rails_error_dashboard/services/similarity_calculator"
15
+ require "rails_error_dashboard/services/cascade_detector"
16
+ require "rails_error_dashboard/services/baseline_calculator"
17
+ require "rails_error_dashboard/services/baseline_alert_throttler"
18
+ require "rails_error_dashboard/services/pattern_detector"
19
+ require "rails_error_dashboard/queries/co_occurring_errors"
20
+ require "rails_error_dashboard/queries/error_cascades"
21
+ require "rails_error_dashboard/queries/baseline_stats"
22
+ require "rails_error_dashboard/queries/platform_comparison"
23
+ require "rails_error_dashboard/queries/error_correlation"
14
24
  require "rails_error_dashboard/commands/log_error"
15
25
  require "rails_error_dashboard/commands/resolve_error"
16
26
  require "rails_error_dashboard/commands/batch_resolve_errors"
@@ -19,6 +29,7 @@ require "rails_error_dashboard/queries/errors_list"
19
29
  require "rails_error_dashboard/queries/dashboard_stats"
20
30
  require "rails_error_dashboard/queries/analytics_stats"
21
31
  require "rails_error_dashboard/queries/filter_options"
32
+ require "rails_error_dashboard/queries/similar_errors"
22
33
  require "rails_error_dashboard/error_reporter"
23
34
  require "rails_error_dashboard/middleware/error_catcher"
24
35
 
@@ -28,12 +39,22 @@ require "rails_error_dashboard/plugin_registry"
28
39
 
29
40
  module RailsErrorDashboard
30
41
  class << self
31
- attr_accessor :configuration
32
- end
42
+ attr_writer :configuration
43
+
44
+ # Get or initialize configuration
45
+ def configuration
46
+ @configuration ||= Configuration.new
47
+ end
33
48
 
34
- def self.configure
35
- self.configuration ||= Configuration.new
36
- yield(configuration)
49
+ # Configure the gem
50
+ def configure
51
+ yield(configuration)
52
+ end
53
+
54
+ # Reset configuration to defaults
55
+ def reset_configuration!
56
+ @configuration = Configuration.new
57
+ end
37
58
  end
38
59
 
39
60
  # Register a plugin
@@ -55,6 +76,33 @@ module RailsErrorDashboard
55
76
  PluginRegistry.plugins
56
77
  end
57
78
 
58
- # Initialize with default configuration
59
- self.configuration = Configuration.new
79
+ # Register a callback for when any error is logged
80
+ # @param block [Proc] The callback to execute, receives error_log as parameter
81
+ # @example
82
+ # RailsErrorDashboard.on_error_logged do |error_log|
83
+ # puts "Error logged: #{error_log.error_type}"
84
+ # end
85
+ def self.on_error_logged(&block)
86
+ configuration.notification_callbacks[:error_logged] << block if block_given?
87
+ end
88
+
89
+ # Register a callback for when a critical error is logged
90
+ # @param block [Proc] The callback to execute, receives error_log as parameter
91
+ # @example
92
+ # RailsErrorDashboard.on_critical_error do |error_log|
93
+ # PagerDuty.trigger(error_log)
94
+ # end
95
+ def self.on_critical_error(&block)
96
+ configuration.notification_callbacks[:critical_error] << block if block_given?
97
+ end
98
+
99
+ # Register a callback for when an error is resolved
100
+ # @param block [Proc] The callback to execute, receives error_log as parameter
101
+ # @example
102
+ # RailsErrorDashboard.on_error_resolved do |error_log|
103
+ # Slack.notify("Error #{error_log.id} resolved")
104
+ # end
105
+ def self.on_error_resolved(&block)
106
+ configuration.notification_callbacks[:error_resolved] << block if block_given?
107
+ end
60
108
  end