exception_hunter 0.1.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 +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +22 -0
  5. data/app/assets/config/exception_hunter_manifest.js +3 -0
  6. data/app/assets/images/exception_hunter/logo.png +0 -0
  7. data/app/assets/stylesheets/exception_hunter/application.css +15 -0
  8. data/app/assets/stylesheets/exception_hunter/base.css +19 -0
  9. data/app/assets/stylesheets/exception_hunter/errors.css +84 -0
  10. data/app/assets/stylesheets/exception_hunter/navigation.css +21 -0
  11. data/app/controllers/exception_hunter/application_controller.rb +5 -0
  12. data/app/controllers/exception_hunter/errors_controller.rb +24 -0
  13. data/app/helpers/exception_hunter/application_helper.rb +5 -0
  14. data/app/jobs/exception_hunter/application_job.rb +4 -0
  15. data/app/mailers/exception_hunter/application_mailer.rb +6 -0
  16. data/app/models/exception_hunter/application_record.rb +5 -0
  17. data/app/models/exception_hunter/error.rb +26 -0
  18. data/app/models/exception_hunter/error_group.rb +29 -0
  19. data/app/presenters/exception_hunter/error_presenter.rb +34 -0
  20. data/app/services/exception_hunter/error_creator.rb +41 -0
  21. data/app/views/exception_hunter/errors/_error_backtrace.erb +20 -0
  22. data/app/views/exception_hunter/errors/_error_summary.erb +24 -0
  23. data/app/views/exception_hunter/errors/_error_user_data.erb +11 -0
  24. data/app/views/exception_hunter/errors/index.html.erb +36 -0
  25. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +17 -0
  26. data/app/views/exception_hunter/errors/show.html.erb +36 -0
  27. data/app/views/layouts/exception_hunter/application.html.erb +39 -0
  28. data/config/initializers/exception_hunter.rb +16 -0
  29. data/config/rails_best_practices.yml +42 -0
  30. data/config/routes.rb +3 -0
  31. data/lib/exception_hunter.rb +15 -0
  32. data/lib/exception_hunter/config.rb +5 -0
  33. data/lib/exception_hunter/engine.rb +16 -0
  34. data/lib/exception_hunter/railtie.rb +11 -0
  35. data/lib/exception_hunter/request_hunter.rb +41 -0
  36. data/lib/exception_hunter/user_attributes_collector.rb +15 -0
  37. data/lib/exception_hunter/version.rb +3 -0
  38. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +28 -0
  39. data/lib/generators/exception_hunter/install/USAGE +6 -0
  40. data/lib/generators/exception_hunter/install/install_generator.rb +30 -0
  41. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +14 -0
  42. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_errors.rb.erb +19 -0
  43. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +18 -0
  44. data/lib/tasks/code_analysis.rake +7 -0
  45. data/lib/tasks/exception_hunter_tasks.rake +4 -0
  46. metadata +200 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11f6f14e231b447819eaa8d2f6b107a44ef3f7060e3485381efd5ba3206d8a01
4
+ data.tar.gz: 26d9b864d2a728317009f1c097e97a849660667e678386cfec6f79cb46b9373a
5
+ SHA512:
6
+ metadata.gz: 51ced2ec4f0fd66cdc29b71ec0b8214966742848ee926cfd70aeeeee19b9b8f94e4fd03b5193676dcb27d38d2634e75fc13be070c27f6efec82a8271445fd072
7
+ data.tar.gz: 8b274f7de5f265208f3e142b33ef176f62e9586425628f6b9af1e8fae76493ac5f0b0c07aa43b4ef65355319185065c4123973adacf8f5da6b3aab671f5d92ec
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Bruno Vezoli
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # ExceptionHunter
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'exception_hunter'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install exception_hunter
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ExceptionHunter'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ load 'lib/tasks/code_analysis.rake'
@@ -0,0 +1,3 @@
1
+ //= link_directory ../stylesheets/exception_hunter .css
2
+ //= link_directory ../images/exception_hunter
3
+
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,19 @@
1
+ :root {
2
+ --main-color: #C8193C;
3
+ --secondary-color: #F4F5F6;
4
+ --link-color: #158FEF;
5
+ --border-color: #D1D1D1;
6
+ --file-name-color: #16AF90
7
+ }
8
+
9
+ .wrapper {
10
+ margin: 5.5rem auto auto;
11
+ }
12
+
13
+ a {
14
+ color: var(--link-color);
15
+ }
16
+
17
+ a:hover, a:focus, a:active {
18
+ color: var(--main-color);
19
+ }
@@ -0,0 +1,84 @@
1
+ /*
2
+ Place all the styles related to the matching controller here.
3
+ They will automatically be included in application.css.
4
+ */
5
+
6
+ .row.statistics-row {
7
+ padding-top: 1rem;
8
+ }
9
+
10
+ .statistics__cell {
11
+ color: var(--main-color);
12
+ background-color: var(--secondary-color);
13
+ border: 1px solid var(--border-color);
14
+ height: 5rem;
15
+ border-radius: 10px;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ font-size: 2.2rem;
20
+ }
21
+
22
+ .row.error-row {
23
+ padding-top: 1rem;
24
+ padding-bottom: 1rem;
25
+ border-bottom: 1px solid;
26
+ }
27
+
28
+ .row.error-row-no-border {
29
+ padding-top: 1rem;
30
+ padding-bottom: 1rem;
31
+ }
32
+
33
+ .error-row.error-row--header {
34
+ font-weight: bold;
35
+ }
36
+
37
+ .error-cell.error-cell--highlight {
38
+ color: var(--main-color);
39
+ }
40
+
41
+ .error-title {
42
+ font-size: 20px;
43
+ border-bottom: 1px solid var(--border-color);
44
+ }
45
+
46
+ .error-occurred_at {
47
+ color: var(--main-color);
48
+ font-size: 14px;
49
+ margin-top: 0.5em;
50
+ margin-bottom: 2em;
51
+ }
52
+
53
+ .tab-content {
54
+ padding: 1em 0.5em;
55
+ }
56
+
57
+ .data-title {
58
+ font-weight: bold;
59
+ color: var(--main-color);
60
+ }
61
+
62
+ .backtrace {
63
+ padding-top: 1em;
64
+ padding-bottom: 1em;
65
+ overflow-x: scroll;
66
+ }
67
+
68
+ .backtrace-line {
69
+ white-space: nowrap;
70
+ margin-top: .3em;
71
+ margin-bottom: .3em;
72
+ display: flex;
73
+ font-family: "Courier New", Courier, monospace;
74
+ font-size: 14px;
75
+ }
76
+
77
+ .backtrace-line__line-number {
78
+ margin-right: 5px;
79
+ color: var(--main-color);
80
+ }
81
+
82
+ .backtrace-line__file-name {
83
+ color: var(--file-name-color);
84
+ }
@@ -0,0 +1,21 @@
1
+ .nav {
2
+ background: var(--secondary-color);
3
+ border-bottom: .1rem solid var(--border-color);
4
+ display: block;
5
+ height: 5.2rem;
6
+ left: 0;
7
+ max-width: 100%;
8
+ position: fixed;
9
+ right: 0;
10
+ top: 0;
11
+ width: 100%;
12
+ z-index: 1;
13
+ }
14
+ .container__nav {
15
+ display: flex;
16
+ height: 100%;
17
+ }
18
+
19
+ .nav__logo img {
20
+ height: 100%;
21
+ }
@@ -0,0 +1,5 @@
1
+ module ExceptionHunter
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ require_dependency 'exception_hunter/application_controller'
2
+
3
+ module ExceptionHunter
4
+ class ErrorsController < ApplicationController
5
+ include Pagy::Backend
6
+
7
+ def index
8
+ @errors = ErrorGroup.all.order(created_at: :desc)
9
+ @errors_count = Error.count
10
+ @month_errors = Error.in_current_month.count
11
+ end
12
+
13
+ def show
14
+ @pagy, errors = pagy(most_recent_errors, items: 1)
15
+ @error = ErrorPresenter.new(errors.first)
16
+ end
17
+
18
+ private
19
+
20
+ def most_recent_errors
21
+ Error.most_recent(params[:id])
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module ExceptionHunter
2
+ module ApplicationHelper
3
+ include Pagy::Frontend
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module ExceptionHunter
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module ExceptionHunter
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module ExceptionHunter
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ module ExceptionHunter
2
+ class Error < ApplicationRecord
3
+ validates :class_name, presence: true
4
+ validates :occurred_at, presence: true
5
+
6
+ belongs_to :error_group
7
+
8
+ before_validation :set_occurred_at, on: :create
9
+
10
+ scope :most_recent, lambda { |error_group_id|
11
+ where(error_group_id: error_group_id).order(occurred_at: :desc)
12
+ }
13
+
14
+ def self.in_current_month
15
+ current_month = Date.today.beginning_of_month..Date.today.end_of_month
16
+
17
+ where(occurred_at: current_month)
18
+ end
19
+
20
+ private
21
+
22
+ def set_occurred_at
23
+ self.occurred_at ||= Time.now
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ module ExceptionHunter
2
+ class ErrorGroup < ApplicationRecord
3
+ SIMILARITY_THRESHOLD = 0.75
4
+
5
+ validates :error_class_name, presence: true
6
+
7
+ has_many :grouped_errors, class_name: 'ExceptionHunter::Error'
8
+
9
+ scope :most_similar, lambda { |message|
10
+ quoted_message = ActiveRecord::Base.connection.quote_string(message)
11
+ where("similarity(exception_hunter_error_groups.message, :message) >= #{SIMILARITY_THRESHOLD}", message: message)
12
+ .order(Arel.sql("similarity(exception_hunter_error_groups.message, '#{quoted_message}') DESC"))
13
+ }
14
+
15
+ def self.find_matching_group(error)
16
+ where(error_class_name: error.class_name)
17
+ .most_similar(error.message.to_s)
18
+ .first
19
+ end
20
+
21
+ def last_occurrence
22
+ @last_occurrence ||= grouped_errors.maximum(:occurred_at)
23
+ end
24
+
25
+ def total_occurrences
26
+ @total_occurrences ||= grouped_errors.count
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ module ExceptionHunter
2
+ class ErrorPresenter
3
+ delegate_missing_to :error
4
+
5
+ BacktraceLine = Struct.new(:path, :file_name, :line_number, :method_call)
6
+
7
+ def initialize(error)
8
+ @error = error
9
+ end
10
+
11
+ def backtrace
12
+ error.backtrace.map do |line|
13
+ format_backtrace_line(line)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :error
20
+
21
+ def format_backtrace_line(line)
22
+ matches = line.match(%r{(?<path>.*)/(?<file_name>[^:]*):(?<line_number>\d*).*`(?<method_call>.*)'})
23
+
24
+ if matches.nil?
25
+ line
26
+ else
27
+ BacktraceLine.new(matches[:path],
28
+ matches[:file_name],
29
+ matches[:line_number],
30
+ matches[:method_call])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module ExceptionHunter
2
+ class ErrorCreator
3
+ class << self
4
+ def call(**error_attrs)
5
+ ActiveRecord::Base.transaction do
6
+ 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)
10
+ error.error_group = error_group
11
+ error.save!
12
+ error
13
+ end
14
+ rescue ActiveRecord::RecordInvalid
15
+ false
16
+ end
17
+
18
+ private
19
+
20
+ def update_error_group(error_group, error)
21
+ error_group.error_class_name = error.class_name
22
+ error_group.message = error.message
23
+
24
+ error_group.save!
25
+ end
26
+
27
+ def extract_user_data(**error_attrs)
28
+ user = error_attrs[:user]
29
+ error_attrs[:user_data] =
30
+ if user.nil?
31
+ {}
32
+ else
33
+ UserAttributesCollector.collect_attributes(user)
34
+ end
35
+
36
+ error_attrs.delete(:user)
37
+ error_attrs
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,20 @@
1
+ <% if error.backtrace.empty? %>
2
+ Unfortunately, no backtrace has been registered for this error.
3
+ <% else %>
4
+ <div class="backtrace">
5
+ <% error.backtrace.each do |line| %>
6
+ <div class="backtrace-line">
7
+ <% if line.is_a?(String) %>
8
+ <%= line %>
9
+ <% else %>
10
+ <div class="backtrace-line__path"><%= line.path %></div>
11
+ /
12
+ <div class="backtrace-line__file-name"><%= line.file_name %></div>
13
+ :
14
+ <div class="backtrace-line__line-number"><%= line.line_number %></div>
15
+ <div class="backtrace-line__method-call"><%= line.method_call %></div>
16
+ <% end %>
17
+ </div>
18
+ <% end %>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <% if error.environment_data.empty? %>
2
+ <div class="row error-row-no-border data-title">
3
+ Unfortunately, no environment information has been registered for this error.
4
+ </div>
5
+ <% else %>
6
+ <div class="row error-row-no-border data-title">
7
+ Environment Data
8
+ </div>
9
+ <% error.environment_data.each do |key, value| %>
10
+ <b><%= key %></b>: <%= value %><br>
11
+ <% end %>
12
+ <% end %>
13
+ <% if error.custom_data.nil? %>
14
+ <div class="row error-row-no-border data-title">
15
+ No custom data included.
16
+ </div>
17
+ <% else %>
18
+ <div class="row error-row-no-border data-title">
19
+ Custom Data
20
+ </div>
21
+ <% error.custom_data.each do |key, value| %>
22
+ <b><%= key %></b>: <%= value || 'None' %> <br>
23
+ <% end %>
24
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <% if error.user_data.empty? %>
2
+ <div class="data-title">
3
+ Unfortunately, no user information has been registered for this error.
4
+ </div>
5
+ <% else %>
6
+ <% error.user_data.transform_keys(&:to_sym).each do |key, value| %>
7
+ <div class="column column-15">
8
+ <b><%= key %></b>: <%= value %>
9
+ </div>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <div class="row statistics-row">
2
+ <div class="column column-offset-50 column-25">
3
+ <div class="statistics__cell">
4
+ <%= number_with_delimiter(@errors_count) %> Total errors
5
+ </div>
6
+ </div>
7
+ <div class="column column-offset-50 column-25">
8
+ <div class="statistics__cell">
9
+ <%= number_with_delimiter(@month_errors) %> Errors this month
10
+ </div>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="row error-row error-row--header">
15
+ <div class="column column-75">Message</div>
16
+ <div class="column column-15">Last Occurrence</div>
17
+ <div class="column column-10">Occurrences</div>
18
+ </div>
19
+
20
+ <% @errors.each do |error| %>
21
+ <div class="row error-row">
22
+ <div class="column column-75 error-cell">
23
+ <%= link_to error.message, error_path(error.id) %>
24
+ </div>
25
+ <div class="column column-15 error-cell error-cell--highlight">
26
+ <% if error.last_occurrence.present? %>
27
+ <%= time_ago_in_words(error.last_occurrence) %> ago
28
+ <% else %>
29
+ Never
30
+ <% end %>
31
+ </div>
32
+ <div class="column column-10 error-cell error-cell--highlight">
33
+ <%= error.total_occurrences %>
34
+ </div>
35
+ </div>
36
+ <% end %>
@@ -0,0 +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>
9
+ <% 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>
16
+ <% end %>
17
+ </nav>
@@ -0,0 +1,36 @@
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 } %>
4
+ </div>
5
+ </div>
6
+
7
+ <div class="error-title">
8
+ <%= @error.class_name %>: <%= @error.message %>
9
+ </div>
10
+ <div class="error-occurred_at">
11
+ <%= @error.occurred_at %>
12
+ </div>
13
+
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
+
20
+ <div class="tab-content">
21
+ <div id="summary">
22
+ <%= render partial: 'exception_hunter/errors/error_summary', locals: { error: @error } %>
23
+ </div>
24
+
25
+ <div id="backtrace">
26
+ <%= render partial: 'exception_hunter/errors/error_backtrace', locals: { error: @error } %>
27
+ </div>
28
+
29
+ <div id="user-data">
30
+ <%= render partial: 'exception_hunter/errors/error_user_data', locals: { error: @error } %>
31
+ </div>
32
+ </div>
33
+
34
+ <script type="text/javascript" charset="utf-8">
35
+ const tabs = new Tabby('[data-tabs]')
36
+ </script>
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Exception Hunter</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= favicon_link_tag 'exception_hunter/logo.png' %>
9
+
10
+ <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
11
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
12
+ <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css">
13
+
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
+ <script src="https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0/dist/js/tabby.polyfills.min.js"></script>
17
+
18
+ <%= stylesheet_link_tag "exception_hunter/application", media: "all" %>
19
+ </head>
20
+ <body>
21
+
22
+ <div class="wrapper">
23
+ <nav class="nav">
24
+ <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 %>
29
+ </div>
30
+ </div>
31
+ </nav>
32
+
33
+ <div class="container">
34
+ <%= yield %>
35
+ </div>
36
+ </div>
37
+
38
+ </body>
39
+ </html>
@@ -0,0 +1,16 @@
1
+ ExceptionHunter.setup do |config|
2
+ # == Current User
3
+ #
4
+ # Exception Hunter will include the user as part of the environment
5
+ # data, if it was to be available. The default configuration uses devise
6
+ # :current_user method. You can change it in case
7
+ #
8
+ config.current_user_method = :current_user
9
+
10
+ # == Current User Attributes
11
+ #
12
+ # Exception Hunter will try to include the attributes defined here
13
+ # as part of the user information that is kept from the request.
14
+ #
15
+ config.user_attributes = [:id, :email]
16
+ end
@@ -0,0 +1,42 @@
1
+ AddModelVirtualAttributeCheck: { }
2
+ AlwaysAddDbIndexCheck: { }
3
+ #CheckSaveReturnValueCheck: { }
4
+ #CheckDestroyReturnValueCheck: { }
5
+ DefaultScopeIsEvilCheck: { }
6
+ DryBundlerInCapistranoCheck: { }
7
+ #HashSyntaxCheck: { }
8
+ IsolateSeedDataCheck: { }
9
+ KeepFindersOnTheirOwnModelCheck: { }
10
+ LawOfDemeterCheck: { }
11
+ #LongLineCheck: { max_line_length: 80 }
12
+ MoveCodeIntoControllerCheck: { }
13
+ MoveCodeIntoHelperCheck: { array_count: 3 }
14
+ MoveCodeIntoModelCheck: { use_count: 2 }
15
+ MoveFinderToNamedScopeCheck: { }
16
+ MoveModelLogicIntoModelCheck: { use_count: 4 }
17
+ NeedlessDeepNestingCheck: { nested_count: 2 }
18
+ NotRescueExceptionCheck: { ignored_files: 'request_hunter.rb' }
19
+ NotUseDefaultRouteCheck: { }
20
+ NotUseTimeAgoInWordsCheck: { ignored_files: ['index.html.erb'] }
21
+ OveruseRouteCustomizationsCheck: { customize_count: 3 }
22
+ ProtectMassAssignmentCheck: { }
23
+ RemoveEmptyHelpersCheck: { }
24
+ #RemoveTabCheck: { }
25
+ RemoveTrailingWhitespaceCheck: { }
26
+ RemoveUnusedMethodsInControllersCheck: { except_methods: [] }
27
+ RemoveUnusedMethodsInHelpersCheck: { except_methods: [] }
28
+ RemoveUnusedMethodsInModelsCheck: { except_methods: [] }
29
+ ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 }
30
+ ReplaceInstanceVariableWithLocalVariableCheck: { }
31
+ RestrictAutoGeneratedRoutesCheck: { }
32
+ SimplifyRenderInControllersCheck: { }
33
+ SimplifyRenderInViewsCheck: { }
34
+ #UseBeforeFilterCheck: { customize_count: 2 }
35
+ UseModelAssociationCheck: { }
36
+ UseMultipartAlternativeAsContentTypeOfEmailCheck: { }
37
+ #UseParenthesesInMethodDefCheck: { }
38
+ UseObserverCheck: { }
39
+ UseQueryAttributeCheck: { }
40
+ UseSayWithTimeInMigrationsCheck: { }
41
+ UseScopeAccessCheck: { }
42
+ UseTurboSprocketsRails3Check: { }
@@ -0,0 +1,3 @@
1
+ ExceptionHunter::Engine.routes.draw do
2
+ resources :errors, only: %i[index show]
3
+ end
@@ -0,0 +1,15 @@
1
+ require 'exception_hunter/engine'
2
+ require 'exception_hunter/railtie'
3
+ require 'exception_hunter/config'
4
+ require 'exception_hunter/user_attributes_collector'
5
+ require 'pagy'
6
+
7
+ module ExceptionHunter
8
+ def self.setup(&block)
9
+ block.call(Config)
10
+ end
11
+
12
+ def self.routes(router)
13
+ router.mount(ExceptionHunter::Engine, at: 'exception_hunter')
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module ExceptionHunter
2
+ class Config
3
+ cattr_accessor :current_user_method, :user_attributes
4
+ end
5
+ end
@@ -0,0 +1,16 @@
1
+ module ExceptionHunter
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ExceptionHunter
4
+
5
+ config.generators do |gen|
6
+ gen.test_framework :rspec
7
+ gen.fixture_replacement :factory_bot
8
+ gen.factory_bot dir: 'spec/factories'
9
+ end
10
+
11
+ initializer 'exception_hunter.precompile', group: :all do |app|
12
+ app.config.assets.precompile << 'exception_hunter/application.css'
13
+ app.config.assets.precompile << 'exception_hunter/logo.png'
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ require_dependency 'exception_hunter/request_hunter'
2
+
3
+ module ExceptionHunter
4
+ class Railtie < Rails::Railtie
5
+ initializer 'exception_hunter.add_middleware', after: :load_config_initializers do |app|
6
+ app.config.middleware.insert_after(
7
+ ActionDispatch::DebugExceptions, ExceptionHunter::RequestHunter
8
+ )
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ module ExceptionHunter
2
+ class RequestHunter
3
+ ENVIRONMENT_KEYS =
4
+ %w[PATH_INFO QUERY_STRING REMOTE_HOST REQUEST_METHOD REQUEST_URI
5
+ SERVER_PROTOCOL HTTP_HOST HTTP_USER_AGENT].freeze
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ @app.call(env)
13
+ rescue Exception => exception
14
+ catch_prey(env, exception)
15
+ raise exception
16
+ end
17
+
18
+ private
19
+
20
+ def catch_prey(env, exception)
21
+ user = user_from_env(env)
22
+ ErrorCreator.call(
23
+ class_name: exception.class.to_s,
24
+ message: exception.message,
25
+ environment_data: environment_data(env),
26
+ backtrace: exception.backtrace,
27
+ user: user
28
+ )
29
+ end
30
+
31
+ def environment_data(env)
32
+ env.select { |key, _value| ENVIRONMENT_KEYS.include?(key) }
33
+ end
34
+
35
+ def user_from_env(env)
36
+ current_user_method = Config.current_user_method
37
+ controller = env['action_controller.instance']
38
+ controller.try(current_user_method)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module ExceptionHunter
2
+ module UserAttributesCollector
3
+ extend self
4
+
5
+ def collect_attributes(user)
6
+ attributes.reduce({}) do |data, attribute|
7
+ data.merge(attribute => user.try(attribute))
8
+ end
9
+ end
10
+
11
+ def attributes
12
+ Config.user_attributes
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module ExceptionHunter
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,28 @@
1
+ module ExceptionHunter
2
+ class CreateUsersGenerator < Rails::Generators::NamedBase
3
+ argument :name, type: :string, default: 'AdminUser'
4
+
5
+ def install_devise
6
+ begin
7
+ require 'devise'
8
+ rescue LoadError
9
+ log :error, 'Please install devise and require add it to your gemfile or run with --skip-users'
10
+ exit(false)
11
+ end
12
+
13
+ initializer_file =
14
+ File.join(destination_root, 'config', 'initializers', 'devise.rb')
15
+
16
+ if File.exist?(initializer_file)
17
+ log :generate, 'No need to install devise, already done.'
18
+ else
19
+ log :generate, 'devise:install'
20
+ invoke 'devise:install'
21
+ end
22
+ end
23
+
24
+ def create_admin_user
25
+ invoke 'devise', [name]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Generates an initializer and the needed setup to use the ExceptionHunter gem. It also
3
+ creates a user for developers to access the dashboard by invoking devise.
4
+
5
+ Example:
6
+ rails generate exception_hunter:install SuperUser
@@ -0,0 +1,30 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ExceptionHunter
4
+ class InstallGenerator < ActiveRecord::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ argument :name, type: :string, default: 'AdminUser'
8
+ hook_for :users, default: 'create_users', desc: 'Admin user generator to run. Skip with --skip-users'
9
+
10
+ def copy_initializer
11
+ @underscored_user_name = name.underscore.gsub('/', '_')
12
+ @use_authentication_method = options[:users].present?
13
+ template 'exception_hunter.rb.erb', 'config/initializers/exception_hunter.rb'
14
+ end
15
+
16
+ def setup_routes
17
+ if options[:users]
18
+ inject_into_file 'config/routes.rb', "\n ExceptionHunter.routes(self)", after: /devise_for .*/
19
+ else
20
+ route 'ExceptionHunter.routes(self)'
21
+ end
22
+ end
23
+
24
+ def create_migrations
25
+ migration_template 'create_exception_hunter_error_groups.rb.erb',
26
+ 'db/migrate/create_exception_hunter_error_groups.rb'
27
+ migration_template 'create_exception_hunter_errors.rb.erb', 'db/migrate/create_exception_hunter_errors.rb'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ class CreateExceptionHunterErrorGroups < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
2
+ def change
3
+ enable_extension :pg_trgm
4
+
5
+ create_table :exception_hunter_error_groups do |t|
6
+ t.string :error_class_name, null: false
7
+ t.string :message
8
+
9
+ t.timestamps
10
+
11
+ t.index :message, opclass: :gin_trgm_ops, using: :gin
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ class CreateExceptionHunterErrors < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
2
+ def change
3
+ create_table :exception_hunter_errors do |t|
4
+ t.string :class_name, null: false
5
+ t.string :message
6
+ t.timestamp :occurred_at, null: false
7
+ t.json :environment_data
8
+ t.json :custom_data
9
+ t.json :user_data
10
+ t.string :backtrace, array: true, default: []
11
+
12
+ t.belongs_to :error_group,
13
+ index: true,
14
+ foreign_key: { to_table: :exception_hunter_error_groups }
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ ExceptionHunter.setup do |config|
2
+ # == Current User
3
+ #
4
+ # Exception Hunter will include the user as part of the environment
5
+ # data, if it was to be available. The default configuration uses devise
6
+ # :current_user method. You can change it in case you named your user model
7
+ # in some other way (i.e. Member). You can also remove the configuration if
8
+ # you don't wish to track user data.
9
+ #
10
+ config.current_user_method = :current_user
11
+
12
+ # == Current User Attributes
13
+ #
14
+ # Exception Hunter will try to include the attributes defined here
15
+ # as part of the user information that is kept from the request.
16
+ #
17
+ config.user_attributes = [:id, :email]
18
+ end
@@ -0,0 +1,7 @@
1
+ desc 'code analysis'
2
+ task :code_analysis do
3
+ sh 'bundle exec brakeman . -z -q'
4
+ sh 'bundle exec rubocop app config lib spec'
5
+ sh 'bundle exec reek app config lib'
6
+ sh 'bundle exec rails_best_practices .'
7
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :exception_hunter do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exception_hunter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bruno Vezoli
8
+ - Tiziana Romani
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-05-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pagy
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.8'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.8'
28
+ - !ruby/object:Gem::Dependency
29
+ name: brakeman
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '4.8'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '4.8'
42
+ - !ruby/object:Gem::Dependency
43
+ name: factory_bot_rails
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pg
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rails_best_practices
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '1.20'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.20'
84
+ - !ruby/object:Gem::Dependency
85
+ name: reek
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '5.6'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '5.6'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rubocop
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: 0.80.1
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: 0.80.1
112
+ - !ruby/object:Gem::Dependency
113
+ name: simplecov
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 0.18.5
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: 0.18.5
126
+ description:
127
+ email:
128
+ - bruno.vezoli@rootstrap.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - MIT-LICENSE
134
+ - README.md
135
+ - Rakefile
136
+ - app/assets/config/exception_hunter_manifest.js
137
+ - app/assets/images/exception_hunter/logo.png
138
+ - app/assets/stylesheets/exception_hunter/application.css
139
+ - app/assets/stylesheets/exception_hunter/base.css
140
+ - app/assets/stylesheets/exception_hunter/errors.css
141
+ - app/assets/stylesheets/exception_hunter/navigation.css
142
+ - app/controllers/exception_hunter/application_controller.rb
143
+ - app/controllers/exception_hunter/errors_controller.rb
144
+ - app/helpers/exception_hunter/application_helper.rb
145
+ - app/jobs/exception_hunter/application_job.rb
146
+ - app/mailers/exception_hunter/application_mailer.rb
147
+ - app/models/exception_hunter/application_record.rb
148
+ - app/models/exception_hunter/error.rb
149
+ - app/models/exception_hunter/error_group.rb
150
+ - app/presenters/exception_hunter/error_presenter.rb
151
+ - app/services/exception_hunter/error_creator.rb
152
+ - app/views/exception_hunter/errors/_error_backtrace.erb
153
+ - app/views/exception_hunter/errors/_error_summary.erb
154
+ - app/views/exception_hunter/errors/_error_user_data.erb
155
+ - app/views/exception_hunter/errors/index.html.erb
156
+ - app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb
157
+ - app/views/exception_hunter/errors/show.html.erb
158
+ - app/views/layouts/exception_hunter/application.html.erb
159
+ - config/initializers/exception_hunter.rb
160
+ - config/rails_best_practices.yml
161
+ - config/routes.rb
162
+ - lib/exception_hunter.rb
163
+ - lib/exception_hunter/config.rb
164
+ - lib/exception_hunter/engine.rb
165
+ - lib/exception_hunter/railtie.rb
166
+ - lib/exception_hunter/request_hunter.rb
167
+ - lib/exception_hunter/user_attributes_collector.rb
168
+ - lib/exception_hunter/version.rb
169
+ - lib/generators/exception_hunter/create_users/create_users_generator.rb
170
+ - lib/generators/exception_hunter/install/USAGE
171
+ - lib/generators/exception_hunter/install/install_generator.rb
172
+ - lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb
173
+ - lib/generators/exception_hunter/install/templates/create_exception_hunter_errors.rb.erb
174
+ - lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb
175
+ - lib/tasks/code_analysis.rake
176
+ - lib/tasks/exception_hunter_tasks.rake
177
+ homepage: https://github.com/rootstrap/exception_hunter
178
+ licenses:
179
+ - MIT
180
+ metadata: {}
181
+ post_install_message:
182
+ rdoc_options: []
183
+ require_paths:
184
+ - lib
185
+ required_ruby_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: 2.5.5
190
+ required_rubygems_version: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ requirements: []
196
+ rubygems_version: 3.0.8
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Exception tracking engine
200
+ test_files: []