exception_hunter 0.1.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -7
  3. data/app/assets/stylesheets/exception_hunter/base.css +62 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +166 -24
  5. data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
  6. data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
  7. data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
  8. data/app/controllers/exception_hunter/application_controller.rb +2 -0
  9. data/app/controllers/exception_hunter/errors_controller.rb +27 -4
  10. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  11. data/app/helpers/exception_hunter/errors_helper.rb +7 -0
  12. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  13. data/app/models/exception_hunter/application_record.rb +8 -0
  14. data/app/models/exception_hunter/error.rb +21 -8
  15. data/app/models/exception_hunter/error_group.rb +24 -5
  16. data/app/presenters/exception_hunter/dashboard_presenter.rb +54 -0
  17. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  18. data/app/presenters/exception_hunter/error_presenter.rb +10 -1
  19. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  20. data/app/views/exception_hunter/errors/_error_row.erb +44 -0
  21. data/app/views/exception_hunter/errors/_error_summary.erb +23 -10
  22. data/app/views/exception_hunter/errors/_error_user_data.erb +4 -5
  23. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  24. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  25. data/app/views/exception_hunter/errors/index.html.erb +71 -30
  26. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  27. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  28. data/app/views/layouts/exception_hunter/application.html.erb +65 -6
  29. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  30. data/config/rails_best_practices.yml +2 -3
  31. data/config/routes.rb +19 -1
  32. data/lib/exception_hunter.rb +12 -2
  33. data/lib/exception_hunter/config.rb +8 -1
  34. data/lib/exception_hunter/devise.rb +17 -0
  35. data/lib/exception_hunter/engine.rb +5 -0
  36. data/{app/services → lib}/exception_hunter/error_creator.rb +17 -5
  37. data/lib/exception_hunter/error_reaper.rb +12 -0
  38. data/lib/exception_hunter/middleware/delayed_job_hunter.rb +69 -0
  39. data/lib/exception_hunter/middleware/request_hunter.rb +71 -0
  40. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +59 -0
  41. data/lib/exception_hunter/tracking.rb +17 -0
  42. data/lib/exception_hunter/user_attributes_collector.rb +4 -0
  43. data/lib/exception_hunter/version.rb +1 -1
  44. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  45. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  46. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  47. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +23 -0
  48. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  49. metadata +25 -10
  50. data/config/initializers/exception_hunter.rb +0 -16
  51. data/lib/exception_hunter/railtie.rb +0 -11
  52. data/lib/exception_hunter/request_hunter.rb +0 -41
@@ -0,0 +1,23 @@
1
+ module ExceptionHunter
2
+ module Authorization
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action :authenticate_admin_user_class
7
+ end
8
+
9
+ def authenticate_admin_user_class
10
+ return unless ExceptionHunter::Config.auth_enabled? && !send("current_#{underscored_admin_user_class}")
11
+
12
+ redirect_to '/exception_hunter/login'
13
+ end
14
+
15
+ def redirect_to_login
16
+ render 'exception_hunter/devise/sessions/new'
17
+ end
18
+
19
+ def underscored_admin_user_class
20
+ ExceptionHunter::Config.admin_user_class.underscore
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,7 @@
1
1
  module ExceptionHunter
2
2
  class ApplicationController < ActionController::Base
3
+ include ExceptionHunter::Authorization
4
+
3
5
  protect_from_forgery with: :exception
4
6
  end
5
7
  end
@@ -5,14 +5,20 @@ module ExceptionHunter
5
5
  include Pagy::Backend
6
6
 
7
7
  def index
8
- @errors = ErrorGroup.all.order(created_at: :desc)
9
- @errors_count = Error.count
10
- @month_errors = Error.in_current_month.count
8
+ @dashboard = DashboardPresenter.new(current_tab)
9
+ shown_errors = errors_for_tab(@dashboard).order(updated_at: :desc).distinct
10
+ @errors = ErrorGroupPresenter.wrap_collection(shown_errors)
11
11
  end
12
12
 
13
13
  def show
14
14
  @pagy, errors = pagy(most_recent_errors, items: 1)
15
- @error = ErrorPresenter.new(errors.first)
15
+ @error = ErrorPresenter.new(errors.first!)
16
+ end
17
+
18
+ def destroy
19
+ ErrorReaper.purge
20
+
21
+ redirect_back fallback_location: errors_path, notice: 'Errors purged successfully'
16
22
  end
17
23
 
18
24
  private
@@ -20,5 +26,22 @@ module ExceptionHunter
20
26
  def most_recent_errors
21
27
  Error.most_recent(params[:id])
22
28
  end
29
+
30
+ def current_tab
31
+ params[:tab]
32
+ end
33
+
34
+ def errors_for_tab(dashboard)
35
+ case dashboard.current_tab
36
+ when DashboardPresenter::LAST_7_DAYS_TAB
37
+ ErrorGroup.with_errors_in_last_7_days.active
38
+ when DashboardPresenter::CURRENT_MONTH_TAB
39
+ ErrorGroup.with_errors_in_current_month.active
40
+ when DashboardPresenter::TOTAL_ERRORS_TAB
41
+ ErrorGroup.active
42
+ when DashboardPresenter::RESOLVED_ERRORS_TAB
43
+ ErrorGroup.resolved
44
+ end
45
+ end
23
46
  end
24
47
  end
@@ -0,0 +1,11 @@
1
+ require_dependency 'exception_hunter/application_controller'
2
+
3
+ module ExceptionHunter
4
+ class ResolvedErrorsController < ApplicationController
5
+ def create
6
+ ErrorGroup.find(params[:error_group][:id]).resolved!
7
+
8
+ redirect_to errors_path, notice: 'Error resolved successfully'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ module ExceptionHunter
2
+ module ErrorsHelper
3
+ def format_tracked_data(tracked_data)
4
+ JSON.pretty_generate(tracked_data)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module ExceptionHunter
2
+ module SessionsHelper
3
+ def current_admin_user?
4
+ underscored_admin_user_class &&
5
+ current_admin_class_name(underscored_admin_user_class)
6
+ end
7
+
8
+ def underscored_admin_user_class
9
+ ExceptionHunter::Config.admin_user_class.try(:underscore)
10
+ end
11
+
12
+ def current_admin_class_name(class_name)
13
+ send("current_#{class_name.underscore}")
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,13 @@
1
1
  module ExceptionHunter
2
2
  class ApplicationRecord < ActiveRecord::Base
3
3
  self.abstract_class = true
4
+
5
+ class << self
6
+ delegate :[], to: :arel_table
7
+
8
+ def sql_similarity(attr, value)
9
+ Arel::Nodes::NamedFunction.new('similarity', [attr, Arel::Nodes.build_quoted(value)])
10
+ end
11
+ end
4
12
  end
5
13
  end
@@ -1,26 +1,39 @@
1
1
  module ExceptionHunter
2
- class Error < ApplicationRecord
2
+ class Error < ::ExceptionHunter::ApplicationRecord
3
3
  validates :class_name, presence: true
4
4
  validates :occurred_at, presence: true
5
5
 
6
- belongs_to :error_group
6
+ belongs_to :error_group, touch: true
7
7
 
8
8
  before_validation :set_occurred_at, on: :create
9
+ after_create :unresolve_error_group, unless: -> { error_group.active? }
9
10
 
10
11
  scope :most_recent, lambda { |error_group_id|
11
12
  where(error_group_id: error_group_id).order(occurred_at: :desc)
12
13
  }
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
14
+ scope :with_occurrences_before, lambda { |max_occurrence_date|
15
+ where(Error[:occurred_at].lteq(max_occurrence_date))
16
+ }
17
+ scope :in_period, ->(period) { where(occurred_at: period) }
18
+ scope :in_last_7_days, -> { in_period(7.days.ago.beginning_of_day..Time.now) }
19
+ scope :in_current_month, lambda {
20
+ in_period(Date.current.beginning_of_month.beginning_of_day..Date.current.end_of_month.end_of_day)
21
+ }
22
+ scope :from_active_error_groups, lambda {
23
+ joins(:error_group).where(error_group: ErrorGroup.active)
24
+ }
25
+ scope :from_resolved_error_groups, lambda {
26
+ joins(:error_group).where(error_group: ErrorGroup.resolved)
27
+ }
19
28
 
20
29
  private
21
30
 
22
31
  def set_occurred_at
23
32
  self.occurred_at ||= Time.now
24
33
  end
34
+
35
+ def unresolve_error_group
36
+ error_group.active!
37
+ end
25
38
  end
26
39
  end
@@ -1,15 +1,30 @@
1
1
  module ExceptionHunter
2
- class ErrorGroup < ApplicationRecord
2
+ class ErrorGroup < ::ExceptionHunter::ApplicationRecord
3
3
  SIMILARITY_THRESHOLD = 0.75
4
4
 
5
5
  validates :error_class_name, presence: true
6
6
 
7
- has_many :grouped_errors, class_name: 'ExceptionHunter::Error'
7
+ has_many :grouped_errors, class_name: 'ExceptionHunter::Error', dependent: :destroy
8
+
9
+ enum status: { active: 0, resolved: 1 }
8
10
 
9
11
  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"))
12
+ message_similarity = sql_similarity(ErrorGroup[:message], message)
13
+ where(message_similarity.gteq(SIMILARITY_THRESHOLD))
14
+ .order(message_similarity.desc)
15
+ }
16
+
17
+ scope :without_errors, lambda {
18
+ is_associated_error = Error[:error_group_id].eq(ErrorGroup[:id])
19
+ where.not(Error.where(is_associated_error).arel.exists)
20
+ }
21
+ scope :with_errors_in_last_7_days, lambda {
22
+ joins(:grouped_errors)
23
+ .where(Error.in_last_7_days.where(Error[:error_group_id].eq(ErrorGroup[:id])).arel.exists)
24
+ }
25
+ scope :with_errors_in_current_month, lambda {
26
+ joins(:grouped_errors)
27
+ .where(Error.in_current_month.where(Error[:error_group_id].eq(ErrorGroup[:id])).arel.exists)
13
28
  }
14
29
 
15
30
  def self.find_matching_group(error)
@@ -18,6 +33,10 @@ module ExceptionHunter
18
33
  .first
19
34
  end
20
35
 
36
+ def first_occurrence
37
+ @first_occurrence ||= grouped_errors.minimum(:occurred_at)
38
+ end
39
+
21
40
  def last_occurrence
22
41
  @last_occurrence ||= grouped_errors.maximum(:occurred_at)
23
42
  end
@@ -0,0 +1,54 @@
1
+ module ExceptionHunter
2
+ class DashboardPresenter
3
+ LAST_7_DAYS_TAB = 'last_7_days'.freeze
4
+ CURRENT_MONTH_TAB = 'current_month'.freeze
5
+ TOTAL_ERRORS_TAB = 'total_errors'.freeze
6
+ RESOLVED_ERRORS_TAB = 'resolved'.freeze
7
+ TABS = [LAST_7_DAYS_TAB, CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB].freeze
8
+ DEFAULT_TAB = LAST_7_DAYS_TAB
9
+
10
+ attr_reader :current_tab
11
+
12
+ def initialize(current_tab)
13
+ assign_tab(current_tab)
14
+ calculate_tabs_counts
15
+ end
16
+
17
+ def tab_active?(tab)
18
+ tab == current_tab
19
+ end
20
+
21
+ def partial_for_tab
22
+ case current_tab
23
+ when LAST_7_DAYS_TAB
24
+ 'exception_hunter/errors/last_7_days_errors_table'
25
+ when CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB
26
+ 'exception_hunter/errors/errors_table'
27
+ end
28
+ end
29
+
30
+ def errors_count(tab)
31
+ @tabs_counts[tab]
32
+ end
33
+
34
+ private
35
+
36
+ def assign_tab(tab)
37
+ @current_tab = if TABS.include?(tab)
38
+ tab
39
+ else
40
+ DEFAULT_TAB
41
+ end
42
+ end
43
+
44
+ def calculate_tabs_counts
45
+ active_errors = Error.from_active_error_groups
46
+ @tabs_counts = {
47
+ LAST_7_DAYS_TAB => active_errors.in_last_7_days.count,
48
+ CURRENT_MONTH_TAB => active_errors.in_current_month.count,
49
+ TOTAL_ERRORS_TAB => active_errors.count,
50
+ RESOLVED_ERRORS_TAB => Error.from_resolved_error_groups.count
51
+ }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ module ExceptionHunter
2
+ class ErrorGroupPresenter
3
+ delegate_missing_to :error_group
4
+
5
+ def initialize(error_group)
6
+ @error_group = error_group
7
+ end
8
+
9
+ def self.wrap_collection(collection)
10
+ collection.map { |error_group| new(error_group) }
11
+ end
12
+
13
+ def self.format_occurrence_day(day)
14
+ day.to_date.strftime('%A, %B %d')
15
+ end
16
+
17
+ def show_for_day?(day)
18
+ last_occurrence.in_time_zone.to_date == day.to_date
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :error_group
24
+ end
25
+ end
@@ -1,6 +1,7 @@
1
1
  module ExceptionHunter
2
2
  class ErrorPresenter
3
3
  delegate_missing_to :error
4
+ delegate :tags, to: :error_group
4
5
 
5
6
  BacktraceLine = Struct.new(:path, :file_name, :line_number, :method_call)
6
7
 
@@ -9,11 +10,19 @@ module ExceptionHunter
9
10
  end
10
11
 
11
12
  def backtrace
12
- error.backtrace.map do |line|
13
+ (error.backtrace || []).map do |line|
13
14
  format_backtrace_line(line)
14
15
  end
15
16
  end
16
17
 
18
+ def environment_data
19
+ error.environment_data&.except('params') || {}
20
+ end
21
+
22
+ def tracked_params
23
+ (error.environment_data || {})['params']
24
+ end
25
+
17
26
  private
18
27
 
19
28
  attr_reader :error
@@ -0,0 +1,24 @@
1
+ <div class="login_form_container">
2
+ <div class="login_left_container">
3
+ <div class="left_column">Exception Hunter</div>
4
+ </div>
5
+ <div class="login_right_container">
6
+ <%= form_for(resource, as: resource_name, url: exception_hunter_create_session_path) do |f| %>
7
+ <div class="row">
8
+ Sign into your account
9
+ </div>
10
+ <div class="login_row row"></div>
11
+ <div class="login_row row">
12
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: 'Email' %>
13
+ </div>
14
+ <div class="login_row row">
15
+ <%= f.password_field :password, autocomplete: "current-password", placeholder: 'Password'%>
16
+ </div>
17
+ <div class="login_button row">
18
+ <div class="column column-50 column-offset-50">
19
+ <input class="button-log-in" type="submit" value="Log in">
20
+ </div>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
@@ -0,0 +1,44 @@
1
+ <div class="row error-row">
2
+ <div class="column column-10 error-cell">
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-40 error-cell error-cell__message">
10
+ <%= link_to error.message, error_path(error.id), class: %w[error-message] %>
11
+ </div>
12
+
13
+ <div class="column column-15 error-cell">
14
+ <% if error.first_occurrence.present? %>
15
+ <%= time_ago_in_words(error.first_occurrence) %> ago
16
+ <% else %>
17
+ Never
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="column column-15 error-cell">
22
+ <% if error.last_occurrence.present? %>
23
+ <%= time_ago_in_words(error.last_occurrence) %> ago
24
+ <% else %>
25
+ Never
26
+ <% end %>
27
+ </div>
28
+
29
+ <div class="column column-10 error-cell">
30
+ <%= error.total_occurrences %>
31
+ </div>
32
+
33
+ <div class="column column-10 error-cell">
34
+ <% if error.active? %>
35
+ <div class="color-green">
36
+ <%= button_to('Resolve', resolved_errors_path(error_group: { id: error.id }),
37
+ method: :post,
38
+ class: %w[button button-outline resolve-button],
39
+ data: { confirm: 'Are you sure you want to resolve this error?' }) %>
40
+ </div>
41
+ <% end %>
42
+ </div>
43
+ </div>
44
+
@@ -1,24 +1,37 @@
1
1
  <% if error.environment_data.empty? %>
2
- <div class="row error-row-no-border data-title">
2
+ <div class="row error-row data-title">
3
3
  Unfortunately, no environment information has been registered for this error.
4
4
  </div>
5
5
  <% else %>
6
- <div class="row error-row-no-border data-title">
6
+ <div class="row error-row data-title">
7
7
  Environment Data
8
8
  </div>
9
- <% error.environment_data.each do |key, value| %>
10
- <b><%= key %></b>: <%= value %><br>
11
- <% end %>
9
+ <pre class="tracked-data">
10
+
11
+ <%= format_tracked_data(error.environment_data) %>
12
+ </pre>
12
13
  <% end %>
14
+
15
+ <% unless error.tracked_params.nil? %>
16
+ <div class="row error-row data-title">
17
+ Tracked Params
18
+ </div>
19
+ <pre class="tracked-data">
20
+
21
+ <%= format_tracked_data(error.tracked_params) %>
22
+ </pre>
23
+ <% end %>
24
+
13
25
  <% if error.custom_data.nil? %>
14
- <div class="row error-row-no-border data-title">
26
+ <div class="row error-row data-title">
15
27
  No custom data included.
16
28
  </div>
17
29
  <% else %>
18
- <div class="row error-row-no-border data-title">
30
+ <div class="row error-row data-title">
19
31
  Custom Data
20
32
  </div>
21
- <% error.custom_data.each do |key, value| %>
22
- <b><%= key %></b>: <%= value || 'None' %> <br>
23
- <% end %>
33
+ <pre class="tracked-data">
34
+
35
+ <%= format_tracked_data(error.custom_data) %>
36
+ </pre>
24
37
  <% end %>