exception_hunter 0.1.1 → 0.4.2

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 +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