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.
- checksums.yaml +4 -4
- data/README.md +35 -7
- data/app/assets/stylesheets/exception_hunter/base.css +62 -8
- data/app/assets/stylesheets/exception_hunter/errors.css +166 -24
- 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 +27 -4
- data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
- data/app/helpers/exception_hunter/errors_helper.rb +7 -0
- data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
- data/app/models/exception_hunter/application_record.rb +8 -0
- data/app/models/exception_hunter/error.rb +21 -8
- data/app/models/exception_hunter/error_group.rb +24 -5
- data/app/presenters/exception_hunter/dashboard_presenter.rb +54 -0
- data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
- data/app/presenters/exception_hunter/error_presenter.rb +10 -1
- data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
- data/app/views/exception_hunter/errors/_error_row.erb +44 -0
- data/app/views/exception_hunter/errors/_error_summary.erb +23 -10
- data/app/views/exception_hunter/errors/_error_user_data.erb +4 -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 +71 -30
- 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 +65 -6
- data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
- data/config/rails_best_practices.yml +2 -3
- data/config/routes.rb +19 -1
- data/lib/exception_hunter.rb +12 -2
- data/lib/exception_hunter/config.rb +8 -1
- data/lib/exception_hunter/devise.rb +17 -0
- data/lib/exception_hunter/engine.rb +5 -0
- data/{app/services → lib}/exception_hunter/error_creator.rb +17 -5
- data/lib/exception_hunter/error_reaper.rb +12 -0
- data/lib/exception_hunter/middleware/delayed_job_hunter.rb +69 -0
- data/lib/exception_hunter/middleware/request_hunter.rb +71 -0
- data/lib/exception_hunter/middleware/sidekiq_hunter.rb +59 -0
- data/lib/exception_hunter/tracking.rb +17 -0
- data/lib/exception_hunter/user_attributes_collector.rb +4 -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 +23 -0
- data/lib/tasks/exception_hunter_tasks.rake +6 -4
- metadata +25 -10
- data/config/initializers/exception_hunter.rb +0 -16
- data/lib/exception_hunter/railtie.rb +0 -11
- 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
|
@@ -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,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,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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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,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
|
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
|
6
|
+
<div class="row error-row data-title">
|
7
7
|
Environment Data
|
8
8
|
</div>
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
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
|
30
|
+
<div class="row error-row data-title">
|
19
31
|
Custom Data
|
20
32
|
</div>
|
21
|
-
|
22
|
-
|
23
|
-
|
33
|
+
<pre class="tracked-data">
|
34
|
+
|
35
|
+
<%= format_tracked_data(error.custom_data) %>
|
36
|
+
</pre>
|
24
37
|
<% end %>
|