exception_hunter 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -6
  3. data/app/assets/stylesheets/exception_hunter/base.css +62 -8
  4. data/app/assets/stylesheets/exception_hunter/errors.css +163 -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 +27 -4
  10. data/app/controllers/exception_hunter/resolved_errors_controller.rb +11 -0
  11. data/app/helpers/exception_hunter/sessions_helper.rb +16 -0
  12. data/app/models/exception_hunter/application_record.rb +8 -0
  13. data/app/models/exception_hunter/error.rb +20 -7
  14. data/app/models/exception_hunter/error_group.rb +23 -4
  15. data/app/presenters/exception_hunter/dashboard_presenter.rb +54 -0
  16. data/app/presenters/exception_hunter/error_group_presenter.rb +25 -0
  17. data/app/presenters/exception_hunter/error_presenter.rb +1 -0
  18. data/app/views/exception_hunter/devise/sessions/new.html.erb +24 -0
  19. data/app/views/exception_hunter/errors/_error_row.erb +44 -0
  20. data/app/views/exception_hunter/errors/_error_summary.erb +5 -5
  21. data/app/views/exception_hunter/errors/_errors_table.erb +1 -0
  22. data/app/views/exception_hunter/errors/_last_7_days_errors_table.erb +12 -0
  23. data/app/views/exception_hunter/errors/index.html.erb +71 -30
  24. data/app/views/exception_hunter/errors/pagy/_pagy_nav.html.erb +15 -15
  25. data/app/views/exception_hunter/errors/show.html.erb +58 -22
  26. data/app/views/layouts/exception_hunter/application.html.erb +65 -6
  27. data/app/views/layouts/exception_hunter/exception_hunter_logged_out.html.erb +24 -0
  28. data/config/rails_best_practices.yml +2 -2
  29. data/config/routes.rb +19 -1
  30. data/lib/exception_hunter.rb +11 -1
  31. data/lib/exception_hunter/config.rb +8 -1
  32. data/lib/exception_hunter/devise.rb +17 -0
  33. data/{app/services → lib}/exception_hunter/error_creator.rb +15 -3
  34. data/lib/exception_hunter/error_reaper.rb +12 -0
  35. data/lib/exception_hunter/middleware/request_hunter.rb +1 -0
  36. data/lib/exception_hunter/middleware/sidekiq_hunter.rb +1 -0
  37. data/lib/exception_hunter/tracking.rb +16 -0
  38. data/lib/exception_hunter/user_attributes_collector.rb +4 -0
  39. data/lib/exception_hunter/version.rb +1 -1
  40. data/lib/generators/exception_hunter/create_users/create_users_generator.rb +8 -1
  41. data/lib/generators/exception_hunter/install/install_generator.rb +3 -1
  42. data/lib/generators/exception_hunter/install/templates/create_exception_hunter_error_groups.rb.erb +3 -0
  43. data/lib/generators/exception_hunter/install/templates/exception_hunter.rb.erb +23 -0
  44. data/lib/tasks/exception_hunter_tasks.rake +6 -4
  45. metadata +18 -5
  46. data/config/initializers/exception_hunter.rb +0 -16
@@ -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,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
@@ -3,24 +3,37 @@ module ExceptionHunter
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
- 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
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
@@ -4,12 +4,27 @@ module ExceptionHunter
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
- 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,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
 
@@ -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,9 +1,9 @@
1
1
  <% if error.environment_data.empty? %>
2
- <div class="row error-row-no-border data-title">
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-no-border data-title">
6
+ <div class="row error-row data-title">
7
7
  Environment Data
8
8
  </div>
9
9
  <pre class="tracked-data">
@@ -13,7 +13,7 @@
13
13
  <% end %>
14
14
 
15
15
  <% unless error.tracked_params.nil? %>
16
- <div class="row error-row-no-border data-title">
16
+ <div class="row error-row data-title">
17
17
  Tracked Params
18
18
  </div>
19
19
  <pre class="tracked-data">
@@ -23,11 +23,11 @@
23
23
  <% end %>
24
24
 
25
25
  <% if error.custom_data.nil? %>
26
- <div class="row error-row-no-border data-title">
26
+ <div class="row error-row data-title">
27
27
  No custom data included.
28
28
  </div>
29
29
  <% else %>
30
- <div class="row error-row-no-border data-title">
30
+ <div class="row error-row data-title">
31
31
  Custom Data
32
32
  </div>
33
33
  <pre class="tracked-data">
@@ -0,0 +1 @@
1
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors, as: :error %>
@@ -0,0 +1,12 @@
1
+ <% today_errors = errors.select { |error| error.show_for_day?(Date.current) } %>
2
+ <%= render partial: 'exception_hunter/errors/error_row', collection: today_errors, as: :error %>
3
+
4
+ <% yesterday_errors = errors.select { |error| error.show_for_day?(Date.yesterday) } %>
5
+ <div class="errors-date-group">Yesterday</div>
6
+ <%= render partial: 'exception_hunter/errors/error_row', collection: yesterday_errors, as: :error %>
7
+
8
+ <% (2..6).each do |i| %>
9
+ <% errors_on_day = errors.select { |error| error.show_for_day?(i.days.ago) } %>
10
+ <div class="errors-date-group"><%= ExceptionHunter::ErrorGroupPresenter.format_occurrence_day(i.days.ago) %></div>
11
+ <%= render partial: 'exception_hunter/errors/error_row', collection: errors_on_day, as: :error %>
12
+ <% end %>
@@ -1,36 +1,77 @@
1
- <div class="row statistics-row">
2
- <div class="column column-offset-50 column-25">
3
- <div class="statistics__cell">
4
- <%= number_with_delimiter(@errors_count) %> Total errors
5
- </div>
6
- </div>
7
- <div class="column column-25">
8
- <div class="statistics__cell">
9
- <%= number_with_delimiter(@month_errors) %> Errors this month
1
+ <div class="row">
2
+ <div class="column column-80">
3
+ <div class="errors-tabs">
4
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::LAST_7_DAYS_TAB) %>">
5
+ <%= link_to errors_path(tab: @dashboard.class::LAST_7_DAYS_TAB) do %>
6
+ <div class="errors-tab__content">
7
+ <div class="errors-tab__badge">
8
+ <%= @dashboard.errors_count(@dashboard.class::LAST_7_DAYS_TAB) %>
9
+ </div>
10
+ <div class="errors-tab__description">
11
+ Errors in the last 7 days
12
+ </div>
13
+ </div>
14
+ <% end %>
15
+ </div>
16
+
17
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::CURRENT_MONTH_TAB) %>">
18
+ <%= link_to errors_path(tab: @dashboard.class::CURRENT_MONTH_TAB) do %>
19
+ <div class="errors-tab__content">
20
+ <div class="errors-tab__badge">
21
+ <%= @dashboard.errors_count(@dashboard.class::CURRENT_MONTH_TAB) %>
22
+ </div>
23
+ <div class="errors-tab__description">
24
+ Errors this month
25
+ </div>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="errors-tab <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::TOTAL_ERRORS_TAB) %>">
31
+ <%= link_to errors_path(tab: @dashboard.class::TOTAL_ERRORS_TAB) do %>
32
+ <div class="errors-tab__content">
33
+ <div class="errors-tab__badge">
34
+ <%= @dashboard.errors_count(@dashboard.class::TOTAL_ERRORS_TAB) %>
35
+ </div>
36
+ <div class="errors-tab__description">
37
+ Total errors
38
+ </div>
39
+ </div>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="errors-tab errors-tab__resolved <%= 'errors-tab--active' if @dashboard.tab_active?(@dashboard.class::RESOLVED_ERRORS_TAB) %>">
44
+ <%= link_to errors_path(tab: @dashboard.class::RESOLVED_ERRORS_TAB) do %>
45
+ <div class="errors-tab__content">
46
+ <div class="errors-tab__badge">
47
+ <%= @dashboard.errors_count(@dashboard.class::RESOLVED_ERRORS_TAB) || '-' %>
48
+ </div>
49
+ <div class="errors-tab__description">
50
+ Resolved
51
+ </div>
52
+ </div>
53
+ <% end %>
54
+ </div>
10
55
  </div>
11
56
  </div>
12
- </div>
13
57
 
14
- <div class="row error-row error-row--header">
15
- <div class="column column-75">Message</div>
16
- <div class="column column-15">Last Occurrence</div>
17
- <div class="column column-10">Occurrences</div>
58
+ <div class="column column-10 column-offset-10">
59
+ <%= button_to 'Purge', purge_errors_path,
60
+ class: %w[button purge-button],
61
+ method: :delete,
62
+ data: { confirm: 'This will delete all stale errors, do you want to continue?' } %>
63
+ </div>
18
64
  </div>
19
65
 
20
- <% @errors.each do |error| %>
21
- <div class="row error-row">
22
- <div class="column column-75 error-cell">
23
- <%= link_to error.message, error_path(error.id) %>
24
- </div>
25
- <div class="column column-15 error-cell error-cell--highlight">
26
- <% if error.last_occurrence.present? %>
27
- <%= time_ago_in_words(error.last_occurrence) %> ago
28
- <% else %>
29
- Never
30
- <% end %>
31
- </div>
32
- <div class="column column-10 error-cell error-cell--highlight">
33
- <%= error.total_occurrences %>
34
- </div>
66
+ <div class="errors__container">
67
+ <div class="row error-row error-row--header">
68
+ <div class="column column-10">Tags</div>
69
+ <div class="column column-40">Message</div>
70
+ <div class="column column-15">First Occurrence</div>
71
+ <div class="column column-15">Last Occurrence</div>
72
+ <div class="column column-10">Total</div>
73
+ <div class="column column-10"></div>
35
74
  </div>
36
- <% end %>
75
+
76
+ <%= render partial: @dashboard.partial_for_tab, locals: { errors: @errors } %>
77
+ </div>