exception_hunter 0.1.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 +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: []