exception_hunter 0.1.0 → 0.4.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +56 -11
  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 +20 -7
  15. data/app/models/exception_hunter/error_group.rb +23 -4
  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 +9 -0
  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 +15 -3
  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 +16 -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,3 +1,21 @@
1
1
  ExceptionHunter::Engine.routes.draw do
2
- resources :errors, only: %i[index show]
2
+ resources :errors, only: %i[index show] do
3
+ delete 'purge', on: :collection, to: 'errors#destroy', as: :purge
4
+ end
5
+
6
+ resources :resolved_errors, only: %i[create]
7
+
8
+ get '/', to: redirect('/exception_hunter/errors')
9
+
10
+ if ExceptionHunter::Config.auth_enabled?
11
+ admin_user_class = ExceptionHunter::Config.admin_user_class.underscore.to_sym
12
+
13
+ devise_scope admin_user_class do
14
+ get '/login', to: 'devise/sessions#new', as: :exception_hunter_login
15
+ post '/login', to: 'devise/sessions#create', as: :exception_hunter_create_session
16
+ get '/logout', to: 'devise/sessions#destroy', as: :exception_hunter_logout
17
+ end
18
+
19
+ devise_for admin_user_class, only: []
20
+ end
3
21
  end
@@ -1,15 +1,25 @@
1
+ require 'pagy'
2
+
1
3
  require 'exception_hunter/engine'
2
- require 'exception_hunter/railtie'
4
+ require 'exception_hunter/middleware/request_hunter'
3
5
  require 'exception_hunter/config'
6
+ require 'exception_hunter/error_creator'
7
+ require 'exception_hunter/error_reaper'
8
+ require 'exception_hunter/tracking'
4
9
  require 'exception_hunter/user_attributes_collector'
5
- require 'pagy'
6
10
 
7
11
  module ExceptionHunter
12
+ autoload :Devise, 'exception_hunter/devise'
13
+
14
+ extend ::ExceptionHunter::Tracking
15
+
8
16
  def self.setup(&block)
9
17
  block.call(Config)
10
18
  end
11
19
 
12
20
  def self.routes(router)
21
+ return unless Config.enabled
22
+
13
23
  router.mount(ExceptionHunter::Engine, at: 'exception_hunter')
14
24
  end
15
25
  end
@@ -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
13
  error = Error.new(error_attrs)
8
14
  error_group = ErrorGroup.find_matching_group(error) || ErrorGroup.new
9
- update_error_group(error_group, error)
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,16 @@
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
+ )
12
+
13
+ nil
14
+ end
15
+ end
16
+ 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.0'.freeze
2
+ VERSION = '0.4.1'.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