exception_hunter 0.2.0 → 1.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -6
  3. data/app/assets/stylesheets/exception_hunter/base.css +66 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +188 -25
  5. data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
  6. data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
  7. data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
  8. data/app/controllers/exception_hunter/application_controller.rb +2 -0
  9. data/app/controllers/exception_hunter/errors_controller.rb +29 -4
  10. data/app/controllers/exception_hunter/ignored_errors_controller.rb +25 -0
  11. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  12. data/app/helpers/exception_hunter/application_helper.rb +22 -0
  13. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  14. data/app/jobs/exception_hunter/send_notification_job.rb +15 -0
  15. data/app/models/exception_hunter/application_record.rb +8 -0
  16. data/app/models/exception_hunter/error.rb +24 -7
  17. data/app/models/exception_hunter/error_group.rb +24 -5
  18. data/app/presenters/exception_hunter/dashboard_presenter.rb +56 -0
  19. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  20. data/app/presenters/exception_hunter/error_presenter.rb +3 -2
  21. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  22. data/app/views/exception_hunter/errors/_error_row.erb +52 -0
  23. data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
  24. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  25. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  26. data/app/views/exception_hunter/errors/index.html.erb +84 -29
  27. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  28. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  29. data/app/views/layouts/exception_hunter/application.html.erb +67 -6
  30. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  31. data/config/rails_best_practices.yml +3 -3
  32. data/config/routes.rb +21 -1
  33. data/lib/exception_hunter.rb +44 -2
  34. data/lib/exception_hunter/config.rb +25 -1
  35. data/lib/exception_hunter/data_redacter.rb +27 -0
  36. data/lib/exception_hunter/devise.rb +19 -0
  37. data/lib/exception_hunter/engine.rb +6 -0
  38. data/lib/exception_hunter/error_creator.rb +72 -0
  39. data/lib/exception_hunter/error_reaper.rb +20 -0
  40. data/lib/exception_hunter/middleware/delayed_job_hunter.rb +70 -0
  41. data/lib/exception_hunter/middleware/request_hunter.rb +5 -2
  42. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -1
  43. data/lib/exception_hunter/notifiers/misconfigured_notifiers.rb +10 -0
  44. data/lib/exception_hunter/notifiers/slack_notifier.rb +42 -0
  45. data/lib/exception_hunter/notifiers/slack_notifier_serializer.rb +20 -0
  46. data/lib/exception_hunter/tracking.rb +35 -0
  47. data/lib/exception_hunter/user_attributes_collector.rb +21 -0
  48. data/lib/exception_hunter/version.rb +1 -1
  49. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  50. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  51. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  52. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +52 -0
  53. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  54. metadata +46 -12
  55. data/app/services/exception_hunter/error_creator.rb +0 -41
  56. data/config/initializers/exception_hunter.rb +0 -16
@@ -1,8 +1,7 @@
1
1
  .nav {
2
- background: var(--secondary-color);
3
- border-bottom: .1rem solid var(--border-color);
2
+ background: #000;
4
3
  display: block;
5
- height: 5.2rem;
4
+ height: 4.2rem;
6
5
  left: 0;
7
6
  max-width: 100%;
8
7
  position: fixed;
@@ -16,6 +15,22 @@
16
15
  height: 100%;
17
16
  }
18
17
 
19
- .nav__logo img {
20
- height: 100%;
18
+ .nav__title {
19
+ line-height: 4.2rem;
20
+ font-style: normal;
21
+ font-weight: 600;
22
+ font-size: 16px;
23
+ color: #FFF;
24
+ }
25
+
26
+ .footer {
27
+ text-align: center;
28
+ padding-top: 4rem;
29
+ padding-bottom: 1rem;
30
+ }
31
+
32
+ .logout {
33
+ line-height: 4.2rem;
34
+ color: #FFF;
35
+ text-align: right;
21
36
  }
@@ -0,0 +1,71 @@
1
+ .login_form_container {
2
+ margin: 0rem auto auto;
3
+ display: flex;
4
+ max-width: 75%;
5
+ margin-top: 20rem;
6
+ width: 910px;
7
+ height: 356px;
8
+ font-family: 'Inter', sans-serif;
9
+ background-color: white;
10
+ }
11
+
12
+ .login_left_container {
13
+ width: 455px;
14
+ text-align: center;
15
+ width: 38rem;
16
+ background-color: black;
17
+
18
+ }
19
+
20
+ .left_column {
21
+ max-width: 50%;
22
+ padding-top: 15%;
23
+ padding-left: 10%;
24
+ line-height: 130%;
25
+ color: white;
26
+ text-align: left;
27
+ font-size: 30px;
28
+ font-weight: 600;
29
+ }
30
+
31
+ .login_right_container {
32
+ width: 350px;
33
+ margin: 3rem 3.5rem 1rem;
34
+ font-size: 24px;
35
+ font-weight: 400;
36
+ text-align: left;
37
+ line-height: 29px;
38
+ line-height: 100%;
39
+ color: black;
40
+ }
41
+
42
+ .login_row {
43
+ margin: 2rem 0rem 0rem;
44
+ width: 100%;
45
+ align-items: center;
46
+ }
47
+
48
+ .login_button{
49
+ margin: 3rem 3rem 0rem;
50
+ align-items: right;
51
+ }
52
+
53
+ .button-log-in {
54
+ background-color: #2CBB85!important;
55
+ border-color: #2CBB85!important;
56
+ }
57
+
58
+ .field{
59
+ border: 0.1rem solid black;
60
+ border-radius: .4rem;
61
+ }
62
+
63
+ input[type='password'],
64
+ input[type='email'],
65
+ textarea:focus,
66
+ select:focus {
67
+ border-color: black!important;
68
+ outline: 0;
69
+ font-size: 1.2rem!important;
70
+ color: black!important;
71
+ }
@@ -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,24 @@ 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
+ when DashboardPresenter::IGNORED_ERRORS_TAB
45
+ ErrorGroup.ignored
46
+ end
47
+ end
23
48
  end
24
49
  end
@@ -0,0 +1,25 @@
1
+ require_dependency 'exception_hunter/application_controller'
2
+
3
+ module ExceptionHunter
4
+ class IgnoredErrorsController < ApplicationController
5
+ def create
6
+ error_group.ignored!
7
+ redirect_to errors_path, notice: 'Error ignored successfully'
8
+ end
9
+
10
+ def reopen
11
+ error_group.active!
12
+ redirect_to errors_path, notice: 'Error re-opened successfully'
13
+ end
14
+
15
+ private
16
+
17
+ def error_group
18
+ @error_group ||= ErrorGroup.find(error_group_params[:id])
19
+ end
20
+
21
+ def error_group_params
22
+ params.require(:error_group).permit(:id)
23
+ end
24
+ end
25
+ 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
@@ -1,5 +1,27 @@
1
1
  module ExceptionHunter
2
2
  module ApplicationHelper
3
3
  include Pagy::Frontend
4
+
5
+ def application_name
6
+ if defined? Rails.application.class.module_parent_name
7
+ Rails.application.class.module_parent_name
8
+ else
9
+ Rails.application.class.parent_name
10
+ end
11
+ end
12
+
13
+ def display_action_button(title, error)
14
+ button_to(title.to_s, route_for_button(title, error),
15
+ class: "button button-outline #{title}-button",
16
+ data: { confirm: "Are you sure you want to #{title} this error?" }).to_s
17
+ end
18
+
19
+ def route_for_button(title, error)
20
+ if title.eql?('ignore')
21
+ ignored_errors_path(error_group: { id: error.id })
22
+ else
23
+ resolved_errors_path(error_group: { id: error.id })
24
+ end
25
+ end
4
26
  end
5
27
  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
@@ -0,0 +1,15 @@
1
+ module ExceptionHunter
2
+ class SendNotificationJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform(serialized_notifier)
6
+ # Use SlackNotifierSerializer as it's the only one for now.
7
+ serializer = ExceptionHunter::Notifiers::SlackNotifierSerializer
8
+ deserialized_notifier = serializer.deserialize(serialized_notifier)
9
+ deserialized_notifier.notify
10
+ rescue Exception # rubocop:disable Lint/RescueException
11
+ # Suppress all exceptions to avoid loop as this would create a new error in EH.
12
+ false
13
+ end
14
+ end
15
+ 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,43 @@
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, if: -> { error_group.resolved? }
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
  }
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
+ }
13
28
 
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
29
+ scope :from_ignored_error_groups, lambda {
30
+ joins(:error_group).where(error_group: ErrorGroup.ignored)
31
+ }
19
32
 
20
33
  private
21
34
 
22
35
  def set_occurred_at
23
36
  self.occurred_at ||= Time.now
24
37
  end
38
+
39
+ def unresolve_error_group
40
+ error_group.active!
41
+ end
25
42
  end
26
43
  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, ignored: 2 }
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,56 @@
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
+ IGNORED_ERRORS_TAB = 'ignored'.freeze
8
+ TABS = [LAST_7_DAYS_TAB, CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB, IGNORED_ERRORS_TAB].freeze
9
+ DEFAULT_TAB = LAST_7_DAYS_TAB
10
+
11
+ attr_reader :current_tab
12
+
13
+ def initialize(current_tab)
14
+ assign_tab(current_tab)
15
+ calculate_tabs_counts
16
+ end
17
+
18
+ def tab_active?(tab)
19
+ tab == current_tab
20
+ end
21
+
22
+ def partial_for_tab
23
+ case current_tab
24
+ when LAST_7_DAYS_TAB
25
+ 'exception_hunter/errors/last_7_days_errors_table'
26
+ when CURRENT_MONTH_TAB, TOTAL_ERRORS_TAB, RESOLVED_ERRORS_TAB, IGNORED_ERRORS_TAB
27
+ 'exception_hunter/errors/errors_table'
28
+ end
29
+ end
30
+
31
+ def errors_count(tab)
32
+ @tabs_counts[tab]
33
+ end
34
+
35
+ private
36
+
37
+ def assign_tab(tab)
38
+ @current_tab = if TABS.include?(tab)
39
+ tab
40
+ else
41
+ DEFAULT_TAB
42
+ end
43
+ end
44
+
45
+ def calculate_tabs_counts
46
+ active_errors = Error.from_active_error_groups
47
+ @tabs_counts = {
48
+ LAST_7_DAYS_TAB => active_errors.in_last_7_days.count,
49
+ CURRENT_MONTH_TAB => active_errors.in_current_month.count,
50
+ TOTAL_ERRORS_TAB => active_errors.count,
51
+ RESOLVED_ERRORS_TAB => Error.from_resolved_error_groups.count,
52
+ IGNORED_ERRORS_TAB => Error.from_ignored_error_groups.count
53
+ }
54
+ end
55
+ end
56
+ end