exception_hunter 0.1.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -7
  3. data/app/assets/stylesheets/exception_hunter/base.css +62 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +166 -24
  5. data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
  6. data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
  7. data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
  8. data/app/controllers/exception_hunter/application_controller.rb +2 -0
  9. data/app/controllers/exception_hunter/errors_controller.rb +27 -4
  10. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  11. data/app/helpers/exception_hunter/errors_helper.rb +7 -0
  12. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  13. data/app/models/exception_hunter/application_record.rb +8 -0
  14. data/app/models/exception_hunter/error.rb +21 -8
  15. data/app/models/exception_hunter/error_group.rb +24 -5
  16. data/app/presenters/exception_hunter/dashboard_presenter.rb +54 -0
  17. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  18. data/app/presenters/exception_hunter/error_presenter.rb +10 -1
  19. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  20. data/app/views/exception_hunter/errors/_error_row.erb +44 -0
  21. data/app/views/exception_hunter/errors/_error_summary.erb +23 -10
  22. data/app/views/exception_hunter/errors/_error_user_data.erb +4 -5
  23. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  24. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  25. data/app/views/exception_hunter/errors/index.html.erb +71 -30
  26. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  27. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  28. data/app/views/layouts/exception_hunter/application.html.erb +65 -6
  29. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  30. data/config/rails_best_practices.yml +2 -3
  31. data/config/routes.rb +19 -1
  32. data/lib/exception_hunter.rb +12 -2
  33. data/lib/exception_hunter/config.rb +8 -1
  34. data/lib/exception_hunter/devise.rb +17 -0
  35. data/lib/exception_hunter/engine.rb +5 -0
  36. data/{app/services → lib}/exception_hunter/error_creator.rb +17 -5
  37. data/lib/exception_hunter/error_reaper.rb +12 -0
  38. data/lib/exception_hunter/middleware/delayed_job_hunter.rb +69 -0
  39. data/lib/exception_hunter/middleware/request_hunter.rb +71 -0
  40. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +59 -0
  41. data/lib/exception_hunter/tracking.rb +17 -0
  42. data/lib/exception_hunter/user_attributes_collector.rb +4 -0
  43. data/lib/exception_hunter/version.rb +1 -1
  44. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  45. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  46. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  47. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +23 -0
  48. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  49. metadata +25 -10
  50. data/config/initializers/exception_hunter.rb +0 -16
  51. data/lib/exception_hunter/railtie.rb +0 -11
  52. data/lib/exception_hunter/request_hunter.rb +0 -41
@@ -1,5 +1,12 @@
1
1
  module ExceptionHunter
2
2
  class Config
3
- cattr_accessor :current_user_method, :user_attributes
3
+ cattr_accessor :admin_user_class,
4
+ :current_user_method, :user_attributes
5
+ cattr_accessor :enabled, default: true
6
+ cattr_accessor :errors_stale_time, default: 45.days
7
+
8
+ def self.auth_enabled?
9
+ admin_user_class.present? && admin_user_class.try(:underscore)
10
+ end
4
11
  end
5
12
  end
@@ -0,0 +1,17 @@
1
+ module ExceptionHunter
2
+ module Devise
3
+ class SessionsController < ::Devise::SessionsController
4
+ skip_before_action :verify_authenticity_token
5
+
6
+ layout 'exception_hunter/exception_hunter_logged_out'
7
+
8
+ def after_sign_out_path_for(*)
9
+ '/exception_hunter/login'
10
+ end
11
+
12
+ def after_sign_in_path_for(*)
13
+ '/exception_hunter'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -12,5 +12,10 @@ module ExceptionHunter
12
12
  app.config.assets.precompile << 'exception_hunter/application.css'
13
13
  app.config.assets.precompile << 'exception_hunter/logo.png'
14
14
  end
15
+
16
+ initializer 'exception_hunter.load_middleware', group: :all do
17
+ require 'exception_hunter/middleware/sidekiq_hunter' if defined?(Sidekiq)
18
+ require 'exception_hunter/middleware/delayed_job_hunter' if defined?(Delayed)
19
+ end
15
20
  end
16
21
  end
@@ -1,12 +1,18 @@
1
1
  module ExceptionHunter
2
2
  class ErrorCreator
3
+ HTTP_TAG = 'HTTP'.freeze
4
+ WORKER_TAG = 'Worker'.freeze
5
+ MANUAL_TAG = 'Manual'.freeze
6
+
3
7
  class << self
4
- def call(**error_attrs)
8
+ def call(tag: nil, **error_attrs)
9
+ return unless should_create?
10
+
5
11
  ActiveRecord::Base.transaction do
6
12
  error_attrs = extract_user_data(error_attrs)
7
- error = Error.new(error_attrs)
8
- error_group = ErrorGroup.find_matching_group(error) || ErrorGroup.new
9
- update_error_group(error_group, error)
13
+ error = ::ExceptionHunter::Error.new(error_attrs)
14
+ error_group = ::ExceptionHunter::ErrorGroup.find_matching_group(error) || ::ExceptionHunter::ErrorGroup.new
15
+ update_error_group(error_group, error, tag)
10
16
  error.error_group = error_group
11
17
  error.save!
12
18
  error
@@ -17,9 +23,15 @@ module ExceptionHunter
17
23
 
18
24
  private
19
25
 
20
- def update_error_group(error_group, error)
26
+ def should_create?
27
+ Config.enabled
28
+ end
29
+
30
+ def update_error_group(error_group, error, tag)
21
31
  error_group.error_class_name = error.class_name
22
32
  error_group.message = error.message
33
+ error_group.tags << tag unless tag.nil?
34
+ error_group.tags.uniq!
23
35
 
24
36
  error_group.save!
25
37
  end
@@ -0,0 +1,12 @@
1
+ module ExceptionHunter
2
+ class ErrorReaper
3
+ class << self
4
+ def purge(stale_time: Config.errors_stale_time)
5
+ ActiveRecord::Base.transaction do
6
+ Error.with_occurrences_before(Date.today - stale_time).destroy_all
7
+ ErrorGroup.without_errors.destroy_all
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,69 @@
1
+ require 'delayed_job'
2
+
3
+ module ExceptionHunter
4
+ module Middleware
5
+ class DelayedJobHunter < ::Delayed::Plugin
6
+ TRACK_AT_RETRY = [0, 3, 6, 10].freeze
7
+ JOB_TRACKED_DATA = %w[
8
+ attempts
9
+ ].freeze
10
+ ARGS_TRACKED_DATA = %w[
11
+ queue_name
12
+ job_class
13
+ job_id
14
+ arguments
15
+ enqueued_at
16
+ ].freeze
17
+
18
+ callbacks do |lifecycle|
19
+ lifecycle.around(:invoke_job) do |job, *args, &block|
20
+ block.call(job, *args)
21
+
22
+ rescue Exception => exception # rubocop:disable Lint/RescueException
23
+ track_exception(exception, job)
24
+
25
+ raise exception
26
+ end
27
+ end
28
+
29
+ def self.track_exception(exception, job)
30
+ return unless should_track?(job.attempts)
31
+
32
+ ErrorCreator.call(
33
+ tag: ErrorCreator::WORKER_TAG,
34
+ class_name: exception.class.to_s,
35
+ message: exception.message,
36
+ environment_data: environment_data(job),
37
+ backtrace: exception.backtrace
38
+ )
39
+ end
40
+
41
+ def self.environment_data(job)
42
+ job_data =
43
+ JOB_TRACKED_DATA.each_with_object({}) do |data_param, dict|
44
+ dict.merge(data_param => job.try(data_param))
45
+ end
46
+
47
+ job_class = if job.payload_object.class.name == 'ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper'
48
+ # support for Rails 4.2 ActiveJob
49
+ job.payload_object.job_data['job_class']
50
+ elsif job.payload_object.object.is_a?(Class)
51
+ job.payload_object.object.name
52
+ else
53
+ job.payload_object.object.class.name
54
+ end
55
+ args_data = (job.payload_object.try(:job_data) || {}).select { |key, _value| ARGS_TRACKED_DATA.include?(key) }
56
+
57
+ args_data['job_class'] = job_class || job.payload_object.class.name if args_data['job_class'].nil?
58
+
59
+ job_data.merge(args_data)
60
+ end
61
+
62
+ def self.should_track?(attempts)
63
+ TRACK_AT_RETRY.include?(attempts)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ Delayed::Worker.plugins << ExceptionHunter::Middleware::DelayedJobHunter
@@ -0,0 +1,71 @@
1
+ module ExceptionHunter
2
+ module Middleware
3
+ class RequestHunter
4
+ ENVIRONMENT_KEYS =
5
+ %w[PATH_INFO
6
+ QUERY_STRING
7
+ REMOTE_HOST
8
+ REQUEST_METHOD
9
+ REQUEST_URI
10
+ SERVER_PROTOCOL
11
+ HTTP_HOST
12
+ CONTENT_TYPE
13
+ HTTP_USER_AGENT].freeze
14
+
15
+ FILTERED_PARAMS = [/password/].freeze
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ @app.call(env)
23
+ rescue Exception => exception # rubocop:disable Lint/RescueException
24
+ catch_prey(env, exception)
25
+ raise exception
26
+ end
27
+
28
+ private
29
+
30
+ def catch_prey(env, exception)
31
+ user = user_from_env(env)
32
+ ErrorCreator.call(
33
+ tag: ErrorCreator::HTTP_TAG,
34
+ class_name: exception.class.to_s,
35
+ message: exception.message,
36
+ environment_data: environment_data(env),
37
+ backtrace: exception.backtrace,
38
+ user: user
39
+ )
40
+ end
41
+
42
+ def environment_data(env)
43
+ env
44
+ .select { |key, _value| ENVIRONMENT_KEYS.include?(key) }
45
+ .merge(params: filtered_sensitive_params(env))
46
+ end
47
+
48
+ def user_from_env(env)
49
+ current_user_method = Config.current_user_method
50
+ controller = env['action_controller.instance']
51
+ controller.try(current_user_method)
52
+ end
53
+
54
+ def filtered_sensitive_params(env)
55
+ params = env['action_dispatch.request.parameters']
56
+ parameter_filter = ::ActiveSupport::ParameterFilter.new(FILTERED_PARAMS)
57
+ parameter_filter.filter(params || {})
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ module ExceptionHunter
64
+ class Railtie < Rails::Railtie
65
+ initializer 'exception_hunter.add_middleware', after: :load_config_initializers do |app|
66
+ app.config.middleware.insert_after(
67
+ ActionDispatch::DebugExceptions, ExceptionHunter::Middleware::RequestHunter
68
+ )
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,59 @@
1
+ module ExceptionHunter
2
+ module Middleware
3
+ # Middleware to report errors
4
+ # when a Sidekiq worker fails
5
+ #
6
+ class SidekiqHunter
7
+ TRACK_AT_RETRY = [0, 3, 6, 10].freeze
8
+ JOB_TRACKED_DATA = %w[
9
+ queue
10
+ retry_count
11
+ ].freeze
12
+ ARGS_TRACKED_DATA = %w[
13
+ job_class
14
+ job_id
15
+ arguments
16
+ enqueued_at
17
+ ].freeze
18
+
19
+ def call(_worker, context, _queue)
20
+ yield
21
+ rescue Exception => exception # rubocop:disable Lint/RescueException
22
+ track_exception(exception, context)
23
+ raise exception
24
+ end
25
+
26
+ private
27
+
28
+ def track_exception(exception, context)
29
+ return unless should_track?(context)
30
+
31
+ ErrorCreator.call(
32
+ tag: ErrorCreator::WORKER_TAG,
33
+ class_name: exception.class.to_s,
34
+ message: exception.message,
35
+ environment_data: environment_data(context),
36
+ backtrace: exception.backtrace
37
+ )
38
+ end
39
+
40
+ def environment_data(context)
41
+ job_data = context.select { |key, _value| JOB_TRACKED_DATA.include?(key) }
42
+ args_data = (context['args']&.first || {}).select { |key, _value| ARGS_TRACKED_DATA.include?(key) }
43
+
44
+ job_data.merge(args_data)
45
+ end
46
+
47
+ def should_track?(context)
48
+ TRACK_AT_RETRY.include?(context['retry_count'].to_i)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # As seen in https://github.com/mperham/sidekiq/wiki/Error-Handling
55
+ Sidekiq.configure_server do |config|
56
+ config.server_middleware do |chain|
57
+ chain.add(ExceptionHunter::Middleware::SidekiqHunter)
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ module ExceptionHunter
2
+ module Tracking
3
+ def track(exception, custom_data: {}, user: nil)
4
+ ErrorCreator.call(
5
+ tag: ErrorCreator::MANUAL_TAG,
6
+ class_name: exception.class.to_s,
7
+ message: exception.message,
8
+ backtrace: exception.backtrace,
9
+ custom_data: custom_data,
10
+ user: user,
11
+ environment_data: {}
12
+ )
13
+
14
+ nil
15
+ end
16
+ end
17
+ end
@@ -3,11 +3,15 @@ module ExceptionHunter
3
3
  extend self
4
4
 
5
5
  def collect_attributes(user)
6
+ return unless user
7
+
6
8
  attributes.reduce({}) do |data, attribute|
7
9
  data.merge(attribute => user.try(attribute))
8
10
  end
9
11
  end
10
12
 
13
+ private
14
+
11
15
  def attributes
12
16
  Config.user_attributes
13
17
  end
@@ -1,3 +1,3 @@
1
1
  module ExceptionHunter
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.4.2'.freeze
3
3
  end
@@ -22,7 +22,14 @@ module ExceptionHunter
22
22
  end
23
23
 
24
24
  def create_admin_user
25
- invoke 'devise', [name]
25
+ invoke 'devise', [name], routes: false
26
+ end
27
+
28
+ def remove_registerable_from_model
29
+ return if options[:registerable]
30
+
31
+ model_file = File.join(destination_root, 'app', 'models', "#{file_path}.rb")
32
+ gsub_file model_file, /\:registerable([.]*,)?/, ''
26
33
  end
27
34
  end
28
35
  end
@@ -15,7 +15,9 @@ module ExceptionHunter
15
15
 
16
16
  def setup_routes
17
17
  if options[:users]
18
- inject_into_file 'config/routes.rb', "\n ExceptionHunter.routes(self)", after: /devise_for .*/
18
+ gsub_file 'config/routes.rb',
19
+ "\n devise_for :#{plural_table_name}, skip: :all",
20
+ "\n ExceptionHunter.routes(self)"
19
21
  else
20
22
  route 'ExceptionHunter.routes(self)'
21
23
  end
@@ -5,10 +5,13 @@ class CreateExceptionHunterErrorGroups < ActiveRecord::Migration[<%= ActiveRecor
5
5
  create_table :exception_hunter_error_groups do |t|
6
6
  t.string :error_class_name, null: false
7
7
  t.string :message
8
+ t.integer :status, default: 0
9
+ t.text :tags, array: true, default: []
8
10
 
9
11
  t.timestamps
10
12
 
11
13
  t.index :message, opclass: :gin_trgm_ops, using: :gin
14
+ t.index :status
12
15
  end
13
16
  end
14
17
  end
@@ -1,4 +1,19 @@
1
1
  ExceptionHunter.setup do |config|
2
+ # == Enabling
3
+ #
4
+ # This flag allows disabling error tracking, it's set to track in
5
+ # any environment but development or test by default
6
+ #
7
+ config.enabled = !(Rails.env.development? || Rails.env.test?)
8
+
9
+ # == Dashboard User
10
+ # Exception Hunter allows you to restrict users who can see the dashboard
11
+ # to the ones included in the database. You can change the table name in
12
+ # case you are not satisfied with the default one. You can also remove the
13
+ # configuration if you wish to have no access restrictions for the dashboard.
14
+ #
15
+ <%= @use_authentication_method ? "config.admin_user_class = '#{name}'" : "# config.admin_user_class = '#{name}'" %>
16
+
2
17
  # == Current User
3
18
  #
4
19
  # Exception Hunter will include the user as part of the environment
@@ -15,4 +30,12 @@ ExceptionHunter.setup do |config|
15
30
  # as part of the user information that is kept from the request.
16
31
  #
17
32
  config.user_attributes = [:id, :email]
33
+
34
+ # == Stale errors
35
+ #
36
+ # You can configure how long it takes for errors to go stale. This is
37
+ # taken into account when purging old error messages but nothing will
38
+ # happen automatically.
39
+ #
40
+ # config.errors_stale_time = 45.days
18
41
  end
@@ -1,4 +1,6 @@
1
- # desc "Explaining what the task does"
2
- # task :exception_hunter do
3
- # # Task goes here
4
- # end
1
+ namespace :exception_hunter do
2
+ desc 'Purges old errors'
3
+ task purge_errors: [:environment] do
4
+ ::ExceptionHunter::ErrorReaper.call
5
+ end
6
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exception_hunter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vezoli
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-05-17 00:00:00.000000000 Z
12
+ date: 2020-09-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pagy
@@ -17,14 +17,14 @@ dependencies:
17
17
  requirements:
18
18
  - - "~>"
19
19
  - !ruby/object:Gem::Version
20
- version: '3.8'
20
+ version: '3'
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
- version: '3.8'
27
+ version: '3'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: brakeman
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -115,14 +115,14 @@ dependencies:
115
115
  requirements:
116
116
  - - "~>"
117
117
  - !ruby/object:Gem::Version
118
- version: 0.18.5
118
+ version: 0.17.1
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: 0.18.5
125
+ version: 0.17.1
126
126
  description:
127
127
  email:
128
128
  - bruno.vezoli@rootstrap.com
@@ -139,31 +139,46 @@ files:
139
139
  - app/assets/stylesheets/exception_hunter/base.css
140
140
  - app/assets/stylesheets/exception_hunter/errors.css
141
141
  - app/assets/stylesheets/exception_hunter/navigation.css
142
+ - app/assets/stylesheets/exception_hunter/sessions.css
143
+ - app/controllers/concerns/exception_hunter/authorization.rb
142
144
  - app/controllers/exception_hunter/application_controller.rb
143
145
  - app/controllers/exception_hunter/errors_controller.rb
146
+ - app/controllers/exception_hunter/resolved_errors_controller.rb
144
147
  - app/helpers/exception_hunter/application_helper.rb
148
+ - app/helpers/exception_hunter/errors_helper.rb
149
+ - app/helpers/exception_hunter/sessions_helper.rb
145
150
  - app/jobs/exception_hunter/application_job.rb
146
151
  - app/mailers/exception_hunter/application_mailer.rb
147
152
  - app/models/exception_hunter/application_record.rb
148
153
  - app/models/exception_hunter/error.rb
149
154
  - app/models/exception_hunter/error_group.rb
155
+ - app/presenters/exception_hunter/dashboard_presenter.rb
156
+ - app/presenters/exception_hunter/error_group_presenter.rb
150
157
  - app/presenters/exception_hunter/error_presenter.rb
151
- - app/services/exception_hunter/error_creator.rb
158
+ - app/views/exception_hunter/devise/sessions/new.html.erb
152
159
  - app/views/exception_hunter/errors/_error_backtrace.erb
160
+ - app/views/exception_hunter/errors/_error_row.erb
153
161
  - app/views/exception_hunter/errors/_error_summary.erb
154
162
  - app/views/exception_hunter/errors/_error_user_data.erb
163
+ - app/views/exception_hunter/errors/_errors_table.erb
164
+ - app/views/exception_hunter/errors/_last_7_days_errors_table.erb
155
165
  - app/views/exception_hunter/errors/index.html.erb
156
166
  - app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb
157
167
  - app/views/exception_hunter/errors/show.html.erb
158
168
  - app/views/layouts/exception_hunter/application.html.erb
159
- - config/initializers/exception_hunter.rb
169
+ - app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb
160
170
  - config/rails_best_practices.yml
161
171
  - config/routes.rb
162
172
  - lib/exception_hunter.rb
163
173
  - lib/exception_hunter/config.rb
174
+ - lib/exception_hunter/devise.rb
164
175
  - lib/exception_hunter/engine.rb
165
- - lib/exception_hunter/railtie.rb
166
- - lib/exception_hunter/request_hunter.rb
176
+ - lib/exception_hunter/error_creator.rb
177
+ - lib/exception_hunter/error_reaper.rb
178
+ - lib/exception_hunter/middleware/delayed_job_hunter.rb
179
+ - lib/exception_hunter/middleware/request_hunter.rb
180
+ - lib/exception_hunter/middleware/sidekiq_hunter.rb
181
+ - lib/exception_hunter/tracking.rb
167
182
  - lib/exception_hunter/user_attributes_collector.rb
168
183
  - lib/exception_hunter/version.rb
169
184
  - lib/generators/exception_hunter/create_users/create_users_generator.rb