exception_hunter 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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