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