exception_hunter 0.2.0 → 1.0.0

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