exception_hunter 0.1.1 → 0.4.2
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 +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 %>
|