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.
- checksums.yaml +4 -4
- data/README.md +103 -6
- data/app/assets/stylesheets/exception_hunter/base.css +66 -8
- data/app/assets/stylesheets/exception_hunter/errors.css +188 -25
- data/app/assets/stylesheets/exception_hunter/navigation.css +20 -5
- data/app/assets/stylesheets/exception_hunter/sessions.css +71 -0
- data/app/controllers/concerns/exception_hunter/authorization.rb +23 -0
- data/app/controllers/exception_hunter/application_controller.rb +2 -0
- data/app/controllers/exception_hunter/errors_controller.rb +29 -4
- data/app/controllers/exception_hunter/ignored_errors_controller.rb +25 -0
- data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
- data/app/helpers/exception_hunter/application_helper.rb +22 -0
- data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
- data/app/jobs/exception_hunter/send_notification_job.rb +15 -0
- data/app/models/exception_hunter/application_record.rb +8 -0
- data/app/models/exception_hunter/error.rb +24 -7
- data/app/models/exception_hunter/error_group.rb +24 -5
- data/app/presenters/exception_hunter/dashboard_presenter.rb +56 -0
- data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
- data/app/presenters/exception_hunter/error_presenter.rb +3 -2
- data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
- data/app/views/exception_hunter/errors/_error_row.erb +52 -0
- data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
- data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
- data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
- data/app/views/exception_hunter/errors/index.html.erb +84 -29
- data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
- data/app/views/exception_hunter/errors/show.html.erb +58 -22
- data/app/views/layouts/exception_hunter/application.html.erb +67 -6
- data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
- data/config/rails_best_practices.yml +3 -3
- data/config/routes.rb +21 -1
- data/lib/exception_hunter.rb +44 -2
- data/lib/exception_hunter/config.rb +25 -1
- data/lib/exception_hunter/data_redacter.rb +27 -0
- data/lib/exception_hunter/devise.rb +19 -0
- data/lib/exception_hunter/engine.rb +6 -0
- data/lib/exception_hunter/error_creator.rb +72 -0
- data/lib/exception_hunter/error_reaper.rb +20 -0
- data/lib/exception_hunter/middleware/delayed_job_hunter.rb +70 -0
- data/lib/exception_hunter/middleware/request_hunter.rb +5 -2
- data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -1
- data/lib/exception_hunter/notifiers/misconfigured_notifiers.rb +10 -0
- data/lib/exception_hunter/notifiers/slack_notifier.rb +42 -0
- data/lib/exception_hunter/notifiers/slack_notifier_serializer.rb +20 -0
- data/lib/exception_hunter/tracking.rb +35 -0
- data/lib/exception_hunter/user_attributes_collector.rb +21 -0
- data/lib/exception_hunter/version.rb +1 -1
- data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
- data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
- data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
- data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +52 -0
- data/lib/tasks/exception_hunter_tasks.rake +6 -4
- metadata +46 -12
- data/app/services/exception_hunter/error_creator.rb +0 -41
- data/config/initializers/exception_hunter.rb +0 -16
@@ -1,8 +1,7 @@
|
|
1
1
|
.nav {
|
2
|
-
background:
|
3
|
-
border-bottom: .1rem solid var(--border-color);
|
2
|
+
background: #000;
|
4
3
|
display: block;
|
5
|
-
height:
|
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
|
-
.
|
20
|
-
height:
|
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
|
@@ -5,14 +5,20 @@ module ExceptionHunter
|
|
5
5
|
include Pagy::Backend
|
6
6
|
|
7
7
|
def index
|
8
|
-
@
|
9
|
-
|
10
|
-
@
|
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
|
-
|
15
|
-
|
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
|
-
|
11
|
-
where(
|
12
|
-
.order(
|
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
|