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.
- 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
|