exception_hunter 0.1.1 → 0.4.2

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