rails_error_dashboard 0.1.0 → 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.
- checksums.yaml +4 -4
- data/README.md +305 -703
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css +926 -15
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +140 -4
- data/app/helpers/rails_error_dashboard/application_helper.rb +55 -0
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
- data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
- data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
- data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
- data/app/models/rails_error_dashboard/error_comment.rb +27 -0
- data/app/models/rails_error_dashboard/error_log.rb +471 -3
- data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +816 -178
- data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +78 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +152 -56
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +294 -138
- data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +399 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +781 -65
- data/config/routes.rb +9 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
- data/db/migrate/20251226020100_create_error_comments.rb +18 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +276 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +272 -37
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
- data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +272 -7
- data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
- data/lib/rails_error_dashboard/configuration.rb +90 -5
- data/lib/rails_error_dashboard/error_reporter.rb +15 -7
- data/lib/rails_error_dashboard/logger.rb +105 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
- data/lib/rails_error_dashboard/plugin.rb +6 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +3 -4
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -3
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +242 -2
- data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
- data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +106 -10
- data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
- data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
- data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
- data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
- data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +57 -7
- metadata +69 -10
- data/app/models/rails_error_dashboard/application_record.rb +0 -5
- data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
- data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Queries
|
|
5
|
+
# Query object for error correlation analysis
|
|
6
|
+
#
|
|
7
|
+
# Provides analytics for correlating errors with:
|
|
8
|
+
# - Releases (app_version, git_sha)
|
|
9
|
+
# - Users (affected users, multi-error users)
|
|
10
|
+
# - Time patterns (hour-of-day correlation)
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# correlation = ErrorCorrelation.new(days: 30)
|
|
14
|
+
# correlation.errors_by_version
|
|
15
|
+
# # => { "1.0.0" => { count: 100, error_types: 15, critical_count: 5 } }
|
|
16
|
+
class ErrorCorrelation
|
|
17
|
+
attr_reader :days
|
|
18
|
+
|
|
19
|
+
# @param days [Integer] Number of days to analyze (default: 30)
|
|
20
|
+
def initialize(days: 30)
|
|
21
|
+
@days = days
|
|
22
|
+
@start_date = days.days.ago
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get error statistics grouped by app version
|
|
26
|
+
# @return [Hash] Version => { count, error_types, critical_count, platforms }
|
|
27
|
+
def errors_by_version
|
|
28
|
+
return {} unless has_version_column?
|
|
29
|
+
|
|
30
|
+
versions = base_query
|
|
31
|
+
.where.not(app_version: nil)
|
|
32
|
+
.group(:app_version)
|
|
33
|
+
.count
|
|
34
|
+
|
|
35
|
+
versions.each_with_object({}) do |(version, count), result|
|
|
36
|
+
errors = base_query.where(app_version: version)
|
|
37
|
+
|
|
38
|
+
# Count unique error types
|
|
39
|
+
error_types = errors.distinct.pluck(:error_type).count
|
|
40
|
+
|
|
41
|
+
# Count critical errors
|
|
42
|
+
critical_count = errors.select { |error| error.severity == :critical }.count
|
|
43
|
+
|
|
44
|
+
# Get platforms for this version
|
|
45
|
+
platforms = errors.distinct.pluck(:platform).compact
|
|
46
|
+
|
|
47
|
+
result[version] = {
|
|
48
|
+
count: count,
|
|
49
|
+
error_types: error_types,
|
|
50
|
+
critical_count: critical_count,
|
|
51
|
+
platforms: platforms,
|
|
52
|
+
first_seen: errors.minimum(:occurred_at),
|
|
53
|
+
last_seen: errors.maximum(:occurred_at)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get error statistics grouped by git SHA
|
|
59
|
+
# @return [Hash] SHA => { count, error_types, app_version }
|
|
60
|
+
def errors_by_git_sha
|
|
61
|
+
return {} unless has_git_sha_column?
|
|
62
|
+
|
|
63
|
+
shas = base_query
|
|
64
|
+
.where.not(git_sha: nil)
|
|
65
|
+
.group(:git_sha)
|
|
66
|
+
.count
|
|
67
|
+
|
|
68
|
+
shas.each_with_object({}) do |(sha, count), result|
|
|
69
|
+
errors = base_query.where(git_sha: sha)
|
|
70
|
+
|
|
71
|
+
# Get associated version (may be multiple)
|
|
72
|
+
versions = errors.distinct.pluck(:app_version).compact
|
|
73
|
+
|
|
74
|
+
result[sha] = {
|
|
75
|
+
count: count,
|
|
76
|
+
error_types: errors.distinct.pluck(:error_type).count,
|
|
77
|
+
app_versions: versions,
|
|
78
|
+
first_seen: errors.minimum(:occurred_at),
|
|
79
|
+
last_seen: errors.maximum(:occurred_at)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Find problematic releases (versions with >2x average error rate)
|
|
85
|
+
# @return [Array<Hash>] Array of problematic version data
|
|
86
|
+
def problematic_releases
|
|
87
|
+
return [] unless has_version_column?
|
|
88
|
+
|
|
89
|
+
versions_data = errors_by_version
|
|
90
|
+
return [] if versions_data.empty?
|
|
91
|
+
|
|
92
|
+
total_errors = versions_data.values.map { |v| v[:count] }.sum
|
|
93
|
+
avg_errors = total_errors.to_f / versions_data.count
|
|
94
|
+
threshold = avg_errors * 2
|
|
95
|
+
|
|
96
|
+
versions_data
|
|
97
|
+
.select { |_, data| data[:count] > threshold }
|
|
98
|
+
.map do |version, data|
|
|
99
|
+
deviation = avg_errors > 0 ? ((data[:count] - avg_errors) / avg_errors * 100).round(1) : 0.0
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
version: version,
|
|
103
|
+
error_count: data[:count],
|
|
104
|
+
deviation_from_avg: deviation,
|
|
105
|
+
critical_count: data[:critical_count],
|
|
106
|
+
error_types: data[:error_types],
|
|
107
|
+
platforms: data[:platforms]
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
.sort_by { |v| -v[:error_count] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Find users affected by multiple different error types
|
|
114
|
+
# @param min_error_types [Integer] Minimum number of different error types (default: 2)
|
|
115
|
+
# @return [Array<Hash>] Users with multiple error type exposure
|
|
116
|
+
def multi_error_users(min_error_types: 2)
|
|
117
|
+
users_with_errors = base_query
|
|
118
|
+
.where.not(user_id: nil)
|
|
119
|
+
.group(:user_id, :error_type)
|
|
120
|
+
.count
|
|
121
|
+
|
|
122
|
+
# Group by user_id
|
|
123
|
+
users_by_id = users_with_errors.group_by { |(user_id, _), _| user_id }
|
|
124
|
+
|
|
125
|
+
users_by_id
|
|
126
|
+
.select { |_, error_data| error_data.count >= min_error_types }
|
|
127
|
+
.map do |user_id, error_data|
|
|
128
|
+
error_type_names = error_data.map { |(_, type), _| type }
|
|
129
|
+
total_errors = error_data.map { |_, count| count }.sum
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
user_id: user_id,
|
|
133
|
+
user_email: find_user_email(user_id),
|
|
134
|
+
error_types: error_type_names,
|
|
135
|
+
error_type_count: error_type_names.count,
|
|
136
|
+
total_errors: total_errors
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
.sort_by { |u| -u[:error_type_count] }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Calculate user overlap between two error types
|
|
143
|
+
# Returns percentage of users affected by both errors
|
|
144
|
+
# @param error_type_a [String] First error type
|
|
145
|
+
# @param error_type_b [String] Second error type
|
|
146
|
+
# @return [Hash] Overlap statistics
|
|
147
|
+
def error_type_user_overlap(error_type_a, error_type_b)
|
|
148
|
+
users_a = base_query
|
|
149
|
+
.where(error_type: error_type_a)
|
|
150
|
+
.where.not(user_id: nil)
|
|
151
|
+
.distinct
|
|
152
|
+
.pluck(:user_id)
|
|
153
|
+
|
|
154
|
+
users_b = base_query
|
|
155
|
+
.where(error_type: error_type_b)
|
|
156
|
+
.where.not(user_id: nil)
|
|
157
|
+
.distinct
|
|
158
|
+
.pluck(:user_id)
|
|
159
|
+
|
|
160
|
+
overlap = users_a & users_b
|
|
161
|
+
|
|
162
|
+
{
|
|
163
|
+
error_type_a: error_type_a,
|
|
164
|
+
error_type_b: error_type_b,
|
|
165
|
+
users_a_count: users_a.count,
|
|
166
|
+
users_b_count: users_b.count,
|
|
167
|
+
overlap_count: overlap.count,
|
|
168
|
+
overlap_percentage: calculate_percentage(overlap.count, [ users_a.count, users_b.count ].min),
|
|
169
|
+
overlapping_user_ids: overlap.first(10) # Sample of overlapping users
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Analyze time-based correlation between error types
|
|
174
|
+
# Finds error types that tend to occur at similar hours of day
|
|
175
|
+
# @return [Hash] Error type pairs with correlation scores
|
|
176
|
+
def time_correlated_errors
|
|
177
|
+
# Get hourly distribution for each error type
|
|
178
|
+
error_types = base_query.distinct.pluck(:error_type)
|
|
179
|
+
return {} if error_types.count < 2
|
|
180
|
+
|
|
181
|
+
hourly_distributions = {}
|
|
182
|
+
error_types.each do |error_type|
|
|
183
|
+
distribution = base_query
|
|
184
|
+
.where(error_type: error_type)
|
|
185
|
+
.group_by { |error| error.occurred_at.hour }
|
|
186
|
+
.transform_values(&:count)
|
|
187
|
+
|
|
188
|
+
# Normalize to 0-23 hours
|
|
189
|
+
hourly_distributions[error_type] = (0..23).map { |h| distribution[h] || 0 }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Calculate correlation between error type pairs
|
|
193
|
+
correlations = {}
|
|
194
|
+
error_types.combination(2).each do |type_a, type_b|
|
|
195
|
+
correlation = calculate_time_correlation(
|
|
196
|
+
hourly_distributions[type_a],
|
|
197
|
+
hourly_distributions[type_b]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Only include significant correlations (>0.5)
|
|
201
|
+
if correlation > 0.5
|
|
202
|
+
correlations["#{type_a} <-> #{type_b}"] = {
|
|
203
|
+
error_type_a: type_a,
|
|
204
|
+
error_type_b: type_b,
|
|
205
|
+
correlation: correlation,
|
|
206
|
+
strength: classify_correlation_strength(correlation)
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
correlations.sort_by { |_, v| -v[:correlation] }.to_h
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Compare error rates across different time periods
|
|
215
|
+
# @return [Hash] Comparison of current vs previous period
|
|
216
|
+
def period_comparison
|
|
217
|
+
current_start = (@days / 2).days.ago
|
|
218
|
+
previous_start = @start_date
|
|
219
|
+
previous_end = current_start
|
|
220
|
+
|
|
221
|
+
current_errors = ErrorLog
|
|
222
|
+
.where("occurred_at >= ?", current_start)
|
|
223
|
+
.count
|
|
224
|
+
|
|
225
|
+
previous_errors = ErrorLog
|
|
226
|
+
.where("occurred_at >= ? AND occurred_at < ?", previous_start, previous_end)
|
|
227
|
+
.count
|
|
228
|
+
|
|
229
|
+
change_percentage = if previous_errors > 0
|
|
230
|
+
((current_errors - previous_errors).to_f / previous_errors * 100).round(1)
|
|
231
|
+
else
|
|
232
|
+
current_errors > 0 ? 100.0 : 0.0
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
current_period: {
|
|
237
|
+
start: current_start,
|
|
238
|
+
end: Time.current,
|
|
239
|
+
count: current_errors
|
|
240
|
+
},
|
|
241
|
+
previous_period: {
|
|
242
|
+
start: previous_start,
|
|
243
|
+
end: previous_end,
|
|
244
|
+
count: previous_errors
|
|
245
|
+
},
|
|
246
|
+
change: current_errors - previous_errors,
|
|
247
|
+
change_percentage: change_percentage,
|
|
248
|
+
trend: determine_trend(change_percentage)
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Get top error types by platform
|
|
253
|
+
# Shows which errors are platform-specific vs cross-platform
|
|
254
|
+
# @return [Hash] Platform => top error types
|
|
255
|
+
def platform_specific_errors
|
|
256
|
+
platforms = base_query.distinct.pluck(:platform).compact
|
|
257
|
+
|
|
258
|
+
platforms.each_with_object({}) do |platform, result|
|
|
259
|
+
platform_errors = base_query.where(platform: platform)
|
|
260
|
+
top_errors = platform_errors
|
|
261
|
+
.group(:error_type)
|
|
262
|
+
.count
|
|
263
|
+
.sort_by { |_, count| -count }
|
|
264
|
+
.first(5)
|
|
265
|
+
|
|
266
|
+
result[platform] = top_errors.map do |error_type, count|
|
|
267
|
+
# Check if this error occurs on other platforms
|
|
268
|
+
other_platforms = base_query
|
|
269
|
+
.where(error_type: error_type)
|
|
270
|
+
.where.not(platform: platform)
|
|
271
|
+
.distinct
|
|
272
|
+
.pluck(:platform)
|
|
273
|
+
.compact
|
|
274
|
+
|
|
275
|
+
{
|
|
276
|
+
error_type: error_type,
|
|
277
|
+
count: count,
|
|
278
|
+
platform_specific: other_platforms.empty?,
|
|
279
|
+
also_on: other_platforms
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def base_query
|
|
288
|
+
ErrorLog.where("occurred_at >= ?", @start_date)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Check if app_version column exists
|
|
292
|
+
def has_version_column?
|
|
293
|
+
ErrorLog.column_names.include?("app_version")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Check if git_sha column exists
|
|
297
|
+
def has_git_sha_column?
|
|
298
|
+
ErrorLog.column_names.include?("git_sha")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Find user email
|
|
302
|
+
def find_user_email(user_id)
|
|
303
|
+
user_model = RailsErrorDashboard.configuration.user_model
|
|
304
|
+
user = user_model.constantize.find_by(id: user_id)
|
|
305
|
+
user&.email || "User ##{user_id}"
|
|
306
|
+
rescue StandardError
|
|
307
|
+
"User ##{user_id}"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Calculate percentage
|
|
311
|
+
def calculate_percentage(part, whole)
|
|
312
|
+
return 0.0 if whole.zero?
|
|
313
|
+
(part.to_f / whole * 100).round(1)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Calculate Pearson correlation coefficient between two time series
|
|
317
|
+
def calculate_time_correlation(series_a, series_b)
|
|
318
|
+
return 0.0 if series_a.sum.zero? || series_b.sum.zero?
|
|
319
|
+
|
|
320
|
+
n = series_a.length
|
|
321
|
+
return 0.0 if n.zero?
|
|
322
|
+
|
|
323
|
+
# Calculate means
|
|
324
|
+
mean_a = series_a.sum.to_f / n
|
|
325
|
+
mean_b = series_b.sum.to_f / n
|
|
326
|
+
|
|
327
|
+
# Calculate covariance and standard deviations
|
|
328
|
+
covariance = 0.0
|
|
329
|
+
std_a = 0.0
|
|
330
|
+
std_b = 0.0
|
|
331
|
+
|
|
332
|
+
n.times do |i|
|
|
333
|
+
diff_a = series_a[i] - mean_a
|
|
334
|
+
diff_b = series_b[i] - mean_b
|
|
335
|
+
covariance += diff_a * diff_b
|
|
336
|
+
std_a += diff_a**2
|
|
337
|
+
std_b += diff_b**2
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Avoid division by zero
|
|
341
|
+
denominator = Math.sqrt(std_a * std_b)
|
|
342
|
+
return 0.0 if denominator.zero?
|
|
343
|
+
|
|
344
|
+
(covariance / denominator).round(3)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Classify correlation strength
|
|
348
|
+
def classify_correlation_strength(correlation)
|
|
349
|
+
abs_corr = correlation.abs
|
|
350
|
+
if abs_corr >= 0.8
|
|
351
|
+
:strong
|
|
352
|
+
elsif abs_corr >= 0.5
|
|
353
|
+
:moderate
|
|
354
|
+
else
|
|
355
|
+
:weak
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Determine trend based on change percentage
|
|
360
|
+
def determine_trend(change_percentage)
|
|
361
|
+
if change_percentage > 20
|
|
362
|
+
:increasing_significantly
|
|
363
|
+
elsif change_percentage > 5
|
|
364
|
+
:increasing
|
|
365
|
+
elsif change_percentage < -20
|
|
366
|
+
:decreasing_significantly
|
|
367
|
+
elsif change_percentage < -5
|
|
368
|
+
:decreasing
|
|
369
|
+
else
|
|
370
|
+
:stable
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
@@ -24,20 +24,19 @@ module RailsErrorDashboard
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def apply_filters(query)
|
|
27
|
-
query = filter_by_environment(query)
|
|
28
27
|
query = filter_by_error_type(query)
|
|
29
28
|
query = filter_by_resolved(query)
|
|
30
29
|
query = filter_by_platform(query)
|
|
31
30
|
query = filter_by_search(query)
|
|
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
|
|
|
35
|
-
def filter_by_environment(query)
|
|
36
|
-
return query unless @filters[:environment].present?
|
|
37
|
-
|
|
38
|
-
query.where(environment: @filters[:environment])
|
|
39
|
-
end
|
|
40
|
-
|
|
41
40
|
def filter_by_error_type(query)
|
|
42
41
|
return query unless @filters[:error_type].present?
|
|
43
42
|
|
|
@@ -45,9 +44,25 @@ module RailsErrorDashboard
|
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def filter_by_resolved(query)
|
|
48
|
-
|
|
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
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
case @filters[:unresolved]
|
|
53
|
+
when false, "false", "0"
|
|
54
|
+
# Explicitly show all errors (resolved and unresolved)
|
|
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
|
|
62
|
+
else
|
|
63
|
+
# Fallback: show only unresolved errors
|
|
64
|
+
query.unresolved
|
|
65
|
+
end
|
|
51
66
|
end
|
|
52
67
|
|
|
53
68
|
def filter_by_platform(query)
|
|
@@ -59,7 +74,88 @@ module RailsErrorDashboard
|
|
|
59
74
|
def filter_by_search(query)
|
|
60
75
|
return query unless @filters[:search].present?
|
|
61
76
|
|
|
62
|
-
|
|
77
|
+
# Use PostgreSQL full-text search if available (much faster with GIN index)
|
|
78
|
+
# Otherwise fall back to LIKE query
|
|
79
|
+
if postgresql?
|
|
80
|
+
# Use to_tsquery for full-text search with GIN index
|
|
81
|
+
# This is dramatically faster on large datasets
|
|
82
|
+
search_term = @filters[:search].split.map { |word| "#{word}:*" }.join(" & ")
|
|
83
|
+
query.where("to_tsvector('english', message) @@ to_tsquery('english', ?)", search_term)
|
|
84
|
+
else
|
|
85
|
+
# Fall back to LIKE for SQLite/MySQL
|
|
86
|
+
# Use LOWER() for case-insensitive search
|
|
87
|
+
query.where("LOWER(message) LIKE LOWER(?)", "%#{@filters[:search]}%")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def postgresql?
|
|
92
|
+
ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def filter_by_severity(query)
|
|
96
|
+
return query unless @filters[:severity].present?
|
|
97
|
+
|
|
98
|
+
# Map severity levels to error types
|
|
99
|
+
error_types = case @filters[:severity].to_sym
|
|
100
|
+
when :critical
|
|
101
|
+
ErrorLog::CRITICAL_ERROR_TYPES
|
|
102
|
+
when :high
|
|
103
|
+
ErrorLog::HIGH_SEVERITY_ERROR_TYPES
|
|
104
|
+
when :medium
|
|
105
|
+
ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES
|
|
106
|
+
when :low
|
|
107
|
+
# Low severity = everything NOT in the other categories
|
|
108
|
+
all_categorized = ErrorLog::CRITICAL_ERROR_TYPES +
|
|
109
|
+
ErrorLog::HIGH_SEVERITY_ERROR_TYPES +
|
|
110
|
+
ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES
|
|
111
|
+
# Use NOT IN to filter out categorized errors
|
|
112
|
+
return query.where.not(error_type: all_categorized)
|
|
113
|
+
else
|
|
114
|
+
return query
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
query.where(error_type: error_types)
|
|
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
|
|
63
159
|
end
|
|
64
160
|
end
|
|
65
161
|
end
|