rails_nexus 2.0.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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +430 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/config/rails_nexus_manifest.js +2 -0
  6. data/app/assets/javascripts/rails_nexus/controllers.js +603 -0
  7. data/app/assets/stylesheets/rails_nexus/application.css +1964 -0
  8. data/app/controllers/rails_nexus/analytics_controller.rb +361 -0
  9. data/app/controllers/rails_nexus/application_controller.rb +15 -0
  10. data/app/controllers/rails_nexus/backup_controller.rb +91 -0
  11. data/app/controllers/rails_nexus/cron_jobs_controller.rb +64 -0
  12. data/app/controllers/rails_nexus/database_health_controller.rb +128 -0
  13. data/app/controllers/rails_nexus/logged_exceptions_controller.rb +227 -0
  14. data/app/controllers/rails_nexus/n1_patterns_controller.rb +10 -0
  15. data/app/controllers/rails_nexus/settings_controller.rb +100 -0
  16. data/app/controllers/rails_nexus/source_code_controller.rb +39 -0
  17. data/app/controllers/rails_nexus/stats_controller.rb +494 -0
  18. data/app/controllers/rails_nexus/workflow_controller.rb +96 -0
  19. data/app/helpers/rails_nexus/application_helper.rb +5 -0
  20. data/app/helpers/rails_nexus/logged_exceptions_helper.rb +254 -0
  21. data/app/helpers/rails_nexus/source_code_helper.rb +107 -0
  22. data/app/javascript/controllers/rails_nexus_controller.js +380 -0
  23. data/app/javascript/controllers/rails_nexus_detail_controller.js +165 -0
  24. data/app/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  25. data/app/javascript/controllers/rails_nexus_theme_controller.js +31 -0
  26. data/app/javascript/rails_nexus/application.js +17 -0
  27. data/app/jobs/rails_nexus/application_job.rb +4 -0
  28. data/app/mailers/rails_nexus/application_mailer.rb +6 -0
  29. data/app/models/rails_nexus/application_record.rb +5 -0
  30. data/app/models/rails_nexus/backup.rb +107 -0
  31. data/app/models/rails_nexus/base_record.rb +26 -0
  32. data/app/models/rails_nexus/comment.rb +13 -0
  33. data/app/models/rails_nexus/cron_job.rb +72 -0
  34. data/app/models/rails_nexus/database_stat.rb +128 -0
  35. data/app/models/rails_nexus/event.rb +101 -0
  36. data/app/models/rails_nexus/logged_exception.rb +864 -0
  37. data/app/models/rails_nexus/logged_exception_n1.rb +87 -0
  38. data/app/models/rails_nexus/metric.rb +107 -0
  39. data/app/models/rails_nexus/nginx_metric.rb +112 -0
  40. data/app/models/rails_nexus/server_metric.rb +168 -0
  41. data/app/models/rails_nexus/webhook_delivery.rb +64 -0
  42. data/app/services/rails_nexus/backup_service.rb +275 -0
  43. data/app/views/layouts/rails_nexus/application.html.erb +185 -0
  44. data/app/views/rails_nexus/analytics/index.html.erb +363 -0
  45. data/app/views/rails_nexus/backup/files.html.erb +53 -0
  46. data/app/views/rails_nexus/backup/index.html.erb +211 -0
  47. data/app/views/rails_nexus/backup/settings.html.erb +158 -0
  48. data/app/views/rails_nexus/cron_jobs/index.html.erb +141 -0
  49. data/app/views/rails_nexus/database_health/index.html.erb +139 -0
  50. data/app/views/rails_nexus/logged_exceptions/_comments_list.html.erb +50 -0
  51. data/app/views/rails_nexus/logged_exceptions/_exceptions.html.erb +190 -0
  52. data/app/views/rails_nexus/logged_exceptions/_feed.html.erb +3 -0
  53. data/app/views/rails_nexus/logged_exceptions/_filter_group.html.erb +10 -0
  54. data/app/views/rails_nexus/logged_exceptions/_show.html.erb +554 -0
  55. data/app/views/rails_nexus/logged_exceptions/_workflow_panel.html.erb +118 -0
  56. data/app/views/rails_nexus/logged_exceptions/clear.turbo_stream.erb +6 -0
  57. data/app/views/rails_nexus/logged_exceptions/destroy.turbo_stream.erb +2 -0
  58. data/app/views/rails_nexus/logged_exceptions/destroy_all.turbo_stream.erb +6 -0
  59. data/app/views/rails_nexus/logged_exceptions/feed.rss.builder +20 -0
  60. data/app/views/rails_nexus/logged_exceptions/index.html.erb +323 -0
  61. data/app/views/rails_nexus/logged_exceptions/query.html.erb +3 -0
  62. data/app/views/rails_nexus/logged_exceptions/query.turbo_stream.erb +6 -0
  63. data/app/views/rails_nexus/logged_exceptions/show.html.erb +3 -0
  64. data/app/views/rails_nexus/logged_exceptions/show.turbo_stream.erb +5 -0
  65. data/app/views/rails_nexus/n1_patterns/index.html.erb +153 -0
  66. data/app/views/rails_nexus/settings/index.html.erb +140 -0
  67. data/app/views/rails_nexus/stats/index.html.erb +569 -0
  68. data/config/importmap.rb +15 -0
  69. data/config/initializers/date_formats.rb +5 -0
  70. data/config/initializers/rails_nexus.rb +18 -0
  71. data/config/locales/en.yml +47 -0
  72. data/config/routes.rb +58 -0
  73. data/db/migrate/20240330122311_create_rails_nexus_logged_exceptions.rb +22 -0
  74. data/lib/generators/rails_nexus/backup_generator.rb +70 -0
  75. data/lib/generators/rails_nexus/customize_generator.rb +159 -0
  76. data/lib/generators/rails_nexus/install_generator.rb +61 -0
  77. data/lib/generators/rails_nexus/templates/INSTALL.md +9 -0
  78. data/lib/generators/rails_nexus/templates/backup/backup_helper.sh +152 -0
  79. data/lib/generators/rails_nexus/templates/backup/config.rb +27 -0
  80. data/lib/generators/rails_nexus/templates/backup/models/daily_backup.rb +24 -0
  81. data/lib/generators/rails_nexus/templates/backup/models/full_backup.rb +35 -0
  82. data/lib/generators/rails_nexus/templates/backup/models/sync_backup.rb +43 -0
  83. data/lib/generators/rails_nexus/templates/backup/mysql-config/db_config.cnf +15 -0
  84. data/lib/generators/rails_nexus/templates/backup/schedule.rb +28 -0
  85. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_controller.js +310 -0
  86. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_detail_controller.js +98 -0
  87. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_sidebar_controller.js +15 -0
  88. data/lib/generators/rails_nexus/templates/javascript/controllers/rails_nexus_theme_controller.js +30 -0
  89. data/lib/generators/rails_nexus/templates/migration.rb +24 -0
  90. data/lib/generators/rails_nexus/templates/migration_advanced_features.rb +30 -0
  91. data/lib/generators/rails_nexus/templates/migration_cron_jobs.rb +22 -0
  92. data/lib/generators/rails_nexus/templates/migration_new_schema.rb +120 -0
  93. data/lib/generators/rails_nexus/templates/migration_platform_detection.rb +11 -0
  94. data/lib/generators/rails_nexus/templates/migration_webhook_deliveries.rb +21 -0
  95. data/lib/generators/rails_nexus/templates/migration_workflow.rb +29 -0
  96. data/lib/generators/rails_nexus/templates/rails_nexus.rb +96 -0
  97. data/lib/generators/rails_nexus/templates/views/layouts/rails_nexus/application.html.erb +129 -0
  98. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_exceptions.html.erb +123 -0
  99. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/_show.html.erb +297 -0
  100. data/lib/generators/rails_nexus/templates/views/rails_nexus/logged_exceptions/index.html.erb +113 -0
  101. data/lib/rails_nexus/breadcrumbs.rb +161 -0
  102. data/lib/rails_nexus/cleanup.rb +32 -0
  103. data/lib/rails_nexus/configuration.rb +319 -0
  104. data/lib/rails_nexus/engine.rb +135 -0
  105. data/lib/rails_nexus/logger.rb +188 -0
  106. data/lib/rails_nexus/notifications.rb +105 -0
  107. data/lib/rails_nexus/ransack_config.rb +4 -0
  108. data/lib/rails_nexus/storm_protection.rb +124 -0
  109. data/lib/rails_nexus/version.rb +3 -0
  110. data/lib/rails_nexus.rb +133 -0
  111. data/lib/tasks/rails_nexus.rake +230 -0
  112. data/lib/tasks/rails_nexus_tasks.rake +37 -0
  113. data/rails_nexus.gemspec +36 -0
  114. metadata +252 -0
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class LoggedExceptionsController < ApplicationController
5
+ before_action :rails_nexus_require_auth!
6
+
7
+ RANSACK_FILTERS = %i[
8
+ action_name_cont assigned_to_cont controller_name_cont created_at_gteq created_at_lteq
9
+ exception_class_cont fingerprint_cont message_cont
10
+ message_or_exception_class_or_controller_name_or_action_name_cont
11
+ occurrence_count_gteq platform_eq priority_eq s
12
+ ].freeze
13
+
14
+ helper_method :exception_feature?, :params_filters, :prev_exception, :next_exception
15
+
16
+ def index
17
+ load_exception_index
18
+ end
19
+
20
+ def query
21
+ load_exception_index
22
+
23
+ respond_to do |format|
24
+ format.turbo_stream
25
+ format.html { render :query }
26
+ end
27
+ end
28
+
29
+ def feed
30
+ @exceptions = LoggedException.all
31
+
32
+ respond_to do |format|
33
+ format.rss { render layout: false }
34
+ end
35
+ end
36
+
37
+ def show
38
+ @exception = LoggedException.find(params[:id])
39
+ load_nav_exceptions if RailsNexus.configuration.enable_navigation
40
+ load_exception_tabs
41
+
42
+ respond_to do |format|
43
+ format.turbo_stream
44
+ format.html
45
+ end
46
+ end
47
+
48
+ def destroy
49
+ @exception = LoggedException.find(params[:id])
50
+ @exception.destroy!
51
+
52
+ respond_to do |format|
53
+ format.turbo_stream
54
+ format.html { redirect_to logged_exceptions_path, status: :see_other }
55
+ end
56
+ end
57
+
58
+ def destroy_all
59
+ exceptions = if params[:ids].present?
60
+ LoggedException.where(id: params[:ids].to_s.split(","))
61
+ else
62
+ ransack_search.result(distinct: false)
63
+ end
64
+ exceptions.delete_all
65
+
66
+ load_exception_index
67
+
68
+ respond_to do |format|
69
+ format.turbo_stream
70
+ format.html { redirect_to logged_exceptions_path(params_filters) }
71
+ end
72
+ end
73
+
74
+ def clear
75
+ LoggedException.delete_all
76
+
77
+ load_exception_index
78
+
79
+ respond_to do |format|
80
+ format.turbo_stream
81
+ format.html { redirect_back fallback_location: rails_nexus_root_path }
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def rails_nexus_root_path
88
+ main_app.respond_to?(:root_path) ? main_app.root_path : "/"
89
+ end
90
+
91
+ def page_size
92
+ requested = params[:per_page].to_i
93
+ options = RailsNexus.configuration.page_size_options
94
+ options.include?(requested) ? requested : (RailsNexus.configuration.per_page || 30)
95
+ end
96
+
97
+ def load_exception_index
98
+ load_filter_options
99
+ @q = ransack_search
100
+ load_stats if RailsNexus.configuration.show_stats
101
+ @pagy, @exceptions = pagy(@q.result(distinct: true), items: page_size)
102
+ rescue Pagy::OverflowError
103
+ @pagy, @exceptions = pagy(@q.result(distinct: true), page: 1, items: page_size)
104
+ end
105
+
106
+ def ransack_search
107
+ search = filtered_scope.ransack(ransack_params)
108
+ search.sorts = "created_at desc" if search.sorts.empty?
109
+ search
110
+ end
111
+
112
+ def ransack_params
113
+ return {} unless params[:q].respond_to?(:permit)
114
+
115
+ params.require(:q).permit(*RANSACK_FILTERS)
116
+ end
117
+
118
+ def params_filters
119
+ {
120
+ q: ransack_params.to_h.presence,
121
+ query: params[:query],
122
+ date_ranges_filter: params[:date_ranges_filter],
123
+ exception_names_filter: params[:exception_names_filter],
124
+ platform_filter: params[:platform_filter],
125
+ controller_actions_filter: params[:controller_actions_filter],
126
+ status_filter: params[:status_filter],
127
+ per_page: params[:per_page]
128
+ }.compact
129
+ end
130
+
131
+ def filtered_scope
132
+ exceptions = LoggedException.all
133
+ exceptions = exceptions.where(id: params[:id]) if params[:id].present?
134
+ exceptions = exceptions.message_like(params[:query]) if params[:query].present?
135
+ exceptions = exceptions.days_old(params[:date_ranges_filter]) if params[:date_ranges_filter].present?
136
+ exceptions = exceptions.by_exception_class(params[:exception_names_filter]) if params[:exception_names_filter].present?
137
+ exceptions = exceptions.by_platform(params[:platform_filter]) if params[:platform_filter].present? && exception_feature?(:platform)
138
+
139
+ if params[:controller_actions_filter].present?
140
+ controller_name, action_name = params[:controller_actions_filter].split("/", 2)
141
+ exceptions = exceptions.by_controller(controller_name)
142
+ exceptions = exceptions.by_action(action_name) if action_name.present?
143
+ end
144
+
145
+ apply_status_filter(exceptions)
146
+ end
147
+
148
+ def load_filter_options
149
+ @exception_names = LoggedException.class_names
150
+ @controller_actions = LoggedException.controller_actions
151
+ @assignees = if exception_feature?(:assigned_to)
152
+ LoggedException.where.not(assigned_to: [nil, ""]).distinct.order(:assigned_to).pluck(:assigned_to)
153
+ else
154
+ []
155
+ end
156
+ end
157
+
158
+ def apply_status_filter(scope)
159
+ return scope if params[:status_filter].blank?
160
+
161
+ case params[:status_filter]
162
+ when "active"
163
+ exception_feature?(:muted) && exception_feature?(:snoozed_until) ? scope.active : scope
164
+ when "muted"
165
+ exception_feature?(:muted) ? scope.muted : scope
166
+ when "snoozed"
167
+ exception_feature?(:snoozed_until) ? scope.snoozed : scope
168
+ when "unassigned"
169
+ exception_feature?(:assigned_to) ? scope.unassigned : scope
170
+ else
171
+ scope
172
+ end
173
+ end
174
+
175
+ def exception_feature?(column)
176
+ LoggedException.column_names.include?(column.to_s)
177
+ end
178
+
179
+ def load_stats
180
+ total = LoggedException.count
181
+ today = LoggedException.where("created_at >= ?", Time.zone.today.beginning_of_day).count
182
+ week = LoggedException.where("created_at >= ?", 7.days.ago.beginning_of_day).count
183
+ yesterday = LoggedException.where("created_at >= ?", 1.day.ago.beginning_of_day)
184
+ .where("created_at < ?", Time.zone.today.beginning_of_day).count
185
+
186
+ @stats = {
187
+ total: total,
188
+ today: today,
189
+ this_week: week,
190
+ yesterday: yesterday,
191
+ top_classes: LoggedException.group(:exception_class).limit(5).count,
192
+ top_controllers: LoggedException.group(:controller_name).limit(5).count
193
+ }
194
+ end
195
+
196
+ def load_nav_exceptions
197
+ scope = filtered_scope.sorted
198
+ ids = scope.pluck(:id)
199
+ current_idx = ids.index(@exception.id)
200
+
201
+ if current_idx
202
+ @prev_exception_id = ids[current_idx + 1]
203
+ @next_exception_id = ids[current_idx - 1]
204
+ end
205
+ end
206
+
207
+ def prev_exception
208
+ return nil unless @prev_exception_id
209
+ Struct.new(:id).new(@prev_exception_id)
210
+ end
211
+
212
+ def next_exception
213
+ return nil unless @next_exception_id
214
+ Struct.new(:id).new(@next_exception_id)
215
+ end
216
+
217
+ def load_exception_tabs
218
+ @exception_tabs = {
219
+ overview: true,
220
+ backtrace: @exception.backtrace.present?,
221
+ request: @exception.request.present?,
222
+ environment: @exception.environment.present?,
223
+ user_info: @exception.user_info.present?
224
+ }
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,10 @@
1
+ module RailsNexus
2
+ class N1PatternsController < ApplicationController
3
+ before_action :rails_nexus_require_auth!
4
+
5
+ def index
6
+ @patterns = RailsNexus::LoggedException.n_plus_one_patterns(limit: 50)
7
+ @summary = RailsNexus::LoggedException.n_plus_one_summary
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class SettingsController < ApplicationController
5
+ before_action :verify_access
6
+
7
+ def index
8
+ @webhooks = RailsNexus.configuration.webhooks || []
9
+ @webhook_headers = RailsNexus.configuration.webhook_headers || {}
10
+ @webhook_timeout = RailsNexus.configuration.webhook_timeout || 5
11
+ @recent_deliveries = WebhookDelivery.recent.limit(20)
12
+ @delivery_stats = WebhookDelivery.summary
13
+ end
14
+
15
+ def test_webhook
16
+ url = params[:url]
17
+ if url.blank?
18
+ redirect_to settings_path, alert: "URL is required."
19
+ return
20
+ end
21
+
22
+ payload = build_test_payload
23
+ result = deliver_test_webhook(url, payload)
24
+
25
+ if result[:success]
26
+ redirect_to settings_path, notice: "Webhook test successful! Response: #{result[:status_code]}"
27
+ else
28
+ redirect_to settings_path, alert: "Webhook test failed: #{result[:error]}"
29
+ end
30
+ end
31
+
32
+ def deliveries
33
+ @deliveries = WebhookDelivery.recent.limit(25).offset(((params[:page] || 1).to_i - 1) * 25)
34
+
35
+ if params[:status].present?
36
+ @deliveries = @deliveries.by_status(params[:status])
37
+ end
38
+
39
+ if params[:url].present?
40
+ @deliveries = @deliveries.where(url: params[:url])
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def verify_access
47
+ config = RailsNexus.configuration
48
+ return if config.auth_block.nil?
49
+ unless config.auth_block&.call(self)
50
+ render plain: "Forbidden", status: :forbidden
51
+ end
52
+ end
53
+
54
+ def build_test_payload
55
+ {
56
+ event: "webhook.test",
57
+ timestamp: Time.current.iso8601,
58
+ message: "This is a test webhook from RailsNexus",
59
+ exception: {
60
+ class: "TestException",
61
+ message: "This is a test exception for webhook verification",
62
+ controller: "rails_nexus/settings",
63
+ action: "test_webhook"
64
+ }
65
+ }
66
+ end
67
+
68
+ def deliver_test_webhook(url, payload)
69
+ require "net/http"
70
+ require "uri"
71
+ require "json"
72
+
73
+ uri = URI(url)
74
+ http = Net::HTTP.new(uri.host, uri.port)
75
+ http.use_ssl = uri.scheme == "https"
76
+ http.open_timeout = 10
77
+ http.read_timeout = 10
78
+
79
+ request = Net::HTTP::Post.new(uri)
80
+ request["Content-Type"] = "application/json"
81
+ request.body = payload.to_json
82
+
83
+ start_time = Time.now
84
+ response = http.request(request)
85
+ duration = Time.now - start_time
86
+
87
+ {
88
+ success: response.is_a?(Net::HTTPSuccess),
89
+ status_code: response.code,
90
+ body: response.body&.truncate(500),
91
+ duration: duration.round(3)
92
+ }
93
+ rescue StandardError => e
94
+ {
95
+ success: false,
96
+ error: e.message
97
+ }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsNexus
4
+ class SourceCodeController < ApplicationController
5
+ include SourceCodeHelper
6
+
7
+ # GET /rails_nexus/source_code?file=...&line=...
8
+ def show
9
+ file_path = params[:file]
10
+ line_number = params[:line]&.to_i
11
+
12
+ if file_path.blank? || line_number.blank?
13
+ render json: { error: "file and line parameters required" }, status: :bad_request
14
+ return
15
+ end
16
+
17
+ # Security: only allow reading files within Rails.root
18
+ begin
19
+ real_path = File.realpath(file_path)
20
+ unless real_path.start_with?(Rails.root.to_s)
21
+ render json: { error: "Access denied" }, status: :forbidden
22
+ return
23
+ end
24
+ rescue Errno::ENOENT
25
+ render json: { error: "File not found" }, status: :not_found
26
+ return
27
+ end
28
+
29
+ source = read_source_snippet(file_path, line_number, context_lines: params[:context]&.to_i || 5)
30
+ blame = git_blame_for_line(file_path, line_number)
31
+
32
+ if source
33
+ render json: { source: source, blame: blame }
34
+ else
35
+ render json: { error: "Could not read file" }, status: :not_found
36
+ end
37
+ end
38
+ end
39
+ end