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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +858 -0
  4. data/Rakefile +3 -0
  5. data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -0
  6. data/app/controllers/rails_error_dashboard/application_controller.rb +12 -0
  7. data/app/controllers/rails_error_dashboard/errors_controller.rb +123 -0
  8. data/app/helpers/rails_error_dashboard/application_helper.rb +4 -0
  9. data/app/jobs/rails_error_dashboard/application_job.rb +4 -0
  10. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +116 -0
  11. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +19 -0
  12. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +105 -0
  13. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +166 -0
  14. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +108 -0
  15. data/app/mailers/rails_error_dashboard/application_mailer.rb +8 -0
  16. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +27 -0
  17. data/app/models/rails_error_dashboard/application_record.rb +5 -0
  18. data/app/models/rails_error_dashboard/error_log.rb +185 -0
  19. data/app/models/rails_error_dashboard/error_logs_record.rb +34 -0
  20. data/app/views/layouts/rails_error_dashboard/application.html.erb +17 -0
  21. data/app/views/layouts/rails_error_dashboard.html.erb +351 -0
  22. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +200 -0
  23. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +32 -0
  24. data/app/views/rails_error_dashboard/errors/analytics.html.erb +237 -0
  25. data/app/views/rails_error_dashboard/errors/index.html.erb +334 -0
  26. data/app/views/rails_error_dashboard/errors/show.html.erb +294 -0
  27. data/config/routes.rb +13 -0
  28. data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +40 -0
  29. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +13 -0
  30. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +10 -0
  31. data/lib/generators/rails_error_dashboard/install/install_generator.rb +27 -0
  32. data/lib/generators/rails_error_dashboard/install/templates/README +33 -0
  33. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +64 -0
  34. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +40 -0
  35. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +60 -0
  36. data/lib/rails_error_dashboard/commands/log_error.rb +134 -0
  37. data/lib/rails_error_dashboard/commands/resolve_error.rb +35 -0
  38. data/lib/rails_error_dashboard/configuration.rb +83 -0
  39. data/lib/rails_error_dashboard/engine.rb +20 -0
  40. data/lib/rails_error_dashboard/error_reporter.rb +35 -0
  41. data/lib/rails_error_dashboard/middleware/error_catcher.rb +41 -0
  42. data/lib/rails_error_dashboard/plugin.rb +98 -0
  43. data/lib/rails_error_dashboard/plugin_registry.rb +88 -0
  44. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +96 -0
  45. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +122 -0
  46. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +78 -0
  47. data/lib/rails_error_dashboard/queries/analytics_stats.rb +108 -0
  48. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +37 -0
  49. data/lib/rails_error_dashboard/queries/developer_insights.rb +277 -0
  50. data/lib/rails_error_dashboard/queries/errors_list.rb +66 -0
  51. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +149 -0
  52. data/lib/rails_error_dashboard/queries/filter_options.rb +21 -0
  53. data/lib/rails_error_dashboard/services/platform_detector.rb +41 -0
  54. data/lib/rails_error_dashboard/value_objects/error_context.rb +148 -0
  55. data/lib/rails_error_dashboard/version.rb +3 -0
  56. data/lib/rails_error_dashboard.rb +60 -0
  57. data/lib/tasks/rails_error_dashboard_tasks.rake +4 -0
  58. metadata +318 -0
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,12 @@
1
+ module RailsErrorDashboard
2
+ class ApplicationController < ActionController::Base
3
+ include Pagy::Backend
4
+
5
+ layout "rails_error_dashboard"
6
+
7
+ protect_from_forgery with: :exception
8
+
9
+ # Make Pagy helpers available in views
10
+ helper Pagy::Frontend
11
+ end
12
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ class ErrorsController < ApplicationController
5
+ include Pagy::Backend
6
+
7
+ before_action :authenticate_dashboard_user!
8
+
9
+ def index
10
+ # Use Query to get filtered errors
11
+ errors_query = Queries::ErrorsList.call(filter_params)
12
+
13
+ # Paginate with Pagy
14
+ @pagy, @errors = pagy(errors_query, items: params[:per_page] || 25)
15
+
16
+ # Get dashboard stats using Query
17
+ @stats = Queries::DashboardStats.call
18
+
19
+ # Get filter options using Query
20
+ filter_options = Queries::FilterOptions.call
21
+ @environments = filter_options[:environments]
22
+ @error_types = filter_options[:error_types]
23
+ @platforms = filter_options[:platforms]
24
+ end
25
+
26
+ def show
27
+ @error = ErrorLog.find(params[:id])
28
+ @related_errors = @error.related_errors(limit: 5)
29
+
30
+ # Dispatch plugin event for error viewed
31
+ RailsErrorDashboard::PluginRegistry.dispatch(:on_error_viewed, @error)
32
+ end
33
+
34
+ def resolve
35
+ # Use Command to resolve error
36
+ @error = Commands::ResolveError.call(
37
+ params[:id],
38
+ resolved_by_name: params[:resolved_by_name],
39
+ resolution_comment: params[:resolution_comment],
40
+ resolution_reference: params[:resolution_reference]
41
+ )
42
+
43
+ redirect_to error_path(@error)
44
+ end
45
+
46
+ def analytics
47
+ days = (params[:days] || 30).to_i
48
+ @days = days
49
+
50
+ # Use Query to get analytics data
51
+ analytics = Queries::AnalyticsStats.call(days)
52
+
53
+ @error_stats = analytics[:error_stats]
54
+ @errors_over_time = analytics[:errors_over_time]
55
+ @errors_by_type = analytics[:errors_by_type]
56
+ @errors_by_platform = analytics[:errors_by_platform]
57
+ @errors_by_environment = analytics[:errors_by_environment]
58
+ @errors_by_hour = analytics[:errors_by_hour]
59
+ @top_users = analytics[:top_users]
60
+ @resolution_rate = analytics[:resolution_rate]
61
+ @mobile_errors = analytics[:mobile_errors]
62
+ @api_errors = analytics[:api_errors]
63
+ end
64
+
65
+ def batch_action
66
+ error_ids = params[:error_ids] || []
67
+ action_type = params[:action_type]
68
+
69
+ result = case action_type
70
+ when "resolve"
71
+ Commands::BatchResolveErrors.call(
72
+ error_ids,
73
+ resolved_by_name: params[:resolved_by_name],
74
+ resolution_comment: params[:resolution_comment]
75
+ )
76
+ when "delete"
77
+ Commands::BatchDeleteErrors.call(error_ids)
78
+ else
79
+ { success: false, count: 0, errors: [ "Invalid action type" ] }
80
+ end
81
+
82
+ if result[:success]
83
+ flash[:notice] = "Successfully #{action_type}d #{result[:count]} error(s)"
84
+ else
85
+ flash[:alert] = "Batch operation failed: #{result[:errors].join(', ')}"
86
+ end
87
+
88
+ redirect_to errors_path
89
+ end
90
+
91
+ private
92
+
93
+ def filter_params
94
+ {
95
+ environment: params[:environment],
96
+ error_type: params[:error_type],
97
+ unresolved: params[:unresolved],
98
+ platform: params[:platform],
99
+ search: params[:search]
100
+ }
101
+ end
102
+
103
+ def authenticate_dashboard_user!
104
+ return if skip_authentication?
105
+
106
+ authenticate_or_request_with_http_basic do |username, password|
107
+ ActiveSupport::SecurityUtils.secure_compare(
108
+ username,
109
+ RailsErrorDashboard.configuration.dashboard_username
110
+ ) &
111
+ ActiveSupport::SecurityUtils.secure_compare(
112
+ password,
113
+ RailsErrorDashboard.configuration.dashboard_password
114
+ )
115
+ end
116
+ end
117
+
118
+ def skip_authentication?
119
+ !RailsErrorDashboard.configuration.require_authentication ||
120
+ (Rails.env.development? && !RailsErrorDashboard.configuration.require_authentication_in_development)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,4 @@
1
+ module RailsErrorDashboard
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module RailsErrorDashboard
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+
5
+ module RailsErrorDashboard
6
+ # Job to send error notifications to Discord via webhook
7
+ class DiscordErrorNotificationJob < ApplicationJob
8
+ queue_as :default
9
+
10
+ def perform(error_log_id)
11
+ error_log = ErrorLog.find(error_log_id)
12
+ webhook_url = RailsErrorDashboard.configuration.discord_webhook_url
13
+
14
+ return unless webhook_url.present?
15
+
16
+ payload = build_discord_payload(error_log)
17
+
18
+ HTTParty.post(
19
+ webhook_url,
20
+ body: payload.to_json,
21
+ headers: { "Content-Type" => "application/json" }
22
+ )
23
+ rescue StandardError => e
24
+ Rails.logger.error("Failed to send Discord notification: #{e.message}")
25
+ Rails.logger.error(e.backtrace.join("\n"))
26
+ end
27
+
28
+ private
29
+
30
+ def build_discord_payload(error_log)
31
+ {
32
+ embeds: [ {
33
+ title: "🚨 New Error: #{error_log.error_type}",
34
+ description: truncate_message(error_log.message),
35
+ color: severity_color(error_log),
36
+ fields: [
37
+ {
38
+ name: "Platform",
39
+ value: error_log.platform || "Unknown",
40
+ inline: true
41
+ },
42
+ {
43
+ name: "Environment",
44
+ value: error_log.environment || "Unknown",
45
+ inline: true
46
+ },
47
+ {
48
+ name: "Occurrences",
49
+ value: error_log.occurrence_count.to_s,
50
+ inline: true
51
+ },
52
+ {
53
+ name: "Controller",
54
+ value: error_log.controller_name || "N/A",
55
+ inline: true
56
+ },
57
+ {
58
+ name: "Action",
59
+ value: error_log.action_name || "N/A",
60
+ inline: true
61
+ },
62
+ {
63
+ name: "First Seen",
64
+ value: format_time(error_log.first_seen_at),
65
+ inline: true
66
+ },
67
+ {
68
+ name: "Location",
69
+ value: extract_first_backtrace_line(error_log.backtrace),
70
+ inline: false
71
+ }
72
+ ],
73
+ footer: {
74
+ text: "Rails Error Dashboard"
75
+ },
76
+ timestamp: error_log.occurred_at.iso8601
77
+ } ]
78
+ }
79
+ end
80
+
81
+ def severity_color(error_log)
82
+ case error_log.severity
83
+ when :critical
84
+ 16711680 # Red
85
+ when :high
86
+ 16744192 # Orange
87
+ when :medium
88
+ 16776960 # Yellow
89
+ else
90
+ 8421504 # Gray
91
+ end
92
+ end
93
+
94
+ def truncate_message(message, length = 200)
95
+ return "" if message.nil?
96
+ message.length > length ? "#{message[0...length]}..." : message
97
+ end
98
+
99
+ def format_time(time)
100
+ return "N/A" if time.nil?
101
+ time.strftime("%Y-%m-%d %H:%M:%S UTC")
102
+ end
103
+
104
+ def extract_first_backtrace_line(backtrace)
105
+ return "N/A" if backtrace.nil?
106
+
107
+ lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
108
+ first_line = lines.first&.strip
109
+
110
+ return "N/A" if first_line.nil?
111
+
112
+ # Truncate if too long
113
+ first_line.length > 100 ? "#{first_line[0...100]}..." : first_line
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ class EmailErrorNotificationJob < ApplicationJob
5
+ queue_as :error_notifications
6
+
7
+ def perform(error_log_id)
8
+ error_log = ErrorLog.find_by(id: error_log_id)
9
+ return unless error_log
10
+
11
+ recipients = RailsErrorDashboard.configuration.notification_email_recipients
12
+ return unless recipients.present?
13
+
14
+ ErrorNotificationMailer.error_alert(error_log, recipients).deliver_now
15
+ rescue => e
16
+ Rails.logger.error("Failed to send email notification: #{e.message}")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+
5
+ module RailsErrorDashboard
6
+ # Job to send critical error notifications to PagerDuty
7
+ # Only triggers for critical severity errors
8
+ class PagerdutyErrorNotificationJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ PAGERDUTY_EVENTS_API = "https://events.pagerduty.com/v2/enqueue"
12
+
13
+ def perform(error_log_id)
14
+ error_log = ErrorLog.find(error_log_id)
15
+
16
+ # Only trigger PagerDuty for critical errors
17
+ return unless error_log.critical?
18
+
19
+ routing_key = RailsErrorDashboard.configuration.pagerduty_integration_key
20
+ return unless routing_key.present?
21
+
22
+ payload = build_pagerduty_payload(error_log, routing_key)
23
+
24
+ response = HTTParty.post(
25
+ PAGERDUTY_EVENTS_API,
26
+ body: payload.to_json,
27
+ headers: { "Content-Type" => "application/json" }
28
+ )
29
+
30
+ unless response.success?
31
+ Rails.logger.error("PagerDuty API error: #{response.code} - #{response.body}")
32
+ end
33
+ rescue StandardError => e
34
+ Rails.logger.error("Failed to send PagerDuty notification: #{e.message}")
35
+ Rails.logger.error(e.backtrace.join("\n"))
36
+ end
37
+
38
+ private
39
+
40
+ def build_pagerduty_payload(error_log, routing_key)
41
+ {
42
+ routing_key: routing_key,
43
+ event_action: "trigger",
44
+ payload: {
45
+ summary: "Critical Error: #{error_log.error_type} in #{error_log.platform}",
46
+ severity: "critical",
47
+ source: error_source(error_log),
48
+ component: error_log.controller_name || "Unknown",
49
+ group: error_log.error_type,
50
+ class: error_log.error_type,
51
+ custom_details: {
52
+ message: error_log.message,
53
+ controller: error_log.controller_name,
54
+ action: error_log.action_name,
55
+ platform: error_log.platform,
56
+ environment: error_log.environment,
57
+ occurrences: error_log.occurrence_count,
58
+ first_seen_at: error_log.first_seen_at&.iso8601,
59
+ last_seen_at: error_log.last_seen_at&.iso8601,
60
+ request_url: error_log.request_url,
61
+ backtrace: extract_backtrace_summary(error_log.backtrace),
62
+ error_id: error_log.id
63
+ }
64
+ },
65
+ links: dashboard_links(error_log),
66
+ client: "Rails Error Dashboard",
67
+ client_url: dashboard_url(error_log)
68
+ }
69
+ end
70
+
71
+ def error_source(error_log)
72
+ if error_log.controller_name && error_log.action_name
73
+ "#{error_log.controller_name}##{error_log.action_name}"
74
+ elsif error_log.request_url
75
+ error_log.request_url
76
+ else
77
+ error_log.platform || "Rails Application"
78
+ end
79
+ end
80
+
81
+ def extract_backtrace_summary(backtrace)
82
+ return [] if backtrace.nil?
83
+
84
+ lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
85
+ lines.first(10).map(&:strip)
86
+ end
87
+
88
+ def dashboard_links(error_log)
89
+ [
90
+ {
91
+ href: dashboard_url(error_log),
92
+ text: "View in Error Dashboard"
93
+ }
94
+ ]
95
+ end
96
+
97
+ def dashboard_url(error_log)
98
+ # This will need to be configured per deployment
99
+ # For now, return a placeholder
100
+ config = RailsErrorDashboard.configuration
101
+ base_url = config.dashboard_base_url || "http://localhost:3000"
102
+ "#{base_url}/error_dashboard/errors/#{error_log.id}"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ class SlackErrorNotificationJob < ApplicationJob
5
+ queue_as :error_notifications
6
+
7
+ def perform(error_log_id)
8
+ error_log = ErrorLog.find_by(id: error_log_id)
9
+ return unless error_log
10
+
11
+ webhook_url = RailsErrorDashboard.configuration.slack_webhook_url
12
+ return unless webhook_url.present?
13
+
14
+ send_slack_notification(error_log, webhook_url)
15
+ rescue => e
16
+ Rails.logger.error("Failed to send Slack notification: #{e.message}")
17
+ end
18
+
19
+ private
20
+
21
+ def send_slack_notification(error_log, webhook_url)
22
+ require "net/http"
23
+ require "json"
24
+
25
+ uri = URI(webhook_url)
26
+ http = Net::HTTP.new(uri.host, uri.port)
27
+ http.use_ssl = true
28
+
29
+ request = Net::HTTP::Post.new(uri.path, { "Content-Type" => "application/json" })
30
+ request.body = slack_payload(error_log).to_json
31
+
32
+ response = http.request(request)
33
+
34
+ unless response.is_a?(Net::HTTPSuccess)
35
+ Rails.logger.error("Slack notification failed: #{response.code} - #{response.body}")
36
+ end
37
+ end
38
+
39
+ def slack_payload(error_log)
40
+ {
41
+ text: "🚨 New Error in #{error_log.environment.titleize} Environment",
42
+ blocks: [
43
+ {
44
+ type: "header",
45
+ text: {
46
+ type: "plain_text",
47
+ text: "🚨 Error Alert",
48
+ emoji: true
49
+ }
50
+ },
51
+ {
52
+ type: "section",
53
+ fields: [
54
+ {
55
+ type: "mrkdwn",
56
+ text: "*Error Type:*\n`#{error_log.error_type}`"
57
+ },
58
+ {
59
+ type: "mrkdwn",
60
+ text: "*Environment:*\n#{error_log.environment.titleize}"
61
+ },
62
+ {
63
+ type: "mrkdwn",
64
+ text: "*Platform:*\n#{platform_emoji(error_log.platform)} #{error_log.platform || 'Unknown'}"
65
+ },
66
+ {
67
+ type: "mrkdwn",
68
+ text: "*Occurred:*\n#{error_log.occurred_at.strftime('%B %d, %Y at %I:%M %p')}"
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ type: "section",
74
+ text: {
75
+ type: "mrkdwn",
76
+ text: "*Message:*\n```#{truncate_message(error_log.message)}```"
77
+ }
78
+ },
79
+ user_section(error_log),
80
+ request_section(error_log),
81
+ {
82
+ type: "actions",
83
+ elements: [
84
+ {
85
+ type: "button",
86
+ text: {
87
+ type: "plain_text",
88
+ text: "View Details",
89
+ emoji: true
90
+ },
91
+ url: dashboard_url(error_log),
92
+ style: "primary"
93
+ }
94
+ ]
95
+ },
96
+ {
97
+ type: "context",
98
+ elements: [
99
+ {
100
+ type: "mrkdwn",
101
+ text: "Error ID: #{error_log.id}"
102
+ }
103
+ ]
104
+ }
105
+ ].compact
106
+ }
107
+ end
108
+
109
+ def user_section(error_log)
110
+ return nil unless error_log.user_id.present?
111
+
112
+ user_email = error_log.user&.email || "User ##{error_log.user_id}"
113
+
114
+ {
115
+ type: "section",
116
+ fields: [
117
+ {
118
+ type: "mrkdwn",
119
+ text: "*User:*\n#{user_email}"
120
+ },
121
+ {
122
+ type: "mrkdwn",
123
+ text: "*IP Address:*\n#{error_log.ip_address || 'N/A'}"
124
+ }
125
+ ]
126
+ }
127
+ end
128
+
129
+ def request_section(error_log)
130
+ return nil unless error_log.request_url.present?
131
+
132
+ {
133
+ type: "section",
134
+ text: {
135
+ type: "mrkdwn",
136
+ text: "*Request URL:*\n`#{truncate_message(error_log.request_url, 200)}`"
137
+ }
138
+ }
139
+ end
140
+
141
+ def platform_emoji(platform)
142
+ case platform&.downcase
143
+ when "ios"
144
+ "📱"
145
+ when "android"
146
+ "🤖"
147
+ when "api"
148
+ "🔌"
149
+ else
150
+ "💻"
151
+ end
152
+ end
153
+
154
+ def truncate_message(message, length = 500)
155
+ return "" unless message
156
+ message.length > length ? "#{message[0...length]}..." : message
157
+ end
158
+
159
+ def dashboard_url(error_log)
160
+ # Generate URL to error dashboard
161
+ # This will need to be configured based on your app's URL
162
+ base_url = RailsErrorDashboard.configuration.dashboard_base_url || "http://localhost:3000"
163
+ "#{base_url}/error_dashboard/errors/#{error_log.id}"
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+
5
+ module RailsErrorDashboard
6
+ # Job to send error notifications to custom webhook URLs
7
+ # Supports multiple webhooks for different integrations
8
+ class WebhookErrorNotificationJob < ApplicationJob
9
+ queue_as :default
10
+
11
+ def perform(error_log_id)
12
+ error_log = ErrorLog.find(error_log_id)
13
+ webhook_urls = RailsErrorDashboard.configuration.webhook_urls
14
+
15
+ return unless webhook_urls.present?
16
+
17
+ # Ensure webhook_urls is an array
18
+ urls = Array(webhook_urls)
19
+
20
+ payload = build_webhook_payload(error_log)
21
+
22
+ urls.each do |url|
23
+ send_webhook(url, payload, error_log)
24
+ end
25
+ rescue StandardError => e
26
+ Rails.logger.error("Failed to send webhook notification: #{e.message}")
27
+ Rails.logger.error(e.backtrace.join("\n"))
28
+ end
29
+
30
+ private
31
+
32
+ def send_webhook(url, payload, error_log)
33
+ response = HTTParty.post(
34
+ url,
35
+ body: payload.to_json,
36
+ headers: {
37
+ "Content-Type" => "application/json",
38
+ "User-Agent" => "RailsErrorDashboard/1.0",
39
+ "X-Error-Dashboard-Event" => "error.created",
40
+ "X-Error-Dashboard-ID" => error_log.id.to_s
41
+ },
42
+ timeout: 10 # 10 second timeout
43
+ )
44
+
45
+ unless response.success?
46
+ Rails.logger.warn("Webhook failed for #{url}: #{response.code}")
47
+ end
48
+ rescue StandardError => e
49
+ Rails.logger.error("Webhook error for #{url}: #{e.message}")
50
+ end
51
+
52
+ def build_webhook_payload(error_log)
53
+ {
54
+ event: "error.created",
55
+ timestamp: Time.current.iso8601,
56
+ error: {
57
+ id: error_log.id,
58
+ type: error_log.error_type,
59
+ message: error_log.message,
60
+ severity: error_log.severity.to_s,
61
+ platform: error_log.platform,
62
+ environment: error_log.environment,
63
+ controller: error_log.controller_name,
64
+ action: error_log.action_name,
65
+ occurrence_count: error_log.occurrence_count,
66
+ first_seen_at: error_log.first_seen_at&.iso8601,
67
+ last_seen_at: error_log.last_seen_at&.iso8601,
68
+ occurred_at: error_log.occurred_at.iso8601,
69
+ resolved: error_log.resolved,
70
+ request: {
71
+ url: error_log.request_url,
72
+ params: parse_request_params(error_log.request_params),
73
+ user_agent: error_log.user_agent,
74
+ ip_address: error_log.ip_address
75
+ },
76
+ user: {
77
+ id: error_log.user_id
78
+ },
79
+ backtrace: extract_backtrace(error_log.backtrace),
80
+ metadata: {
81
+ error_hash: error_log.error_hash,
82
+ dashboard_url: dashboard_url(error_log)
83
+ }
84
+ }
85
+ }
86
+ end
87
+
88
+ def parse_request_params(params_json)
89
+ return {} if params_json.nil?
90
+ JSON.parse(params_json)
91
+ rescue JSON::ParserError
92
+ {}
93
+ end
94
+
95
+ def extract_backtrace(backtrace)
96
+ return [] if backtrace.nil?
97
+
98
+ lines = backtrace.is_a?(String) ? backtrace.lines : backtrace
99
+ lines.first(20).map(&:strip)
100
+ end
101
+
102
+ def dashboard_url(error_log)
103
+ config = RailsErrorDashboard.configuration
104
+ base_url = config.dashboard_base_url || "http://localhost:3000"
105
+ "#{base_url}/error_dashboard/errors/#{error_log.id}"
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsErrorDashboard
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: -> { RailsErrorDashboard.configuration.notification_email_from }
6
+ layout "mailer"
7
+ end
8
+ end