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
|
@@ -6,7 +6,29 @@ module RailsErrorDashboard
|
|
|
6
6
|
# This is a write operation that creates an ErrorLog record
|
|
7
7
|
class LogError
|
|
8
8
|
def self.call(exception, context = {})
|
|
9
|
-
|
|
9
|
+
# Check if async logging is enabled
|
|
10
|
+
if RailsErrorDashboard.configuration.async_logging
|
|
11
|
+
# For async logging, just enqueue the job
|
|
12
|
+
# All filtering happens when the job runs
|
|
13
|
+
call_async(exception, context)
|
|
14
|
+
else
|
|
15
|
+
# For sync logging, execute immediately
|
|
16
|
+
new(exception, context).call
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Queue error logging as a background job
|
|
21
|
+
def self.call_async(exception, context = {})
|
|
22
|
+
# Serialize exception data for the job
|
|
23
|
+
exception_data = {
|
|
24
|
+
class_name: exception.class.name,
|
|
25
|
+
message: exception.message,
|
|
26
|
+
backtrace: exception.backtrace
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Enqueue the async job using ActiveJob
|
|
30
|
+
# The queue adapter (:sidekiq, :solid_queue, :async) is configured separately
|
|
31
|
+
AsyncErrorLoggingJob.perform_later(exception_data, context)
|
|
10
32
|
end
|
|
11
33
|
|
|
12
34
|
def initialize(exception, context = {})
|
|
@@ -15,19 +37,26 @@ module RailsErrorDashboard
|
|
|
15
37
|
end
|
|
16
38
|
|
|
17
39
|
def call
|
|
40
|
+
# Check if this exception should be ignored
|
|
41
|
+
return nil if should_ignore_exception?(@exception)
|
|
42
|
+
|
|
43
|
+
# Check sampling rate for non-critical errors
|
|
44
|
+
# Critical errors are ALWAYS logged regardless of sampling
|
|
45
|
+
return nil if should_skip_due_to_sampling?(@exception)
|
|
46
|
+
|
|
18
47
|
error_context = ValueObjects::ErrorContext.new(@context, @context[:source])
|
|
19
48
|
|
|
20
49
|
# Build error attributes
|
|
50
|
+
truncated_backtrace = truncate_backtrace(@exception.backtrace)
|
|
21
51
|
attributes = {
|
|
22
52
|
error_type: @exception.class.name,
|
|
23
53
|
message: @exception.message,
|
|
24
|
-
backtrace:
|
|
54
|
+
backtrace: truncated_backtrace,
|
|
25
55
|
user_id: error_context.user_id,
|
|
26
56
|
request_url: error_context.request_url,
|
|
27
57
|
request_params: error_context.request_params,
|
|
28
58
|
user_agent: error_context.user_agent,
|
|
29
59
|
ip_address: error_context.ip_address,
|
|
30
|
-
environment: Rails.env,
|
|
31
60
|
platform: error_context.platform,
|
|
32
61
|
controller_name: error_context.controller_name,
|
|
33
62
|
action_name: error_context.action_name,
|
|
@@ -37,31 +66,174 @@ module RailsErrorDashboard
|
|
|
37
66
|
# Generate error hash for deduplication (including controller/action context)
|
|
38
67
|
error_hash = generate_error_hash(@exception, error_context.controller_name, error_context.action_name)
|
|
39
68
|
|
|
69
|
+
# Calculate backtrace signature for fuzzy matching (if column exists)
|
|
70
|
+
if ErrorLog.column_names.include?("backtrace_signature")
|
|
71
|
+
attributes[:backtrace_signature] = calculate_backtrace_signature_from_backtrace(truncated_backtrace)
|
|
72
|
+
end
|
|
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
|
+
|
|
40
89
|
# Find existing error or create new one
|
|
41
90
|
# This ensures accurate occurrence tracking
|
|
42
91
|
error_log = ErrorLog.find_or_increment_by_hash(error_hash, attributes.merge(error_hash: error_hash))
|
|
43
92
|
|
|
93
|
+
# Track individual error occurrence for co-occurrence analysis (if table exists)
|
|
94
|
+
if defined?(ErrorOccurrence) && ErrorOccurrence.table_exists?
|
|
95
|
+
begin
|
|
96
|
+
ErrorOccurrence.create(
|
|
97
|
+
error_log: error_log,
|
|
98
|
+
occurred_at: attributes[:occurred_at],
|
|
99
|
+
user_id: attributes[:user_id],
|
|
100
|
+
request_id: error_context.request_id,
|
|
101
|
+
session_id: error_context.session_id
|
|
102
|
+
)
|
|
103
|
+
rescue => e
|
|
104
|
+
RailsErrorDashboard::Logger.error("Failed to create error occurrence: #{e.message}")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
44
108
|
# Send notifications only for new errors (not increments)
|
|
45
109
|
# Check if this is first occurrence or error was just created
|
|
46
110
|
if error_log.occurrence_count == 1
|
|
47
111
|
send_notifications(error_log)
|
|
48
112
|
# Dispatch plugin event for new error
|
|
49
113
|
PluginRegistry.dispatch(:on_error_logged, error_log)
|
|
114
|
+
# Trigger notification callbacks
|
|
115
|
+
trigger_callbacks(error_log)
|
|
116
|
+
# Emit ActiveSupport::Notifications instrumentation events
|
|
117
|
+
emit_instrumentation_events(error_log)
|
|
50
118
|
else
|
|
51
119
|
# Dispatch plugin event for error recurrence
|
|
52
120
|
PluginRegistry.dispatch(:on_error_recurred, error_log)
|
|
53
121
|
end
|
|
54
122
|
|
|
123
|
+
# Check for baseline anomalies
|
|
124
|
+
check_baseline_anomaly(error_log)
|
|
125
|
+
|
|
55
126
|
error_log
|
|
56
127
|
rescue => e
|
|
57
|
-
# Don't let error logging cause more errors
|
|
58
|
-
|
|
59
|
-
Rails
|
|
60
|
-
|
|
128
|
+
# Don't let error logging cause more errors - fail silently
|
|
129
|
+
# CRITICAL: Log but never propagate exception
|
|
130
|
+
# Log to Rails logger for visibility during development
|
|
131
|
+
Rails.logger.error("[RailsErrorDashboard] LogError command failed: #{e.class} - #{e.message}")
|
|
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
|
|
138
|
+
nil # Explicitly return nil, never raise
|
|
61
139
|
end
|
|
62
140
|
|
|
63
141
|
private
|
|
64
142
|
|
|
143
|
+
# Trigger notification callbacks for error logging
|
|
144
|
+
def trigger_callbacks(error_log)
|
|
145
|
+
# Trigger general error_logged callbacks
|
|
146
|
+
RailsErrorDashboard.configuration.notification_callbacks[:error_logged].each do |callback|
|
|
147
|
+
callback.call(error_log)
|
|
148
|
+
rescue => e
|
|
149
|
+
RailsErrorDashboard::Logger.error("Error in error_logged callback: #{e.message}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Trigger critical_error callbacks if this is a critical error
|
|
153
|
+
if error_log.critical?
|
|
154
|
+
RailsErrorDashboard.configuration.notification_callbacks[:critical_error].each do |callback|
|
|
155
|
+
callback.call(error_log)
|
|
156
|
+
rescue => e
|
|
157
|
+
RailsErrorDashboard::Logger.error("Error in critical_error callback: #{e.message}")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Emit ActiveSupport::Notifications instrumentation events
|
|
163
|
+
def emit_instrumentation_events(error_log)
|
|
164
|
+
# Payload for instrumentation subscribers
|
|
165
|
+
payload = {
|
|
166
|
+
error_log: error_log,
|
|
167
|
+
error_id: error_log.id,
|
|
168
|
+
error_type: error_log.error_type,
|
|
169
|
+
message: error_log.message,
|
|
170
|
+
severity: error_log.severity,
|
|
171
|
+
platform: error_log.platform,
|
|
172
|
+
occurred_at: error_log.occurred_at
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Emit general error_logged event
|
|
176
|
+
ActiveSupport::Notifications.instrument("error_logged.rails_error_dashboard", payload)
|
|
177
|
+
|
|
178
|
+
# Emit critical_error event if this is a critical error
|
|
179
|
+
if error_log.critical?
|
|
180
|
+
ActiveSupport::Notifications.instrument("critical_error.rails_error_dashboard", payload)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Check if error should be skipped due to sampling rate
|
|
185
|
+
# Critical errors are ALWAYS logged, sampling only applies to non-critical errors
|
|
186
|
+
def should_skip_due_to_sampling?(exception)
|
|
187
|
+
sampling_rate = RailsErrorDashboard.configuration.sampling_rate
|
|
188
|
+
|
|
189
|
+
# If sampling is 100% (1.0) or higher, log everything
|
|
190
|
+
return false if sampling_rate >= 1.0
|
|
191
|
+
|
|
192
|
+
# Always log critical errors regardless of sampling rate
|
|
193
|
+
# Check this BEFORE checking for 0% sampling
|
|
194
|
+
return false if is_critical_error?(exception)
|
|
195
|
+
|
|
196
|
+
# If sampling is 0% or negative, skip all non-critical errors
|
|
197
|
+
return true if sampling_rate <= 0.0
|
|
198
|
+
|
|
199
|
+
# For non-critical errors, use probabilistic sampling
|
|
200
|
+
# rand returns 0.0 to 1.0, so if sampling_rate is 0.1 (10%),
|
|
201
|
+
# we skip 90% of errors (when rand > 0.1)
|
|
202
|
+
rand > sampling_rate
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Check if exception is a critical error type
|
|
206
|
+
def is_critical_error?(exception)
|
|
207
|
+
exception_class_name = exception.class.name
|
|
208
|
+
RailsErrorDashboard::ErrorLog::CRITICAL_ERROR_TYPES.include?(exception_class_name)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Check if exception should be ignored based on configuration
|
|
212
|
+
# Supports both string class names and regex patterns
|
|
213
|
+
def should_ignore_exception?(exception)
|
|
214
|
+
ignored_exceptions = RailsErrorDashboard.configuration.ignored_exceptions
|
|
215
|
+
return false if ignored_exceptions.blank?
|
|
216
|
+
|
|
217
|
+
exception_class_name = exception.class.name
|
|
218
|
+
|
|
219
|
+
ignored_exceptions.any? do |ignored|
|
|
220
|
+
case ignored
|
|
221
|
+
when String
|
|
222
|
+
# Exact class name match (supports inheritance)
|
|
223
|
+
exception.is_a?(ignored.constantize)
|
|
224
|
+
when Regexp
|
|
225
|
+
# Regex pattern match on class name
|
|
226
|
+
exception_class_name.match?(ignored)
|
|
227
|
+
else
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
rescue NameError
|
|
231
|
+
# Handle invalid class names in configuration
|
|
232
|
+
RailsErrorDashboard::Logger.warn("Invalid ignored exception class: #{ignored}")
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
65
237
|
# Generate consistent hash for error deduplication
|
|
66
238
|
# Same hash = same error type
|
|
67
239
|
# Note: This is also defined in ErrorLog model for backward compatibility
|
|
@@ -101,6 +273,28 @@ module RailsErrorDashboard
|
|
|
101
273
|
Digest::SHA256.hexdigest(digest_input)[0..15]
|
|
102
274
|
end
|
|
103
275
|
|
|
276
|
+
# Truncate backtrace to configured maximum lines
|
|
277
|
+
# This reduces database storage and improves performance
|
|
278
|
+
def truncate_backtrace(backtrace)
|
|
279
|
+
return nil if backtrace.nil?
|
|
280
|
+
|
|
281
|
+
max_lines = RailsErrorDashboard.configuration.max_backtrace_lines
|
|
282
|
+
|
|
283
|
+
# Limit backtrace to max_lines
|
|
284
|
+
limited_backtrace = backtrace.first(max_lines)
|
|
285
|
+
|
|
286
|
+
# Join into string
|
|
287
|
+
result = limited_backtrace.join("\n")
|
|
288
|
+
|
|
289
|
+
# Add truncation notice if we cut lines
|
|
290
|
+
if backtrace.length > max_lines
|
|
291
|
+
truncation_notice = "... (#{backtrace.length - max_lines} more lines truncated)"
|
|
292
|
+
result = result.empty? ? truncation_notice : result + "\n" + truncation_notice
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
result
|
|
296
|
+
end
|
|
297
|
+
|
|
104
298
|
def send_notifications(error_log)
|
|
105
299
|
config = RailsErrorDashboard.configuration
|
|
106
300
|
|
|
@@ -129,6 +323,77 @@ module RailsErrorDashboard
|
|
|
129
323
|
WebhookErrorNotificationJob.perform_later(error_log.id)
|
|
130
324
|
end
|
|
131
325
|
end
|
|
326
|
+
|
|
327
|
+
# Calculate backtrace signature from backtrace string/array
|
|
328
|
+
# This matches the algorithm in ErrorLog#calculate_backtrace_signature
|
|
329
|
+
def calculate_backtrace_signature_from_backtrace(backtrace)
|
|
330
|
+
return nil if backtrace.blank?
|
|
331
|
+
|
|
332
|
+
lines = backtrace.is_a?(String) ? backtrace.split("\n") : backtrace
|
|
333
|
+
frames = lines.first(20).map do |line|
|
|
334
|
+
# Extract file path and method name, ignore line numbers
|
|
335
|
+
if line =~ %r{([^/]+\.rb):.*?in `(.+)'$}
|
|
336
|
+
"#{Regexp.last_match(1)}:#{Regexp.last_match(2)}"
|
|
337
|
+
elsif line =~ %r{([^/]+\.rb)}
|
|
338
|
+
Regexp.last_match(1)
|
|
339
|
+
end
|
|
340
|
+
end.compact.uniq
|
|
341
|
+
|
|
342
|
+
return nil if frames.empty?
|
|
343
|
+
|
|
344
|
+
# Create signature from sorted file paths (order-independent)
|
|
345
|
+
file_paths = frames.map { |frame| frame.split(":").first }.sort
|
|
346
|
+
Digest::SHA256.hexdigest(file_paths.join("|"))[0..15]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Check if error exceeds baseline and send alert if needed
|
|
350
|
+
def check_baseline_anomaly(error_log)
|
|
351
|
+
config = RailsErrorDashboard.configuration
|
|
352
|
+
|
|
353
|
+
# Return early if baseline alerts are disabled
|
|
354
|
+
return unless config.enable_baseline_alerts
|
|
355
|
+
return unless defined?(Queries::BaselineStats)
|
|
356
|
+
return unless defined?(BaselineAlertJob)
|
|
357
|
+
|
|
358
|
+
# Get baseline anomaly info
|
|
359
|
+
anomaly = error_log.baseline_anomaly(sensitivity: config.baseline_alert_threshold_std_devs)
|
|
360
|
+
|
|
361
|
+
# Return if no anomaly detected
|
|
362
|
+
return unless anomaly[:anomaly]
|
|
363
|
+
|
|
364
|
+
# Check if severity level should trigger alert
|
|
365
|
+
return unless config.baseline_alert_severities.include?(anomaly[:level])
|
|
366
|
+
|
|
367
|
+
# Enqueue alert job (which will handle throttling)
|
|
368
|
+
BaselineAlertJob.perform_later(error_log.id, anomaly)
|
|
369
|
+
|
|
370
|
+
RailsErrorDashboard::Logger.info(
|
|
371
|
+
"Baseline alert queued for #{error_log.error_type} on #{error_log.platform}: " \
|
|
372
|
+
"#{anomaly[:level]} (#{anomaly[:std_devs_above]&.round(1)}σ above baseline)"
|
|
373
|
+
)
|
|
374
|
+
rescue => e
|
|
375
|
+
# Don't let baseline alerting cause errors
|
|
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
|
|
396
|
+
end
|
|
132
397
|
end
|
|
133
398
|
end
|
|
134
399
|
end
|
|
@@ -28,6 +28,22 @@ module RailsErrorDashboard
|
|
|
28
28
|
# Dispatch plugin event for resolved error
|
|
29
29
|
PluginRegistry.dispatch(:on_error_resolved, error)
|
|
30
30
|
|
|
31
|
+
# Trigger notification callbacks
|
|
32
|
+
RailsErrorDashboard.configuration.notification_callbacks[:error_resolved].each do |callback|
|
|
33
|
+
callback.call(error)
|
|
34
|
+
rescue => e
|
|
35
|
+
RailsErrorDashboard::Logger.error("Error in error_resolved callback: #{e.message}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Emit ActiveSupport::Notifications instrumentation event
|
|
39
|
+
ActiveSupport::Notifications.instrument("error_resolved.rails_error_dashboard", {
|
|
40
|
+
error_log: error,
|
|
41
|
+
error_id: error.id,
|
|
42
|
+
error_type: error.error_type,
|
|
43
|
+
resolved_by: @resolution_data[:resolved_by_name],
|
|
44
|
+
resolved_at: error.resolved_at
|
|
45
|
+
})
|
|
46
|
+
|
|
31
47
|
error
|
|
32
48
|
end
|
|
33
49
|
end
|
|
@@ -43,22 +43,65 @@ module RailsErrorDashboard
|
|
|
43
43
|
# Enable/disable Rails.error subscriber
|
|
44
44
|
attr_accessor :enable_error_subscriber
|
|
45
45
|
|
|
46
|
+
# Advanced configuration options
|
|
47
|
+
# Custom severity classification rules (hash of error_type => severity)
|
|
48
|
+
attr_accessor :custom_severity_rules
|
|
49
|
+
|
|
50
|
+
# Exceptions to ignore (array of strings, regexes, or classes)
|
|
51
|
+
attr_accessor :ignored_exceptions
|
|
52
|
+
|
|
53
|
+
# Sampling rate for non-critical errors (0.0 to 1.0, default 1.0 = 100%)
|
|
54
|
+
attr_accessor :sampling_rate
|
|
55
|
+
|
|
56
|
+
# Async logging configuration
|
|
57
|
+
attr_accessor :async_logging
|
|
58
|
+
attr_accessor :async_adapter # :sidekiq, :solid_queue, or :async
|
|
59
|
+
|
|
60
|
+
# Backtrace configuration
|
|
61
|
+
attr_accessor :max_backtrace_lines
|
|
62
|
+
|
|
63
|
+
# Enhanced metrics
|
|
64
|
+
attr_accessor :app_version
|
|
65
|
+
attr_accessor :git_sha
|
|
66
|
+
attr_accessor :total_users_for_impact # For user impact % calculation
|
|
67
|
+
|
|
68
|
+
# Advanced error analysis features
|
|
69
|
+
attr_accessor :enable_similar_errors # Fuzzy error matching
|
|
70
|
+
attr_accessor :enable_co_occurring_errors # Detect errors happening together
|
|
71
|
+
attr_accessor :enable_error_cascades # Parent→child error relationships
|
|
72
|
+
attr_accessor :enable_error_correlation # Version/user/time correlation
|
|
73
|
+
attr_accessor :enable_platform_comparison # iOS vs Android analytics
|
|
74
|
+
attr_accessor :enable_occurrence_patterns # Cyclical/burst pattern detection
|
|
75
|
+
|
|
76
|
+
# Baseline alert configuration
|
|
77
|
+
attr_accessor :enable_baseline_alerts
|
|
78
|
+
attr_accessor :baseline_alert_threshold_std_devs # Number of std devs to trigger alert (default: 2.0)
|
|
79
|
+
attr_accessor :baseline_alert_severities # Array of severities to alert on (default: [:critical, :high])
|
|
80
|
+
attr_accessor :baseline_alert_cooldown_minutes # Minutes between alerts for same error type (default: 120)
|
|
81
|
+
|
|
82
|
+
# Notification callbacks (managed via helper methods, not set directly)
|
|
83
|
+
attr_reader :notification_callbacks
|
|
84
|
+
|
|
85
|
+
# Internal logging configuration
|
|
86
|
+
attr_accessor :enable_internal_logging
|
|
87
|
+
attr_accessor :log_level
|
|
88
|
+
|
|
46
89
|
def initialize
|
|
47
90
|
# Default values
|
|
48
|
-
@dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "
|
|
49
|
-
@dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "
|
|
91
|
+
@dashboard_username = ENV.fetch("ERROR_DASHBOARD_USER", "gandalf")
|
|
92
|
+
@dashboard_password = ENV.fetch("ERROR_DASHBOARD_PASSWORD", "youshallnotpass")
|
|
50
93
|
@require_authentication = true
|
|
51
94
|
@require_authentication_in_development = false
|
|
52
95
|
|
|
53
96
|
@user_model = "User"
|
|
54
97
|
|
|
55
|
-
# Notification settings
|
|
98
|
+
# Notification settings (disabled by default - enable during installation or in initializer)
|
|
56
99
|
@slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
|
|
57
100
|
@notification_email_recipients = ENV.fetch("ERROR_NOTIFICATION_EMAILS", "").split(",").map(&:strip)
|
|
58
101
|
@notification_email_from = ENV.fetch("ERROR_NOTIFICATION_FROM", "errors@example.com")
|
|
59
102
|
@dashboard_base_url = ENV["DASHBOARD_BASE_URL"]
|
|
60
|
-
@enable_slack_notifications =
|
|
61
|
-
@enable_email_notifications =
|
|
103
|
+
@enable_slack_notifications = false
|
|
104
|
+
@enable_email_notifications = false
|
|
62
105
|
|
|
63
106
|
# Discord notification settings
|
|
64
107
|
@discord_webhook_url = ENV["DISCORD_WEBHOOK_URL"]
|
|
@@ -78,6 +121,48 @@ module RailsErrorDashboard
|
|
|
78
121
|
|
|
79
122
|
@enable_middleware = true
|
|
80
123
|
@enable_error_subscriber = true
|
|
124
|
+
|
|
125
|
+
# Advanced configuration defaults
|
|
126
|
+
@custom_severity_rules = {}
|
|
127
|
+
@ignored_exceptions = []
|
|
128
|
+
@sampling_rate = 1.0 # 100% by default
|
|
129
|
+
@async_logging = false
|
|
130
|
+
@async_adapter = :sidekiq # Battle-tested default
|
|
131
|
+
@max_backtrace_lines = 50
|
|
132
|
+
|
|
133
|
+
# Enhanced metrics defaults
|
|
134
|
+
@app_version = ENV["APP_VERSION"]
|
|
135
|
+
@git_sha = ENV["GIT_SHA"]
|
|
136
|
+
@total_users_for_impact = nil # Auto-detect if not set
|
|
137
|
+
|
|
138
|
+
# Advanced error analysis features (all OFF by default - opt-in)
|
|
139
|
+
@enable_similar_errors = false # Fuzzy error matching
|
|
140
|
+
@enable_co_occurring_errors = false # Co-occurring error detection
|
|
141
|
+
@enable_error_cascades = false # Error cascade detection
|
|
142
|
+
@enable_error_correlation = false # Version/user/time correlation
|
|
143
|
+
@enable_platform_comparison = false # Platform health comparison
|
|
144
|
+
@enable_occurrence_patterns = false # Pattern detection
|
|
145
|
+
|
|
146
|
+
# Baseline alert defaults
|
|
147
|
+
@enable_baseline_alerts = false # OFF by default (opt-in)
|
|
148
|
+
@baseline_alert_threshold_std_devs = ENV.fetch("BASELINE_ALERT_THRESHOLD", "2.0").to_f
|
|
149
|
+
@baseline_alert_severities = [ :critical, :high ] # Alert on critical and high severity anomalies
|
|
150
|
+
@baseline_alert_cooldown_minutes = ENV.fetch("BASELINE_ALERT_COOLDOWN", "120").to_i
|
|
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
|
+
|
|
156
|
+
@notification_callbacks = {
|
|
157
|
+
error_logged: [],
|
|
158
|
+
critical_error: [],
|
|
159
|
+
error_resolved: []
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Reset configuration to defaults
|
|
164
|
+
def reset!
|
|
165
|
+
initialize
|
|
81
166
|
end
|
|
82
167
|
end
|
|
83
168
|
end
|
|
@@ -22,14 +22,22 @@ module RailsErrorDashboard
|
|
|
22
22
|
# Skip low-severity warnings
|
|
23
23
|
return if handled && severity == :warning
|
|
24
24
|
|
|
25
|
-
#
|
|
26
|
-
|
|
25
|
+
# CRITICAL: Wrap entire process in rescue to ensure failures don't break the app
|
|
26
|
+
begin
|
|
27
|
+
# Extract context information
|
|
28
|
+
error_context = ValueObjects::ErrorContext.new(context, source)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Log to our error dashboard using Command
|
|
31
|
+
Commands::LogError.call(error, error_context.to_h.merge(source: source))
|
|
32
|
+
rescue => e
|
|
33
|
+
# Don't let error logging cause more errors - fail silently
|
|
34
|
+
# Log failure for debugging but NEVER propagate exception
|
|
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
|
+
nil # Explicitly return nil, never raise
|
|
40
|
+
end
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
43
|
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
|
|
@@ -23,17 +23,24 @@ module RailsErrorDashboard
|
|
|
23
23
|
@app.call(env)
|
|
24
24
|
rescue => exception
|
|
25
25
|
# Report to Rails.error (will be logged by our ErrorReporter)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
# CRITICAL: Wrap in rescue to ensure gem failures don't break the app
|
|
27
|
+
begin
|
|
28
|
+
Rails.error.report(exception,
|
|
29
|
+
handled: false,
|
|
30
|
+
severity: :error,
|
|
31
|
+
context: {
|
|
32
|
+
request: ActionDispatch::Request.new(env),
|
|
33
|
+
middleware: true
|
|
34
|
+
},
|
|
35
|
+
source: "rack.middleware"
|
|
36
|
+
)
|
|
37
|
+
rescue => e
|
|
38
|
+
# If error reporting fails, log it but DON'T break the app
|
|
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
|
+
end
|
|
35
42
|
|
|
36
|
-
# Re-raise to let Rails handle the response
|
|
43
|
+
# Re-raise original exception to let Rails handle the response
|
|
37
44
|
raise exception
|
|
38
45
|
end
|
|
39
46
|
end
|
|
@@ -85,14 +85,17 @@ module RailsErrorDashboard
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# Helper method to safely execute plugin hooks
|
|
88
|
-
# Prevents plugin errors from breaking the main application
|
|
88
|
+
# CRITICAL: Prevents plugin errors from breaking the main application
|
|
89
89
|
def safe_execute(method_name, *args)
|
|
90
90
|
return unless enabled?
|
|
91
91
|
|
|
92
92
|
send(method_name, *args)
|
|
93
93
|
rescue => e
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
# Log plugin failures but never propagate - plugins must not break the app
|
|
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
|
+
nil # Explicitly return nil, never raise
|
|
96
99
|
end
|
|
97
100
|
end
|
|
98
101
|
end
|