exception_hunter 0.2.0 → 0.3.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -6
  3. data/app/assets/stylesheets/exception_hunter/base.css +62 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +163 -25
  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/sessions_helper.rb +16 -0
  12. data/app/models/exception_hunter/application_record.rb +8 -0
  13. data/app/models/exception_hunter/error.rb +20 -7
  14. data/app/models/exception_hunter/error_group.rb +23 -4
  15. data/app/presenters/exception_hunter/dashboard_presenter.rb +54 -0
  16. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  17. data/app/presenters/exception_hunter/error_presenter.rb +1 -0
  18. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  19. data/app/views/exception_hunter/errors/_error_row.erb +44 -0
  20. data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
  21. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  22. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  23. data/app/views/exception_hunter/errors/index.html.erb +71 -30
  24. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  25. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  26. data/app/views/layouts/exception_hunter/application.html.erb +65 -6
  27. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  28. data/config/rails_best_practices.yml +2 -2
  29. data/config/routes.rb +19 -1
  30. data/lib/exception_hunter.rb +11 -1
  31. data/lib/exception_hunter/config.rb +8 -1
  32. data/lib/exception_hunter/devise.rb +17 -0
  33. data/{app/services → lib}/exception_hunter/error_creator.rb +15 -3
  34. data/lib/exception_hunter/error_reaper.rb +12 -0
  35. data/lib/exception_hunter/middleware/request_hunter.rb +1 -0
  36. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -0
  37. data/lib/exception_hunter/tracking.rb +16 -0
  38. data/lib/exception_hunter/user_attributes_collector.rb +4 -0
  39. data/lib/exception_hunter/version.rb +1 -1
  40. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  41. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  42. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  43. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +23 -0
  44. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  45. metadata +18 -5
  46. data/config/initializers/exception_hunter.rb +0 -16
@@ -1,17 +1,17 @@
1
- <% link = pagy_link_proc(pagy) %>
2
- <nav aria-label="pager" class="pagy_nav pagination" role="navigation">
3
- <% if pagy.prev %>
4
- <span class="page prev"><%== link.call(pagy.prev, '◄', 'aria-label="previous"') %></span>
5
- <% else %>
6
- <span class="page prev disabled">
7
-
8
- </span>
1
+ <div class="error-occurrences__nav">
2
+ <%= link_to pagy_url_for(1, pagy) do %>
3
+ <%= button_tag 'First', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
9
4
  <% end %>
10
- <% if pagy.next %>
11
- <span class="page next"><%== link.call(pagy.next, '►', 'aria-label="next"') %></span>
12
- <% else %>
13
- <span class="page next disabled">
14
-
15
- </span>
5
+ <%= link_to pagy_url_for(pagy.prev, pagy) do %>
6
+ <%= button_tag '<', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.prev.nil? %>
16
7
  <% end %>
17
- </nav>
8
+ <div class="error-occurrences__nav-current">
9
+ <%= occurred_at %> (<%= pagy.page %>/<%= pagy.last %>)
10
+ </div>
11
+ <%= link_to pagy_url_for(pagy.next, pagy) do %>
12
+ <%= button_tag '>', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
13
+ <% end %>
14
+ <%= link_to pagy_url_for(pagy.last, pagy) do %>
15
+ <%= button_tag 'Last', class: %w[button button-outline error-occurrences__nav-link], disabled: pagy.next.nil? %>
16
+ <% end %>
17
+ </div>
@@ -1,33 +1,69 @@
1
- <div class="row">
2
- <div class="column column-offset-80 column-20">
3
- <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy } %>
1
+ <div class="row error-occurrence__header">
2
+ <div class="column column-10">
3
+ <% @error.tags.each do|tag| %>
4
+ <div class="error-cell__tags">
5
+ <span class="error-tag"><%= tag %></span>
6
+ </div>
7
+ <% end %>
8
+ </div>
9
+ <div class="column column-80">
10
+ <div class="error-title">
11
+ <%= @error.class_name %>: <%= @error.message %>
12
+ </div>
13
+ </div>
14
+ <div class="column column-10">
15
+ <%= button_to('Resolve', resolved_errors_path(error_group: { id: @error.error_group_id }),
16
+ method: :post,
17
+ class: %w[button resolve-button],
18
+ data: { confirm: 'Are you sure you want to resolve this error?' }) %>
4
19
  </div>
5
20
  </div>
6
21
 
7
- <div class="error-title">
8
- <%= @error.class_name %>: <%= @error.message %>
9
- </div>
10
- <div class="error-occurred_at">
11
- <%= @error.occurred_at %>
22
+ <div class="row">
23
+ <div class="column column-50">
24
+ <ul data-tabs class="errors-tabs">
25
+ <li class="errors-tab">
26
+ <a data-tabby-default href="#summary">
27
+ <div class="errors-tab__content">
28
+ Summary
29
+ </div>
30
+ </a>
31
+ </li>
32
+ <li class="errors-tab">
33
+ <a href="#backtrace">
34
+ <div class="errors-tab__content">
35
+ Backtrace
36
+ </div>
37
+ </a>
38
+ </li>
39
+ <li class="errors-tab">
40
+ <a href="#user-data">
41
+ <div class="errors-tab__content">
42
+ User Data
43
+ </div>
44
+ </a>
45
+ </li>
46
+ </ul>
47
+ </div>
48
+ <div class="column column-50">
49
+ <%= render partial: 'exception_hunter/errors/pagy/pagy_nav', locals: { pagy: @pagy, occurred_at: @error.occurred_at } %>
50
+ </div>
12
51
  </div>
13
52
 
14
- <ul data-tabs>
15
- <li><a data-tabby-default href="#summary">Summary</a></li>
16
- <li><a href="#backtrace">Backtrace</a></li>
17
- <li><a href="#user-data">User Data</a></li>
18
- </ul>
19
53
 
20
- <div class="tab-content">
21
- <div id="summary">
22
- <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
23
- </div>
54
+ <div class="errors__container">
55
+ <div class="tab-content">
56
+ <div id="summary">
57
+ <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
58
+ </div>
24
59
 
25
- <div id="backtrace">
26
- <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
27
- </div>
60
+ <div id="backtrace">
61
+ <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
62
+ </div>
28
63
 
29
- <div id="user-data">
30
- <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
64
+ <div id="user-data">
65
+ <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
66
+ </div>
31
67
  </div>
32
68
  </div>
33
69
 
@@ -7,12 +7,11 @@
7
7
 
8
8
  <%= favicon_link_tag 'exception_hunter/logo.png' %>
9
9
 
10
- <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
10
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
11
11
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
12
12
  <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">
13
13
 
14
14
  <!-- Get patch fixes within a minor version -->
15
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/css/tabby-ui.min.css">
16
15
  <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/js/tabby.polyfills.min.js"></script>
17
16
 
18
17
  <%= stylesheet_link_tag "exception_hunter/application", media: "all" %>
@@ -22,18 +21,78 @@
22
21
  <div class="wrapper">
23
22
  <nav class="nav">
24
23
  <div class="container container__nav">
25
- <div class="nav__logo">
26
- <%= link_to errors_path do %>
27
- <%= image_tag 'exception_hunter/logo.png', alt: 'Logo' %>
28
- <% end %>
24
+ <div class="row">
25
+ <div class="column column-40">
26
+ <%= link_to errors_path do %>
27
+ <div class="nav__title">Exception Hunter</div>
28
+ <% end %>
29
+ </div>
30
+ <div class="column column-10 column-offset-50">
31
+ <div class="logout">
32
+ <% if current_admin_user? %>
33
+ <div class="sign_out">
34
+ <%= link_to exception_hunter_logout_path do %>
35
+ Sign out
36
+ <% end %>
37
+ </div>
38
+ <% end %>
39
+ </div>
40
+ </div>
29
41
  </div>
30
42
  </div>
31
43
  </nav>
32
44
 
33
45
  <div class="container">
46
+ <% if flash[:notice] %>
47
+ <div id="flash-notice" class="flash flash--notice row">
48
+ <div class="column column-80">
49
+ <%= flash[:notice] %>
50
+ </div>
51
+ <div class="column column-20">
52
+ <div class="button button-clear button-dismiss" data-dismiss="#flash-notice">
53
+ <%= ['Cool!', 'Nice!', 'Ok', 'Dismiss', 'Fine. Whatever.', 'I know that'].sample %>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <% end %>
58
+
34
59
  <%= yield %>
35
60
  </div>
61
+
62
+ <div class="footer">
63
+ Made with ♥ at <a href="https://www.rootstrap.com/" class="text--underline">Rootstrap</a>
64
+ </div>
36
65
  </div>
37
66
 
67
+ <script type="text/javascript" charset="utf-8">
68
+ function confirmable(element) {
69
+ const message = element.getAttribute('data-confirm')
70
+ element.form.addEventListener('submit', confirm(message))
71
+
72
+ function confirm(message) {
73
+ return function (event) {
74
+ if (!window.confirm(message)) {
75
+ event.preventDefault()
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ function dismissible(element) {
82
+ element.addEventListener('click', dismiss(element))
83
+
84
+ function dismiss(element) {
85
+ return function () {
86
+ const selector = element.getAttribute('data-dismiss')
87
+ const elementToDismiss = document.querySelector(selector)
88
+ elementToDismiss.remove()
89
+ }
90
+ }
91
+ }
92
+
93
+ Array.from(document.querySelectorAll('[data-confirm]')).forEach(confirmable)
94
+ Array.from(document.querySelectorAll('[data-dismiss]')).forEach(dismissible)
95
+ </script>
96
+
38
97
  </body>
39
98
  </html>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Exception Hunter</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
9
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
10
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">
11
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
12
+
13
+ <!-- Get patch fixes within a minor version -->
14
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/css/tabby-ui.min.css">
15
+ <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/js/tabby.polyfills.min.js"></script>
16
+
17
+ <%= stylesheet_link_tag "exception_hunter/application", media: "all" %>
18
+ </head>
19
+ <body style="background-color: #E5E5E5;">
20
+ <div class="container">
21
+ <%= yield %>
22
+ </div>
23
+ </body>
24
+ </html>
@@ -16,7 +16,7 @@ MoveFinderToNamedScopeCheck: { }
16
16
  MoveModelLogicIntoModelCheck: { use_count: 4 }
17
17
  NeedlessDeepNestingCheck: { nested_count: 2 }
18
18
  NotUseDefaultRouteCheck: { }
19
- NotUseTimeAgoInWordsCheck: { ignored_files: ['index.html.erb'] }
19
+ #NotUseTimeAgoInWordsCheck: { ignored_files: ['index.html.erb'] }
20
20
  OveruseRouteCustomizationsCheck: { customize_count: 3 }
21
21
  ProtectMassAssignmentCheck: { }
22
22
  RemoveEmptyHelpersCheck: { }
@@ -29,7 +29,7 @@ ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
29
29
  ReplaceInstanceVariableWithLocalVariableCheck: { }
30
30
  RestrictAutoGeneratedRoutesCheck: { }
31
31
  SimplifyRenderInControllersCheck: { }
32
- SimplifyRenderInViewsCheck: { }
32
+ #SimplifyRenderInViewsCheck: { }
33
33
  #UseBeforeFilterCheck: { customize_count: 2 }
34
34
  UseModelAssociationCheck: { }
35
35
  UseMultipartAlternativeAsContentTypeOfEmailCheck: { }
@@ -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,16 +1,26 @@
1
+ require 'pagy'
2
+
1
3
  require 'exception_hunter/engine'
2
4
  require 'exception_hunter/middleware/request_hunter'
3
5
  require 'exception_hunter/middleware/sidekiq_hunter' if defined?(Sidekiq)
4
6
  require 'exception_hunter/config'
7
+ require 'exception_hunter/error_creator'
8
+ require 'exception_hunter/error_reaper'
9
+ require 'exception_hunter/tracking'
5
10
  require 'exception_hunter/user_attributes_collector'
6
- require 'pagy'
7
11
 
8
12
  module ExceptionHunter
13
+ autoload :Devise, 'exception_hunter/devise'
14
+
15
+ extend ::ExceptionHunter::Tracking
16
+
9
17
  def self.setup(&block)
10
18
  block.call(Config)
11
19
  end
12
20
 
13
21
  def self.routes(router)
22
+ return unless Config.enabled
23
+
14
24
  router.mount(ExceptionHunter::Engine, at: 'exception_hunter')
15
25
  end
16
26
  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
@@ -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
@@ -30,6 +30,7 @@ module ExceptionHunter
30
30
  def catch_prey(env, exception)
31
31
  user = user_from_env(env)
32
32
  ErrorCreator.call(
33
+ tag: ErrorCreator::HTTP_TAG,
33
34
  class_name: exception.class.to_s,
34
35
  message: exception.message,
35
36
  environment_data: environment_data(env),
@@ -29,6 +29,7 @@ module ExceptionHunter
29
29
  return unless should_track?(context)
30
30
 
31
31
  ErrorCreator.call(
32
+ tag: ErrorCreator::WORKER_TAG,
32
33
  class_name: exception.class.to_s,
33
34
  message: exception.message,
34
35
  environment_data: environment_data(context),
@@ -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