rails_error_dashboard 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +858 -0
- data/Rakefile +3 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -0
- data/app/controllers/rails_error_dashboard/application_controller.rb +12 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +123 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +4 -0
- data/app/jobs/rails_error_dashboard/application_job.rb +4 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +116 -0
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +105 -0
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +166 -0
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +108 -0
- data/app/mailers/rails_error_dashboard/application_mailer.rb +8 -0
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +27 -0
- data/app/models/rails_error_dashboard/application_record.rb +5 -0
- data/app/models/rails_error_dashboard/error_log.rb +185 -0
- data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +17 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +351 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +200 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +32 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +237 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +334 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +294 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +40 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +13 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +10 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +27 -0
- data/lib/generators/rails_error_dashboard/install/templates/README +33 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +64 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +40 -0
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +60 -0
- data/lib/rails_error_dashboard/commands/log_error.rb +134 -0
- data/lib/rails_error_dashboard/commands/resolve_error.rb +35 -0
- data/lib/rails_error_dashboard/configuration.rb +83 -0
- data/lib/rails_error_dashboard/engine.rb +20 -0
- data/lib/rails_error_dashboard/error_reporter.rb +35 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +41 -0
- data/lib/rails_error_dashboard/plugin.rb +98 -0
- data/lib/rails_error_dashboard/plugin_registry.rb +88 -0
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +96 -0
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +122 -0
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +78 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +108 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +37 -0
- data/lib/rails_error_dashboard/queries/developer_insights.rb +277 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +66 -0
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +149 -0
- data/lib/rails_error_dashboard/queries/filter_options.rb +21 -0
- data/lib/rails_error_dashboard/services/platform_detector.rb +41 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +148 -0
- data/lib/rails_error_dashboard/version.rb +3 -0
- data/lib/rails_error_dashboard.rb +60 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +4 -0
- metadata +318 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Base class for creating plugins
|
|
5
|
+
# Plugins can hook into error lifecycle events and extend functionality
|
|
6
|
+
#
|
|
7
|
+
# Example plugin:
|
|
8
|
+
#
|
|
9
|
+
# class MyNotificationPlugin < RailsErrorDashboard::Plugin
|
|
10
|
+
# def name
|
|
11
|
+
# "My Custom Notifier"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# def on_error_logged(error_log)
|
|
15
|
+
# # Send notification to custom service
|
|
16
|
+
# MyService.notify(error_log)
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # Register the plugin
|
|
21
|
+
# RailsErrorDashboard.register_plugin(MyNotificationPlugin.new)
|
|
22
|
+
#
|
|
23
|
+
class Plugin
|
|
24
|
+
# Plugin name (must be implemented by subclass)
|
|
25
|
+
def name
|
|
26
|
+
raise NotImplementedError, "Plugin must implement #name"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Plugin description (optional)
|
|
30
|
+
def description
|
|
31
|
+
"No description provided"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Plugin version (optional)
|
|
35
|
+
def version
|
|
36
|
+
"1.0.0"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Called when plugin is registered
|
|
40
|
+
# Use this for initialization logic
|
|
41
|
+
def on_register
|
|
42
|
+
# Override in subclass if needed
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Called when a new error is logged (first occurrence)
|
|
46
|
+
# @param error_log [ErrorLog] The newly created error log
|
|
47
|
+
def on_error_logged(error_log)
|
|
48
|
+
# Override in subclass to handle event
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Called when an existing error recurs (subsequent occurrences)
|
|
52
|
+
# @param error_log [ErrorLog] The updated error log
|
|
53
|
+
def on_error_recurred(error_log)
|
|
54
|
+
# Override in subclass to handle event
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Called when an error is resolved
|
|
58
|
+
# @param error_log [ErrorLog] The resolved error log
|
|
59
|
+
def on_error_resolved(error_log)
|
|
60
|
+
# Override in subclass to handle event
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Called when errors are batch resolved
|
|
64
|
+
# @param error_logs [Array<ErrorLog>] The resolved error logs
|
|
65
|
+
def on_errors_batch_resolved(error_logs)
|
|
66
|
+
# Override in subclass to handle event
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Called when errors are batch deleted
|
|
70
|
+
# @param error_ids [Array<Integer>] The IDs of deleted errors
|
|
71
|
+
def on_errors_batch_deleted(error_ids)
|
|
72
|
+
# Override in subclass to handle event
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Called when an error is viewed in the dashboard
|
|
76
|
+
# @param error_log [ErrorLog] The viewed error log
|
|
77
|
+
def on_error_viewed(error_log)
|
|
78
|
+
# Override in subclass to handle event
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Helper method to check if plugin is enabled
|
|
82
|
+
# Override this to add conditional logic
|
|
83
|
+
def enabled?
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Helper method to safely execute plugin hooks
|
|
88
|
+
# Prevents plugin errors from breaking the main application
|
|
89
|
+
def safe_execute(method_name, *args)
|
|
90
|
+
return unless enabled?
|
|
91
|
+
|
|
92
|
+
send(method_name, *args)
|
|
93
|
+
rescue => e
|
|
94
|
+
Rails.logger.error("Plugin '#{name}' failed in #{method_name}: #{e.message}")
|
|
95
|
+
Rails.logger.error(e.backtrace.first(5).join("\n"))
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
# Registry for managing plugins
|
|
5
|
+
# Provides plugin registration and event dispatching
|
|
6
|
+
class PluginRegistry
|
|
7
|
+
class << self
|
|
8
|
+
# Get all registered plugins
|
|
9
|
+
def plugins
|
|
10
|
+
@plugins ||= []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Register a plugin
|
|
14
|
+
# @param plugin [Plugin] The plugin instance to register
|
|
15
|
+
def register(plugin)
|
|
16
|
+
unless plugin.is_a?(Plugin)
|
|
17
|
+
raise ArgumentError, "Plugin must be an instance of RailsErrorDashboard::Plugin"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if plugins.any? { |p| p.name == plugin.name }
|
|
21
|
+
Rails.logger.warn("Plugin '#{plugin.name}' is already registered, skipping")
|
|
22
|
+
return false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
plugins << plugin
|
|
26
|
+
plugin.on_register
|
|
27
|
+
Rails.logger.info("Registered plugin: #{plugin.name} (#{plugin.version})")
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Unregister a plugin by name
|
|
32
|
+
# @param plugin_name [String] The name of the plugin to unregister
|
|
33
|
+
def unregister(plugin_name)
|
|
34
|
+
plugins.reject! { |p| p.name == plugin_name }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Clear all plugins (useful for testing)
|
|
38
|
+
def clear
|
|
39
|
+
@plugins = []
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get a plugin by name
|
|
43
|
+
# @param plugin_name [String] The name of the plugin
|
|
44
|
+
# @return [Plugin, nil] The plugin instance or nil if not found
|
|
45
|
+
def find(plugin_name)
|
|
46
|
+
plugins.find { |p| p.name == plugin_name }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Dispatch an event to all registered plugins
|
|
50
|
+
# @param event_name [Symbol] The event name (e.g., :on_error_logged)
|
|
51
|
+
# @param args [Array] Arguments to pass to the event handler
|
|
52
|
+
def dispatch(event_name, *args)
|
|
53
|
+
plugins.each do |plugin|
|
|
54
|
+
next unless plugin.enabled?
|
|
55
|
+
|
|
56
|
+
plugin.safe_execute(event_name, *args)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get count of registered plugins
|
|
61
|
+
def count
|
|
62
|
+
plugins.size
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if any plugins are registered
|
|
66
|
+
def any?
|
|
67
|
+
plugins.any?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get list of plugin names
|
|
71
|
+
def names
|
|
72
|
+
plugins.map(&:name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get plugin information for debugging
|
|
76
|
+
def info
|
|
77
|
+
plugins.map do |plugin|
|
|
78
|
+
{
|
|
79
|
+
name: plugin.name,
|
|
80
|
+
version: plugin.version,
|
|
81
|
+
description: plugin.description,
|
|
82
|
+
enabled: plugin.enabled?
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Plugins
|
|
5
|
+
# Example plugin: Audit logging
|
|
6
|
+
# Logs all error dashboard activities to a separate audit log
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# RailsErrorDashboard.register_plugin(
|
|
10
|
+
# RailsErrorDashboard::Plugins::AuditLogPlugin.new(logger: Rails.logger)
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
class AuditLogPlugin < Plugin
|
|
14
|
+
def initialize(logger: Rails.logger)
|
|
15
|
+
@logger = logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
"Audit Logger"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def description
|
|
23
|
+
"Logs all error dashboard activities for compliance and auditing"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def version
|
|
27
|
+
"1.0.0"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def on_error_logged(error_log)
|
|
31
|
+
log_event(
|
|
32
|
+
event: "error_logged",
|
|
33
|
+
error_id: error_log.id,
|
|
34
|
+
error_type: error_log.error_type,
|
|
35
|
+
platform: error_log.platform,
|
|
36
|
+
environment: error_log.environment,
|
|
37
|
+
timestamp: Time.current
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def on_error_recurred(error_log)
|
|
42
|
+
log_event(
|
|
43
|
+
event: "error_recurred",
|
|
44
|
+
error_id: error_log.id,
|
|
45
|
+
error_type: error_log.error_type,
|
|
46
|
+
occurrence_count: error_log.occurrence_count,
|
|
47
|
+
timestamp: Time.current
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_error_resolved(error_log)
|
|
52
|
+
log_event(
|
|
53
|
+
event: "error_resolved",
|
|
54
|
+
error_id: error_log.id,
|
|
55
|
+
error_type: error_log.error_type,
|
|
56
|
+
resolved_by: error_log.resolved_by_name,
|
|
57
|
+
resolution_comment: error_log.resolution_comment,
|
|
58
|
+
timestamp: Time.current
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on_errors_batch_resolved(error_logs)
|
|
63
|
+
log_event(
|
|
64
|
+
event: "errors_batch_resolved",
|
|
65
|
+
count: error_logs.size,
|
|
66
|
+
error_ids: error_logs.map(&:id),
|
|
67
|
+
timestamp: Time.current
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on_errors_batch_deleted(error_ids)
|
|
72
|
+
log_event(
|
|
73
|
+
event: "errors_batch_deleted",
|
|
74
|
+
count: error_ids.size,
|
|
75
|
+
error_ids: error_ids,
|
|
76
|
+
timestamp: Time.current
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_error_viewed(error_log)
|
|
81
|
+
log_event(
|
|
82
|
+
event: "error_viewed",
|
|
83
|
+
error_id: error_log.id,
|
|
84
|
+
error_type: error_log.error_type,
|
|
85
|
+
timestamp: Time.current
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def log_event(data)
|
|
92
|
+
@logger.info("[RailsErrorDashboard Audit] #{data.to_json}")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Plugins
|
|
5
|
+
# Example plugin: Jira integration
|
|
6
|
+
# Automatically creates Jira tickets for critical errors
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# RailsErrorDashboard.register_plugin(
|
|
10
|
+
# RailsErrorDashboard::Plugins::JiraIntegrationPlugin.new(
|
|
11
|
+
# jira_url: ENV['JIRA_URL'],
|
|
12
|
+
# jira_username: ENV['JIRA_USERNAME'],
|
|
13
|
+
# jira_api_token: ENV['JIRA_API_TOKEN'],
|
|
14
|
+
# jira_project_key: ENV['JIRA_PROJECT_KEY'],
|
|
15
|
+
# only_critical: true
|
|
16
|
+
# )
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
class JiraIntegrationPlugin < Plugin
|
|
20
|
+
def initialize(jira_url: nil, jira_username: nil, jira_api_token: nil, jira_project_key: nil, only_critical: true)
|
|
21
|
+
@jira_url = jira_url
|
|
22
|
+
@jira_username = jira_username
|
|
23
|
+
@jira_api_token = jira_api_token
|
|
24
|
+
@jira_project_key = jira_project_key
|
|
25
|
+
@only_critical = only_critical
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
"Jira Integration"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def description
|
|
33
|
+
"Automatically creates Jira tickets for critical errors"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def version
|
|
37
|
+
"1.0.0"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def enabled?
|
|
41
|
+
@jira_url.present? && @jira_username.present? && @jira_api_token.present? && @jira_project_key.present?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def on_error_logged(error_log)
|
|
45
|
+
return if @only_critical && !error_log.critical?
|
|
46
|
+
|
|
47
|
+
create_jira_ticket(error_log)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def create_jira_ticket(error_log)
|
|
53
|
+
# Example Jira ticket creation
|
|
54
|
+
# In production, you'd use the jira-ruby gem or make API calls directly
|
|
55
|
+
|
|
56
|
+
ticket_data = {
|
|
57
|
+
project: { key: @jira_project_key },
|
|
58
|
+
summary: "[#{error_log.environment}] #{error_log.error_type}",
|
|
59
|
+
description: build_description(error_log),
|
|
60
|
+
issuetype: { name: "Bug" },
|
|
61
|
+
priority: { name: jira_priority(error_log) },
|
|
62
|
+
labels: [ "rails-error-dashboard", error_log.platform, error_log.environment ]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Rails.logger.info("Would create Jira ticket: #{ticket_data.to_json}")
|
|
66
|
+
|
|
67
|
+
# Actual implementation:
|
|
68
|
+
# require 'httparty'
|
|
69
|
+
# response = HTTParty.post(
|
|
70
|
+
# "#{@jira_url}/rest/api/2/issue",
|
|
71
|
+
# basic_auth: { username: @jira_username, password: @jira_api_token },
|
|
72
|
+
# headers: { 'Content-Type' => 'application/json' },
|
|
73
|
+
# body: { fields: ticket_data }.to_json
|
|
74
|
+
# )
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_description(error_log)
|
|
78
|
+
<<~DESC
|
|
79
|
+
h2. Error Details
|
|
80
|
+
|
|
81
|
+
*Error Type:* #{error_log.error_type}
|
|
82
|
+
*Message:* #{error_log.message}
|
|
83
|
+
*Platform:* #{error_log.platform}
|
|
84
|
+
*Environment:* #{error_log.environment}
|
|
85
|
+
*Severity:* #{error_log.severity}
|
|
86
|
+
*Controller:* #{error_log.controller_name}
|
|
87
|
+
*Action:* #{error_log.action_name}
|
|
88
|
+
*First Seen:* #{error_log.first_seen_at}
|
|
89
|
+
*Occurrences:* #{error_log.occurrence_count}
|
|
90
|
+
|
|
91
|
+
h2. Backtrace
|
|
92
|
+
|
|
93
|
+
{code}
|
|
94
|
+
#{error_log.backtrace&.lines&.first(10)&.join}
|
|
95
|
+
{code}
|
|
96
|
+
|
|
97
|
+
h2. Dashboard Link
|
|
98
|
+
|
|
99
|
+
[View in Dashboard|#{dashboard_url(error_log)}]
|
|
100
|
+
DESC
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def jira_priority(error_log)
|
|
104
|
+
case error_log.severity.to_s
|
|
105
|
+
when "critical"
|
|
106
|
+
"Highest"
|
|
107
|
+
when "high"
|
|
108
|
+
"High"
|
|
109
|
+
when "medium"
|
|
110
|
+
"Medium"
|
|
111
|
+
else
|
|
112
|
+
"Low"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def dashboard_url(error_log)
|
|
117
|
+
base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
|
|
118
|
+
"#{base_url}/error_dashboard/errors/#{error_log.id}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Plugins
|
|
5
|
+
# Example plugin: Metrics tracking
|
|
6
|
+
# Tracks error counts and sends to metrics service (e.g., StatsD, Datadog)
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# RailsErrorDashboard.register_plugin(
|
|
10
|
+
# RailsErrorDashboard::Plugins::MetricsPlugin.new
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
class MetricsPlugin < Plugin
|
|
14
|
+
def name
|
|
15
|
+
"Metrics Tracker"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def description
|
|
19
|
+
"Tracks error metrics and sends to monitoring service"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def version
|
|
23
|
+
"1.0.0"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def on_error_logged(error_log)
|
|
27
|
+
increment_counter("errors.new", error_log)
|
|
28
|
+
increment_counter("errors.by_type.#{sanitize_metric_name(error_log.error_type)}", error_log)
|
|
29
|
+
increment_counter("errors.by_platform.#{error_log.platform || 'unknown'}", error_log)
|
|
30
|
+
increment_counter("errors.by_environment.#{error_log.environment}", error_log)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def on_error_recurred(error_log)
|
|
34
|
+
increment_counter("errors.recurred", error_log)
|
|
35
|
+
increment_counter("errors.occurrence.#{error_log.id}", error_log)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_error_resolved(error_log)
|
|
39
|
+
increment_counter("errors.resolved", error_log)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def on_errors_batch_resolved(error_logs)
|
|
43
|
+
increment_counter("errors.batch_resolved", count: error_logs.size)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def on_errors_batch_deleted(error_ids)
|
|
47
|
+
increment_counter("errors.batch_deleted", count: error_ids.size)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def increment_counter(metric_name, data)
|
|
53
|
+
# Example: Send to StatsD
|
|
54
|
+
# StatsD.increment(metric_name, tags: metric_tags(data))
|
|
55
|
+
|
|
56
|
+
# Example: Send to Datadog
|
|
57
|
+
# Datadog::Statsd.increment(metric_name, tags: metric_tags(data))
|
|
58
|
+
|
|
59
|
+
# For demonstration, just log
|
|
60
|
+
Rails.logger.info("Metrics: #{metric_name} - #{data.is_a?(Hash) ? data : data.class.name}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def metric_tags(data)
|
|
64
|
+
return [] unless data.respond_to?(:platform)
|
|
65
|
+
|
|
66
|
+
[
|
|
67
|
+
"platform:#{data.platform || 'unknown'}",
|
|
68
|
+
"environment:#{data.environment}",
|
|
69
|
+
"severity:#{data.severity}"
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sanitize_metric_name(name)
|
|
74
|
+
name.gsub("::", ".").downcase
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Queries
|
|
5
|
+
# Query: Fetch analytics statistics for charts and trends
|
|
6
|
+
# This is a read operation that aggregates error data over time
|
|
7
|
+
class AnalyticsStats
|
|
8
|
+
def self.call(days = 30)
|
|
9
|
+
new(days).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(days = 30)
|
|
13
|
+
@days = days
|
|
14
|
+
@start_date = days.days.ago
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
{
|
|
19
|
+
days: @days,
|
|
20
|
+
error_stats: error_statistics,
|
|
21
|
+
errors_over_time: errors_over_time,
|
|
22
|
+
errors_by_type: errors_by_type,
|
|
23
|
+
errors_by_platform: errors_by_platform,
|
|
24
|
+
errors_by_environment: errors_by_environment,
|
|
25
|
+
errors_by_hour: errors_by_hour,
|
|
26
|
+
top_users: top_affected_users,
|
|
27
|
+
resolution_rate: resolution_rate,
|
|
28
|
+
mobile_errors: mobile_errors_count,
|
|
29
|
+
api_errors: api_errors_count
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def base_query
|
|
36
|
+
ErrorLog.where("occurred_at >= ?", @start_date)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def error_statistics
|
|
40
|
+
{
|
|
41
|
+
total: base_query.count,
|
|
42
|
+
unresolved: base_query.unresolved.count,
|
|
43
|
+
by_type: base_query.group(:error_type).count.sort_by { |_, count| -count }.to_h,
|
|
44
|
+
by_day: base_query.group("DATE(occurred_at)").count
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def errors_over_time
|
|
49
|
+
base_query.group_by_day(:occurred_at).count
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def errors_by_type
|
|
53
|
+
base_query.group(:error_type)
|
|
54
|
+
.count
|
|
55
|
+
.sort_by { |_, count| -count }
|
|
56
|
+
.first(10)
|
|
57
|
+
.to_h
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def errors_by_platform
|
|
61
|
+
base_query.group(:platform).count
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def errors_by_environment
|
|
65
|
+
base_query.group(:environment).count
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def errors_by_hour
|
|
69
|
+
base_query.group_by_hour(:occurred_at).count
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def top_affected_users
|
|
73
|
+
user_model = RailsErrorDashboard.configuration.user_model
|
|
74
|
+
|
|
75
|
+
base_query.where.not(user_id: nil)
|
|
76
|
+
.group(:user_id)
|
|
77
|
+
.count
|
|
78
|
+
.sort_by { |_, count| -count }
|
|
79
|
+
.first(10)
|
|
80
|
+
.map { |user_id, count| [ find_user_email(user_id, user_model), count ] }
|
|
81
|
+
.to_h
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def find_user_email(user_id, user_model)
|
|
85
|
+
user = user_model.constantize.find_by(id: user_id)
|
|
86
|
+
user&.email || "User ##{user_id}"
|
|
87
|
+
rescue
|
|
88
|
+
"User ##{user_id}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolution_rate
|
|
92
|
+
total = error_statistics[:total]
|
|
93
|
+
return 0 if total.zero?
|
|
94
|
+
|
|
95
|
+
resolved_count = ErrorLog.resolved.where("occurred_at >= ?", @start_date).count
|
|
96
|
+
((resolved_count.to_f / total) * 100).round(1)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def mobile_errors_count
|
|
100
|
+
base_query.where(platform: [ "iOS", "Android" ]).count
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def api_errors_count
|
|
104
|
+
base_query.where("platform IS NULL OR platform = ?", "API").count
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Queries
|
|
5
|
+
# Query: Fetch dashboard statistics
|
|
6
|
+
# This is a read operation that aggregates error data for the dashboard
|
|
7
|
+
class DashboardStats
|
|
8
|
+
def self.call
|
|
9
|
+
new.call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
{
|
|
14
|
+
total_today: ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count,
|
|
15
|
+
total_week: ErrorLog.where("occurred_at >= ?", 7.days.ago).count,
|
|
16
|
+
total_month: ErrorLog.where("occurred_at >= ?", 30.days.ago).count,
|
|
17
|
+
unresolved: ErrorLog.unresolved.count,
|
|
18
|
+
resolved: ErrorLog.resolved.count,
|
|
19
|
+
by_environment: ErrorLog.group(:environment).count,
|
|
20
|
+
by_platform: ErrorLog.group(:platform).count,
|
|
21
|
+
top_errors: top_errors
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def top_errors
|
|
28
|
+
ErrorLog.where("occurred_at >= ?", 7.days.ago)
|
|
29
|
+
.group(:error_type)
|
|
30
|
+
.count
|
|
31
|
+
.sort_by { |_, count| -count }
|
|
32
|
+
.first(10)
|
|
33
|
+
.to_h
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|