Commit 4cf6b44fd88178c7c46650547b951daccf25c773
Exists in
master
and in
1 other branch
Merge pull request #946 from stevecrozz/configurable_backtraces
configurable notice fingerprinting
Showing
32 changed files
with
606 additions
and
571 deletions
Show diff stats
.gitignore
README.md
| @@ -88,6 +88,19 @@ Deployment | @@ -88,6 +88,19 @@ Deployment | ||
| 88 | ---------- | 88 | ---------- |
| 89 | See [notes on deployment](docs/deployment.md) | 89 | See [notes on deployment](docs/deployment.md) |
| 90 | 90 | ||
| 91 | +Notice Grouping | ||
| 92 | +--------------- | ||
| 93 | +The way Errbit arranges notices into error groups is configurable. By default, | ||
| 94 | +Errbit uses the notice's error class, error message, complete backtrace, | ||
| 95 | +component (or controller), action and environment name to generate a unique | ||
| 96 | +fingerprint for every notice. Notices with identical fingerprints appear in the | ||
| 97 | +UI as different occurences of the same error and notices with differing | ||
| 98 | +fingerprints are displayed as separate errors. | ||
| 99 | + | ||
| 100 | +Changing the fingerprinter (under the 'config' menu) applies to all apps and | ||
| 101 | +the change affects only notices that arrive after the change. If you want to | ||
| 102 | +refingerprint old notices, you can run `rake errbit:notice_refingerprint`. | ||
| 103 | + | ||
| 91 | Authentication | 104 | Authentication |
| 92 | -------------- | 105 | -------------- |
| 93 | ### Configuring GitHub authentication: | 106 | ### Configuring GitHub authentication: |
| @@ -230,21 +243,6 @@ Airbrake.setProject("ERRBIT API KEY", "ERRBIT API KEY"); | @@ -230,21 +243,6 @@ Airbrake.setProject("ERRBIT API KEY", "ERRBIT API KEY"); | ||
| 230 | Airbrake.setHost("http://errbit.yourdomain.com"); | 243 | Airbrake.setHost("http://errbit.yourdomain.com"); |
| 231 | ``` | 244 | ``` |
| 232 | 245 | ||
| 233 | -Using custom fingerprinting methods | ||
| 234 | ------------------------------------ | ||
| 235 | - | ||
| 236 | -Errbit collates errors into groups using a fingerprinting strategy. If you find | ||
| 237 | -your errors are not getting grouped the way you would expect, you may need to | ||
| 238 | -implement your own strategy. A fingerprinting strategy is just a class that | ||
| 239 | -implements a ::generate class method. See the classes in | ||
| 240 | -`app/models/fingerprint/` if you need some inspiration. You can install it with | ||
| 241 | -an initializer like: | ||
| 242 | - | ||
| 243 | -```ruby | ||
| 244 | -# config/initializers/fingerprint.rb | ||
| 245 | -ErrorReport.fingerprint_strategy = MyStrategy | ||
| 246 | -``` | ||
| 247 | - | ||
| 248 | Plugins and Integrations | 246 | Plugins and Integrations |
| 249 | ------------------------ | 247 | ------------------------ |
| 250 | You can extend Errbit by adding Ruby gems and plugins which are typically gems. | 248 | You can extend Errbit by adding Ruby gems and plugins which are typically gems. |
| @@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
| 1 | +class SiteConfigController < ApplicationController | ||
| 2 | + before_action :require_admin! | ||
| 3 | + | ||
| 4 | + def index | ||
| 5 | + @config = SiteConfig.document | ||
| 6 | + end | ||
| 7 | + | ||
| 8 | + def update | ||
| 9 | + SiteConfig.document.update_attributes( | ||
| 10 | + notice_fingerprinter: filtered_update_params) | ||
| 11 | + flash[:success] = 'Updated site config' | ||
| 12 | + redirect_to action: :index | ||
| 13 | + end | ||
| 14 | + | ||
| 15 | + private def filtered_update_params | ||
| 16 | + params | ||
| 17 | + .require(:site_config) | ||
| 18 | + .require(:notice_fingerprinter_attributes) | ||
| 19 | + .permit( | ||
| 20 | + :error_class, | ||
| 21 | + :message, | ||
| 22 | + :backtrace_lines, | ||
| 23 | + :component, | ||
| 24 | + :action, | ||
| 25 | + :environment_name) | ||
| 26 | + end | ||
| 27 | +end |
| @@ -0,0 +1,38 @@ | @@ -0,0 +1,38 @@ | ||
| 1 | +class NoticeRefingerprinter | ||
| 2 | + LOG_EVERY = 100 | ||
| 3 | + LOG_ITR = '%.1f%% complete, %i notice(s) remaining' | ||
| 4 | + LOG_START = 'Regenerating notice fingerprints for %i notices' | ||
| 5 | + | ||
| 6 | + def self.run | ||
| 7 | + count = Notice.count | ||
| 8 | + puts format(LOG_START, count) | ||
| 9 | + | ||
| 10 | + Notice.no_timeout.each_with_index do |notice, index| | ||
| 11 | + refingerprint(notice) | ||
| 12 | + | ||
| 13 | + next unless (index + 1) % LOG_EVERY == 0 | ||
| 14 | + puts format(LOG_ITR, (index * 100 / count), count - index) | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + puts 'Finished generating notice fingerprints' | ||
| 18 | + puts 'Destroying orphaned err records' | ||
| 19 | + | ||
| 20 | + Err.each { |e| e.destroy if e.notices.size == 0 } | ||
| 21 | + | ||
| 22 | + puts 'Finished destroying orphaned err records' | ||
| 23 | + end | ||
| 24 | + | ||
| 25 | + def self.refingerprint(notice) | ||
| 26 | + app = notice.app | ||
| 27 | + notice.err = app.find_or_create_err!( | ||
| 28 | + error_class: notice.error_class, | ||
| 29 | + environment: notice.environment_name, | ||
| 30 | + fingerprint: app.notice_fingerprinter.generate(app.api_key, notice, notice.backtrace) | ||
| 31 | + ) | ||
| 32 | + notice.save! | ||
| 33 | + end | ||
| 34 | + | ||
| 35 | + def self.puts(*args) | ||
| 36 | + Rails.logger.info(*args) | ||
| 37 | + end | ||
| 38 | +end |
app/interactors/problem_merge.rb
| @@ -16,13 +16,7 @@ class ProblemMerge | @@ -16,13 +16,7 @@ class ProblemMerge | ||
| 16 | problem.reload # deference all associate objet to avoid delete him after | 16 | problem.reload # deference all associate objet to avoid delete him after |
| 17 | ProblemDestroy.execute(problem) | 17 | ProblemDestroy.execute(problem) |
| 18 | end | 18 | end |
| 19 | - reset_cached_attributes | 19 | + merged_problem.recache |
| 20 | merged_problem | 20 | merged_problem |
| 21 | end | 21 | end |
| 22 | - | ||
| 23 | - private | ||
| 24 | - | ||
| 25 | - def reset_cached_attributes | ||
| 26 | - ProblemUpdaterCache.new(merged_problem).update | ||
| 27 | - end | ||
| 28 | end | 22 | end |
| @@ -0,0 +1,24 @@ | @@ -0,0 +1,24 @@ | ||
| 1 | +class ProblemRecacher | ||
| 2 | + LOG_EVERY = 100 | ||
| 3 | + LOG_ITR = '%.1f%% complete, %i problem(s) remaining' | ||
| 4 | + LOG_START = 'Re-caching problem attributes for %i problems' | ||
| 5 | + | ||
| 6 | + def self.run | ||
| 7 | + count = Problem.count | ||
| 8 | + puts format(LOG_START, count) | ||
| 9 | + | ||
| 10 | + Problem.no_timeout.each_with_index do |problem, index| | ||
| 11 | + problem.recache | ||
| 12 | + problem.destroy if problem.notices_count == 0 | ||
| 13 | + | ||
| 14 | + next unless (index + 1) % LOG_EVERY == 0 | ||
| 15 | + puts format(LOG_ITR, (index * 100 / count), count - index) | ||
| 16 | + end | ||
| 17 | + | ||
| 18 | + puts "Finished re-caching problem attributes" | ||
| 19 | + end | ||
| 20 | + | ||
| 21 | + def self.puts(*args) | ||
| 22 | + Rails.logger.info(*args) | ||
| 23 | + end | ||
| 24 | +end |
app/interactors/problem_updater_cache.rb
| @@ -1,88 +0,0 @@ | @@ -1,88 +0,0 @@ | ||
| 1 | -class ProblemUpdaterCache | ||
| 2 | - def initialize(problem, notice=nil) | ||
| 3 | - @problem = problem | ||
| 4 | - @notice = notice | ||
| 5 | - end | ||
| 6 | - attr_reader :problem | ||
| 7 | - | ||
| 8 | - ## | ||
| 9 | - # Update cache information about child associate to this problem | ||
| 10 | - # | ||
| 11 | - # update the notices count, and some notice informations | ||
| 12 | - # | ||
| 13 | - # @return [ Problem ] the problem with this update | ||
| 14 | - # | ||
| 15 | - def update | ||
| 16 | - update_notices_count | ||
| 17 | - update_notices_cache | ||
| 18 | - problem | ||
| 19 | - end | ||
| 20 | - | ||
| 21 | - private | ||
| 22 | - | ||
| 23 | - def update_notices_count | ||
| 24 | - if @notice | ||
| 25 | - problem.inc(notices_count: 1) | ||
| 26 | - else | ||
| 27 | - problem.update_attribute( | ||
| 28 | - :notices_count, problem.notices.count | ||
| 29 | - ) | ||
| 30 | - end | ||
| 31 | - end | ||
| 32 | - | ||
| 33 | - ## | ||
| 34 | - # Update problem statistique from some notice information | ||
| 35 | - # | ||
| 36 | - def update_notices_cache | ||
| 37 | - first_notice = notices.first | ||
| 38 | - last_notice = notices.last | ||
| 39 | - notice ||= @notice || first_notice | ||
| 40 | - | ||
| 41 | - attrs = {} | ||
| 42 | - attrs[:first_notice_at] = first_notice.created_at if first_notice | ||
| 43 | - attrs[:last_notice_at] = last_notice.created_at if last_notice | ||
| 44 | - attrs.merge!( | ||
| 45 | - :message => notice.message, | ||
| 46 | - :where => notice.where, | ||
| 47 | - :messages => attribute_count(:message, messages), | ||
| 48 | - :hosts => attribute_count(:host, hosts), | ||
| 49 | - :user_agents => attribute_count(:user_agent_string, user_agents) | ||
| 50 | - ) if notice | ||
| 51 | - problem.update_attributes!(attrs) | ||
| 52 | - end | ||
| 53 | - | ||
| 54 | - def notices | ||
| 55 | - @notices ||= @notice ? [@notice].sort(&:created_at) : problem.notices.order_by([:created_at, :asc]) | ||
| 56 | - end | ||
| 57 | - | ||
| 58 | - def messages | ||
| 59 | - @notice ? problem.messages : {} | ||
| 60 | - end | ||
| 61 | - | ||
| 62 | - def hosts | ||
| 63 | - @notice ? problem.hosts : {} | ||
| 64 | - end | ||
| 65 | - | ||
| 66 | - def user_agents | ||
| 67 | - @notice ? problem.user_agents : {} | ||
| 68 | - end | ||
| 69 | - | ||
| 70 | - private | ||
| 71 | - | ||
| 72 | - def attribute_count(value, init) | ||
| 73 | - init.tap do |counts| | ||
| 74 | - notices.each do |notice| | ||
| 75 | - counts[attribute_index(notice.send(value))] ||= { | ||
| 76 | - 'value' => notice.send(value), | ||
| 77 | - 'count' => 0 | ||
| 78 | - } | ||
| 79 | - counts[attribute_index(notice.send(value))]['count'] += 1 | ||
| 80 | - end | ||
| 81 | - end | ||
| 82 | - end | ||
| 83 | - | ||
| 84 | - def attribute_index(value) | ||
| 85 | - @attributes_index ||= {} | ||
| 86 | - @attributes_index[value.to_s] ||= Digest::MD5.hexdigest(value.to_s) | ||
| 87 | - end | ||
| 88 | -end |
app/models/app.rb
| @@ -27,6 +27,7 @@ class App | @@ -27,6 +27,7 @@ class App | ||
| 27 | embeds_many :deploys | 27 | embeds_many :deploys |
| 28 | embeds_one :issue_tracker, :class_name => 'IssueTracker' | 28 | embeds_one :issue_tracker, :class_name => 'IssueTracker' |
| 29 | embeds_one :notification_service | 29 | embeds_one :notification_service |
| 30 | + embeds_one :notice_fingerprinter, autobuild: true | ||
| 30 | 31 | ||
| 31 | has_many :problems, :inverse_of => :app, :dependent => :destroy | 32 | has_many :problems, :inverse_of => :app, :dependent => :destroy |
| 32 | 33 | ||
| @@ -38,6 +39,7 @@ class App | @@ -38,6 +39,7 @@ class App | ||
| 38 | validates_uniqueness_of :name, :allow_blank => true | 39 | validates_uniqueness_of :name, :allow_blank => true |
| 39 | validates_uniqueness_of :api_key, :allow_blank => true | 40 | validates_uniqueness_of :api_key, :allow_blank => true |
| 40 | validates_associated :watchers | 41 | validates_associated :watchers |
| 42 | + validates_associated :notice_fingerprinter | ||
| 41 | validate :check_issue_tracker | 43 | validate :check_issue_tracker |
| 42 | 44 | ||
| 43 | accepts_nested_attributes_for :watchers, :allow_destroy => true, | 45 | accepts_nested_attributes_for :watchers, :allow_destroy => true, |
| @@ -46,6 +48,7 @@ class App | @@ -46,6 +48,7 @@ class App | ||
| 46 | :reject_if => proc { |attrs| !ErrbitPlugin::Registry.issue_trackers.keys.map(&:to_s).include?(attrs[:type_tracker].to_s) } | 48 | :reject_if => proc { |attrs| !ErrbitPlugin::Registry.issue_trackers.keys.map(&:to_s).include?(attrs[:type_tracker].to_s) } |
| 47 | accepts_nested_attributes_for :notification_service, :allow_destroy => true, | 49 | accepts_nested_attributes_for :notification_service, :allow_destroy => true, |
| 48 | :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) } | 50 | :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) } |
| 51 | + accepts_nested_attributes_for :notice_fingerprinter | ||
| 49 | 52 | ||
| 50 | scope :watched_by, ->(user) do | 53 | scope :watched_by, ->(user) do |
| 51 | where watchers: { "$elemMatch" => { "user_id" => user.id } } | 54 | where watchers: { "$elemMatch" => { "user_id" => user.id } } |
| @@ -62,16 +65,15 @@ class App | @@ -62,16 +65,15 @@ class App | ||
| 62 | # * <tt>:fingerprint</tt> - a unique value identifying the notice | 65 | # * <tt>:fingerprint</tt> - a unique value identifying the notice |
| 63 | # | 66 | # |
| 64 | def find_or_create_err!(attrs) | 67 | def find_or_create_err!(attrs) |
| 65 | - Err.where( | ||
| 66 | - :fingerprint => attrs[:fingerprint] | ||
| 67 | - ).first || ( | ||
| 68 | - problem = problems.create!( | ||
| 69 | - error_class: attrs[:error_class], | ||
| 70 | - environment: attrs[:environment], | ||
| 71 | - app_name: name | ||
| 72 | - ) | ||
| 73 | - problem.errs.create!(attrs.slice(:fingerprint, :problem_id)) | 68 | + err = Err.where(fingerprint: attrs[:fingerprint]).first |
| 69 | + return err if err | ||
| 70 | + | ||
| 71 | + problem = problems.create!( | ||
| 72 | + error_class: attrs[:error_class], | ||
| 73 | + environment: attrs[:environment], | ||
| 74 | + app_name: name | ||
| 74 | ) | 75 | ) |
| 76 | + problem.errs.create!(attrs.slice(:fingerprint, :problem_id)) | ||
| 75 | end | 77 | end |
| 76 | 78 | ||
| 77 | # Mongoid Bug: find(id) on association proxies returns an Enumerator | 79 | # Mongoid Bug: find(id) on association proxies returns an Enumerator |
app/models/error_report.rb
| @@ -26,10 +26,6 @@ class ErrorReport | @@ -26,10 +26,6 @@ class ErrorReport | ||
| 26 | attr_reader :server_environment | 26 | attr_reader :server_environment |
| 27 | attr_reader :user_attributes | 27 | attr_reader :user_attributes |
| 28 | 28 | ||
| 29 | - cattr_accessor :fingerprint_strategy do | ||
| 30 | - Fingerprint::Sha1 | ||
| 31 | - end | ||
| 32 | - | ||
| 33 | def initialize(xml_or_attributes) | 29 | def initialize(xml_or_attributes) |
| 34 | @attributes = xml_or_attributes | 30 | @attributes = xml_or_attributes |
| 35 | @attributes = Hoptoad.parse_xml!(@attributes) if @attributes.is_a? String | 31 | @attributes = Hoptoad.parse_xml!(@attributes) if @attributes.is_a? String |
| @@ -130,9 +126,7 @@ class ErrorReport | @@ -130,9 +126,7 @@ class ErrorReport | ||
| 130 | Gem::Version.new(app_version) >= Gem::Version.new(current_version) | 126 | Gem::Version.new(app_version) >= Gem::Version.new(current_version) |
| 131 | end | 127 | end |
| 132 | 128 | ||
| 133 | - private | ||
| 134 | - | ||
| 135 | def fingerprint | 129 | def fingerprint |
| 136 | - @fingerprint ||= fingerprint_strategy.generate(notice, api_key) | 130 | + app.notice_fingerprinter.generate(api_key, notice, backtrace) |
| 137 | end | 131 | end |
| 138 | end | 132 | end |
app/models/fingerprint/md5.rb
| @@ -1,22 +0,0 @@ | @@ -1,22 +0,0 @@ | ||
| 1 | -require 'digest/md5' | ||
| 2 | - | ||
| 3 | -module Fingerprint | ||
| 4 | - class MD5 < Sha1 | ||
| 5 | - def to_s | ||
| 6 | - Digest::MD5.hexdigest(fingerprint_source) | ||
| 7 | - end | ||
| 8 | - | ||
| 9 | - def fingerprint_source | ||
| 10 | - location['method'] &&= sanitized_method_signature | ||
| 11 | - end | ||
| 12 | - | ||
| 13 | - private | ||
| 14 | - def sanitized_method_signature | ||
| 15 | - location['method'].gsub(/[0-9]+|FRAGMENT/, '#').gsub(/_+#/, '_#') | ||
| 16 | - end | ||
| 17 | - | ||
| 18 | - def location | ||
| 19 | - notice.backtrace.lines.first | ||
| 20 | - end | ||
| 21 | - end | ||
| 22 | -end |
app/models/fingerprint/sha1.rb
| @@ -1,43 +0,0 @@ | @@ -1,43 +0,0 @@ | ||
| 1 | -require 'digest/sha1' | ||
| 2 | - | ||
| 3 | -module Fingerprint | ||
| 4 | - class Sha1 | ||
| 5 | - | ||
| 6 | - attr_reader :notice, :api_key | ||
| 7 | - | ||
| 8 | - def self.generate(notice, api_key) | ||
| 9 | - self.new(notice, api_key).to_s | ||
| 10 | - end | ||
| 11 | - | ||
| 12 | - def initialize(notice, api_key) | ||
| 13 | - @notice = notice | ||
| 14 | - @api_key = api_key | ||
| 15 | - end | ||
| 16 | - | ||
| 17 | - def to_s | ||
| 18 | - Digest::SHA1.hexdigest(fingerprint_source.to_s) | ||
| 19 | - end | ||
| 20 | - | ||
| 21 | - def fingerprint_source | ||
| 22 | - { | ||
| 23 | - :file_or_message => file_or_message, | ||
| 24 | - :error_class => notice.error_class, | ||
| 25 | - :component => notice.component || 'unknown', | ||
| 26 | - :action => notice.action, | ||
| 27 | - :environment => notice.environment_name || 'development', | ||
| 28 | - :api_key => api_key | ||
| 29 | - } | ||
| 30 | - end | ||
| 31 | - | ||
| 32 | - def file_or_message | ||
| 33 | - @file_or_message ||= unified_message + notice.backtrace.fingerprint | ||
| 34 | - end | ||
| 35 | - | ||
| 36 | - # filter memory addresses out of object strings | ||
| 37 | - # example: "#<Object:0x007fa2b33d9458>" becomes "#<Object>" | ||
| 38 | - def unified_message | ||
| 39 | - notice.message.gsub(/(#<.+?):[0-9a-f]x[0-9a-f]+(>)/, '\1\2') | ||
| 40 | - end | ||
| 41 | - | ||
| 42 | - end | ||
| 43 | -end |
app/models/notice.rb
| @@ -29,9 +29,7 @@ class Notice | @@ -29,9 +29,7 @@ class Notice | ||
| 29 | scope :ordered, ->{ order_by(:created_at.asc) } | 29 | scope :ordered, ->{ order_by(:created_at.asc) } |
| 30 | scope :reverse_ordered, ->{ order_by(:created_at.desc) } | 30 | scope :reverse_ordered, ->{ order_by(:created_at.desc) } |
| 31 | scope :for_errs, Proc.new { |errs| | 31 | scope :for_errs, Proc.new { |errs| |
| 32 | - if (ids = errs.all.map(&:id)) && ids.present? | ||
| 33 | - where(:err_id.in => ids) | ||
| 34 | - end | 32 | + where(:err_id.in => errs.all.map(&:id)) |
| 35 | } | 33 | } |
| 36 | 34 | ||
| 37 | def user_agent | 35 | def user_agent |
| @@ -48,7 +46,8 @@ class Notice | @@ -48,7 +46,8 @@ class Notice | ||
| 48 | end | 46 | end |
| 49 | 47 | ||
| 50 | def environment_name | 48 | def environment_name |
| 51 | - server_environment['server-environment'] || server_environment['environment-name'] | 49 | + n = server_environment['server-environment'] || server_environment['environment-name'] |
| 50 | + n.blank? ? 'development' : n | ||
| 52 | end | 51 | end |
| 53 | 52 | ||
| 54 | def component | 53 | def component |
| @@ -119,6 +118,12 @@ class Notice | @@ -119,6 +118,12 @@ class Notice | ||
| 119 | end | 118 | end |
| 120 | end | 119 | end |
| 121 | 120 | ||
| 121 | + # filter memory addresses out of object strings | ||
| 122 | + # example: "#<Object:0x007fa2b33d9458>" becomes "#<Object>" | ||
| 123 | + def filtered_message | ||
| 124 | + message.gsub(/(#<.+?):[0-9a-f]x[0-9a-f]+(>)/, '\1\2') | ||
| 125 | + end | ||
| 126 | + | ||
| 122 | protected | 127 | protected |
| 123 | 128 | ||
| 124 | def problem_recache | 129 | def problem_recache |
| @@ -0,0 +1,32 @@ | @@ -0,0 +1,32 @@ | ||
| 1 | +class NoticeFingerprinter | ||
| 2 | + include Mongoid::Document | ||
| 3 | + include Mongoid::Timestamps | ||
| 4 | + | ||
| 5 | + field :error_class, default: true, type: Boolean | ||
| 6 | + field :message, default: true, type: Boolean | ||
| 7 | + field :backtrace_lines, default: -1, type: Integer | ||
| 8 | + field :component, default: true, type: Boolean | ||
| 9 | + field :action, default: true, type: Boolean | ||
| 10 | + field :environment_name, default: true, type: Boolean | ||
| 11 | + field :source, type: String | ||
| 12 | + | ||
| 13 | + embedded_in :app | ||
| 14 | + embedded_in :site_config | ||
| 15 | + | ||
| 16 | + def generate(api_key, notice, backtrace) | ||
| 17 | + material = [ api_key ] | ||
| 18 | + material << notice.error_class if error_class | ||
| 19 | + material << notice.filtered_message if message | ||
| 20 | + material << notice.component if component | ||
| 21 | + material << notice.action if action | ||
| 22 | + material << notice.environment_name if environment_name | ||
| 23 | + | ||
| 24 | + if backtrace_lines < 0 | ||
| 25 | + material << backtrace.lines | ||
| 26 | + else | ||
| 27 | + material << backtrace.lines.slice(0, backtrace_lines) | ||
| 28 | + end | ||
| 29 | + | ||
| 30 | + Digest::MD5.hexdigest(material.reduce('') { |c, m| c << m.to_s; c }) | ||
| 31 | + end | ||
| 32 | +end |
app/models/problem.rb
| @@ -142,16 +142,22 @@ class Problem | @@ -142,16 +142,22 @@ class Problem | ||
| 142 | { "$match" => { err_id: { "$in" => err_ids } } }, | 142 | { "$match" => { err_id: { "$in" => err_ids } } }, |
| 143 | { "$group" => { _id: "$#{v}", count: {"$sum" => 1} } } | 143 | { "$group" => { _id: "$#{v}", count: {"$sum" => 1} } } |
| 144 | ]).each do |agg| | 144 | ]).each do |agg| |
| 145 | - next if agg[:_id] == nil | ||
| 146 | - | ||
| 147 | - send(k)[Digest::MD5.hexdigest(agg[:_id])] = { | ||
| 148 | - value: agg[:_id], | ||
| 149 | - count: agg[:count] | 145 | + send(k)[Digest::MD5.hexdigest(agg[:_id] || 'N/A')] = { |
| 146 | + 'value' => agg[:_id] || 'N/A', | ||
| 147 | + 'count' => agg[:count] | ||
| 150 | } | 148 | } |
| 151 | end | 149 | end |
| 152 | end | 150 | end |
| 153 | 151 | ||
| 154 | - self.notices_count = Notice.where({ err_id: { "$in" => err_ids }}).count | 152 | + first_notice = notices.order_by([:created_at, :asc]).first |
| 153 | + last_notice = notices.order_by([:created_at, :desc]).first | ||
| 154 | + | ||
| 155 | + self.notices_count = notices.count | ||
| 156 | + self.first_notice_at = first_notice.created_at if first_notice | ||
| 157 | + self.message = first_notice.message if first_notice | ||
| 158 | + self.where = first_notice.where if first_notice | ||
| 159 | + self.last_notice_at = last_notice.created_at if last_notice | ||
| 160 | + | ||
| 155 | save | 161 | save |
| 156 | end | 162 | end |
| 157 | 163 |
| @@ -0,0 +1,36 @@ | @@ -0,0 +1,36 @@ | ||
| 1 | +class SiteConfig | ||
| 2 | + CONFIG_SOURCE_SITE = 'site'.freeze | ||
| 3 | + CONFIG_SOURCE_APP = 'app'.freeze | ||
| 4 | + | ||
| 5 | + include Mongoid::Document | ||
| 6 | + include Mongoid::Timestamps | ||
| 7 | + | ||
| 8 | + before_save :denormalize | ||
| 9 | + | ||
| 10 | + embeds_one :notice_fingerprinter, autobuild: true | ||
| 11 | + validates_associated :notice_fingerprinter | ||
| 12 | + accepts_nested_attributes_for :notice_fingerprinter | ||
| 13 | + | ||
| 14 | + # Get the one and only SiteConfig document | ||
| 15 | + def self.document | ||
| 16 | + first || create | ||
| 17 | + end | ||
| 18 | + | ||
| 19 | + # Denormalize SiteConfig onto individual apps so that this record doesn't | ||
| 20 | + # need to be accessed when inserting new error notices | ||
| 21 | + def denormalize | ||
| 22 | + notice_fingerprinter_attributes = notice_fingerprinter.attributes.tap do |attrs| | ||
| 23 | + attrs.delete('_id') | ||
| 24 | + attrs[:source] = :site | ||
| 25 | + end | ||
| 26 | + | ||
| 27 | + App.each do |app| | ||
| 28 | + f = app.notice_fingerprinter | ||
| 29 | + | ||
| 30 | + if !f || f.source == CONFIG_SOURCE_SITE | ||
| 31 | + app.update_attributes( | ||
| 32 | + notice_fingerprinter: notice_fingerprinter_attributes) | ||
| 33 | + end | ||
| 34 | + end | ||
| 35 | + end | ||
| 36 | +end |
app/views/shared/_navigation.html.haml
| @@ -13,4 +13,8 @@ | @@ -13,4 +13,8 @@ | ||
| 13 | = link_to users_path do | 13 | = link_to users_path do |
| 14 | %i.fa.fa-users | 14 | %i.fa.fa-users |
| 15 | = t('.users') | 15 | = t('.users') |
| 16 | + %li{:class => active_if_here(:site_config)} | ||
| 17 | + = link_to site_config_index_path do | ||
| 18 | + %i.fa.fa-wrench | ||
| 19 | + = t('.config') | ||
| 16 | %div.clear | 20 | %div.clear |
| @@ -0,0 +1,39 @@ | @@ -0,0 +1,39 @@ | ||
| 1 | +- content_for :title, 'Config' | ||
| 2 | + | ||
| 3 | +- content_for :action_bar, link_to('cancel', users_path, :class => 'button') | ||
| 4 | + | ||
| 5 | += form_for @config, url: site_config_index_path, method: 'put' do |f| | ||
| 6 | + = errors_for @config | ||
| 7 | + | ||
| 8 | + %fieldset | ||
| 9 | + %legend Notice Fingerprinter | ||
| 10 | + %p | ||
| 11 | + The notice fingerprinter governs how error notifications are grouped. | ||
| 12 | + Each item counts toward an error's uniqueness if enabled. | ||
| 13 | + | ||
| 14 | + = f.fields_for :notice_fingerprinter do |g| | ||
| 15 | + .checkbox | ||
| 16 | + = g.check_box :error_class | ||
| 17 | + = g.label :error_class, 'Error class' | ||
| 18 | + | ||
| 19 | + .checkbox | ||
| 20 | + = g.check_box :message | ||
| 21 | + = g.label :message, 'Error message' | ||
| 22 | + | ||
| 23 | + %div | ||
| 24 | + = g.label :backtrace_lines, 'Number of backtrace lines (-1 for unlimited)' | ||
| 25 | + = g.text_field :backtrace_lines | ||
| 26 | + | ||
| 27 | + .checkbox | ||
| 28 | + = g.check_box :component | ||
| 29 | + = g.label :component, 'Component (or controller)' | ||
| 30 | + | ||
| 31 | + .checkbox | ||
| 32 | + = g.check_box :action | ||
| 33 | + = g.label :action, 'Action' | ||
| 34 | + | ||
| 35 | + .checkbox | ||
| 36 | + = g.check_box :environment_name | ||
| 37 | + = g.label :environment_name, 'Environment name' | ||
| 38 | + | ||
| 39 | + %div.buttons= f.submit 'Update Config' |
config/routes.rb
| @@ -14,7 +14,14 @@ Rails.application.routes.draw do | @@ -14,7 +14,14 @@ Rails.application.routes.draw do | ||
| 14 | delete :unlink_github | 14 | delete :unlink_github |
| 15 | end | 15 | end |
| 16 | end | 16 | end |
| 17 | - resources :problems, :only => [:index] do | 17 | + |
| 18 | + resources :site_config, :only => [:index] do | ||
| 19 | + collection do | ||
| 20 | + put :update | ||
| 21 | + end | ||
| 22 | + end | ||
| 23 | + | ||
| 24 | + resources :problems, :only => [:index] do | ||
| 18 | collection do | 25 | collection do |
| 19 | post :destroy_several | 26 | post :destroy_several |
| 20 | post :resolve_several | 27 | post :resolve_several |
lib/tasks/errbit/database.rake
| 1 | -require 'digest/sha1' | ||
| 2 | - | ||
| 3 | namespace :errbit do | 1 | namespace :errbit do |
| 4 | - namespace :db do | ||
| 5 | - | ||
| 6 | - desc "Updates cached attributes on Problem" | ||
| 7 | - task :update_problem_attrs => :environment do | ||
| 8 | - puts "Updating problems" | ||
| 9 | - Problem.no_timeout.all.each do |problem| | ||
| 10 | - ProblemUpdaterCache.new(problem).update | ||
| 11 | - end | ||
| 12 | - end | ||
| 13 | - | ||
| 14 | - desc "Updates Problem#notices_count" | ||
| 15 | - task :update_notices_count => :environment do | ||
| 16 | - puts "Updating problem.notices_count" | ||
| 17 | - Problem.no_timeout.all.each do |pr| | ||
| 18 | - pr.update_attributes(:notices_count => pr.notices.count) | ||
| 19 | - end | ||
| 20 | - end | ||
| 21 | - | ||
| 22 | - desc "Delete resolved errors from the database. (Useful for limited heroku databases)" | ||
| 23 | - task :clear_resolved => :environment do | ||
| 24 | - require 'resolved_problem_clearer' | ||
| 25 | - puts "=== Cleared #{ResolvedProblemClearer.new.execute} resolved errors from the database." | ||
| 26 | - end | ||
| 27 | - | ||
| 28 | - desc "Regenerate fingerprints" | ||
| 29 | - task :regenerate_fingerprints => :environment do | ||
| 30 | - | ||
| 31 | - def normalize_backtrace(backtrace) | ||
| 32 | - backtrace[0...3].map do |trace| | ||
| 33 | - trace.merge 'method' => trace['method'].to_s.gsub(/[0-9_]{10,}+/, "__FRAGMENT__") | ||
| 34 | - end | ||
| 35 | - end | 2 | + desc "Updates cached attributes on Problem" |
| 3 | + task :problem_recache => :environment do | ||
| 4 | + ProblemRecacher.run | ||
| 5 | + end | ||
| 36 | 6 | ||
| 37 | - def fingerprint(source) | ||
| 38 | - Digest::SHA1.hexdigest(source.to_s) | ||
| 39 | - end | 7 | + desc "Delete resolved errors from the database. (Useful for limited heroku databases)" |
| 8 | + task :clear_resolved => :environment do | ||
| 9 | + require 'resolved_problem_clearer' | ||
| 10 | + puts "=== Cleared #{ResolvedProblemClearer.new.execute} resolved errors from the database." | ||
| 11 | + end | ||
| 40 | 12 | ||
| 41 | - puts "Regenerating Err fingerprints" | ||
| 42 | - Err.create_indexes | ||
| 43 | - Err.all.each do |err| | ||
| 44 | - next if err.notices.count == 0 | ||
| 45 | - source = { | ||
| 46 | - :backtrace => normalize_backtrace(err.notices.first.backtrace).to_s, | ||
| 47 | - :error_class => err.error_class, | ||
| 48 | - :component => err.component, | ||
| 49 | - :action => err.action, | ||
| 50 | - :environment => err.environment, | ||
| 51 | - :api_key => err.app.api_key | ||
| 52 | - } | ||
| 53 | - err.update_attributes(:fingerprint => fingerprint(source)) | ||
| 54 | - end | ||
| 55 | - end | 13 | + desc "Regenerate fingerprints" |
| 14 | + task :notice_refingerprint => :environment do | ||
| 15 | + NoticeRefingerprinter.run | ||
| 16 | + ProblemRecacher.run | ||
| 17 | + end | ||
| 56 | 18 | ||
| 57 | - desc "Remove notices in batch" | ||
| 58 | - task :notices_delete, [ :problem_id ] => [ :environment ] do | ||
| 59 | - BATCH_SIZE = 1000 | ||
| 60 | - if args[:problem_id] | ||
| 61 | - item_count = Problem.find(args[:problem_id]).notices.count | ||
| 62 | - removed_count = 0 | ||
| 63 | - puts "Notices to remove: #{item_count}" | ||
| 64 | - while item_count > 0 | ||
| 65 | - Problem.find(args[:problem_id]).notices.limit(BATCH_SIZE).each do |notice| | ||
| 66 | - notice.remove | ||
| 67 | - removed_count += 1 | ||
| 68 | - end | ||
| 69 | - item_count -= BATCH_SIZE | ||
| 70 | - puts "Removed #{removed_count} notices" | 19 | + desc "Remove notices in batch" |
| 20 | + task :notices_delete, [ :problem_id ] => [ :environment ] do | ||
| 21 | + BATCH_SIZE = 1000 | ||
| 22 | + if args[:problem_id] | ||
| 23 | + item_count = Problem.find(args[:problem_id]).notices.count | ||
| 24 | + removed_count = 0 | ||
| 25 | + puts "Notices to remove: #{item_count}" | ||
| 26 | + while item_count > 0 | ||
| 27 | + Problem.find(args[:problem_id]).notices.limit(BATCH_SIZE).each do |notice| | ||
| 28 | + notice.remove | ||
| 29 | + removed_count += 1 | ||
| 71 | end | 30 | end |
| 31 | + item_count -= BATCH_SIZE | ||
| 32 | + puts "Removed #{removed_count} notices" | ||
| 72 | end | 33 | end |
| 73 | end | 34 | end |
| 74 | end | 35 | end |
| @@ -0,0 +1,48 @@ | @@ -0,0 +1,48 @@ | ||
| 1 | +describe SiteConfigController, type: 'controller' do | ||
| 2 | + it_requires_admin_privileges for: { | ||
| 3 | + index: :get, | ||
| 4 | + update: :put | ||
| 5 | + } | ||
| 6 | + | ||
| 7 | + let(:admin) { Fabricate(:admin) } | ||
| 8 | + | ||
| 9 | + before { sign_in admin } | ||
| 10 | + | ||
| 11 | + describe '#index' do | ||
| 12 | + it 'has an index action' do | ||
| 13 | + get :index | ||
| 14 | + end | ||
| 15 | + end | ||
| 16 | + | ||
| 17 | + describe '#update' do | ||
| 18 | + it 'updates' do | ||
| 19 | + put :update, site_config: { | ||
| 20 | + notice_fingerprinter_attributes: { | ||
| 21 | + backtrace_lines: 3, | ||
| 22 | + environment_name: false | ||
| 23 | + } | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + fingerprinter = SiteConfig.document.notice_fingerprinter | ||
| 27 | + | ||
| 28 | + expect(fingerprinter.environment_name).to be false | ||
| 29 | + expect(fingerprinter.backtrace_lines).to be 3 | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + it 'redirects to the index' do | ||
| 33 | + put :update, site_config: { | ||
| 34 | + notice_fingerprinter_attributes: { error_class: true } | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + expect(response).to redirect_to(site_config_index_path) | ||
| 38 | + end | ||
| 39 | + | ||
| 40 | + it 'flashes a confirmation' do | ||
| 41 | + put :update, site_config: { | ||
| 42 | + notice_fingerprinter_attributes: { error_class: true } | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + expect(request.flash[:success]).to eq 'Updated site config' | ||
| 46 | + end | ||
| 47 | + end | ||
| 48 | +end |
spec/fabricators/err_fabricator.rb
| @@ -2,28 +2,3 @@ Fabricator :err do | @@ -2,28 +2,3 @@ Fabricator :err do | ||
| 2 | problem | 2 | problem |
| 3 | fingerprint 'some-finger-print' | 3 | fingerprint 'some-finger-print' |
| 4 | end | 4 | end |
| 5 | - | ||
| 6 | -Fabricator :notice do | ||
| 7 | - app | ||
| 8 | - err | ||
| 9 | - message 'FooError: Too Much Bar' | ||
| 10 | - backtrace | ||
| 11 | - server_environment { {'environment-name' => 'production'} } | ||
| 12 | - request {{ 'component' => 'foo', 'action' => 'bar' }} | ||
| 13 | - notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }} | ||
| 14 | - | ||
| 15 | - after_create do | ||
| 16 | - Problem.cache_notice(err.problem_id, self) | ||
| 17 | - problem.reload | ||
| 18 | - end | ||
| 19 | -end | ||
| 20 | - | ||
| 21 | -Fabricator :backtrace do | ||
| 22 | - lines(:count => 99) do | ||
| 23 | - { | ||
| 24 | - number: rand(999), | ||
| 25 | - file: "/path/to/file/#{SecureRandom.hex(4)}.rb", | ||
| 26 | - method: ActiveSupport.methods.shuffle.first | ||
| 27 | - } | ||
| 28 | - end | ||
| 29 | -end |
| @@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
| 1 | +Fabricator :notice do | ||
| 2 | + app | ||
| 3 | + err | ||
| 4 | + error_class 'FooError' | ||
| 5 | + message 'Too Much Bar' | ||
| 6 | + backtrace | ||
| 7 | + server_environment {{ 'environment-name' => 'production' }} | ||
| 8 | + request {{ 'component' => 'foo', 'action' => 'bar' }} | ||
| 9 | + notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }} | ||
| 10 | + | ||
| 11 | + after_create do | ||
| 12 | + Problem.cache_notice(err.problem_id, self) | ||
| 13 | + problem.reload | ||
| 14 | + end | ||
| 15 | +end |
| @@ -0,0 +1,52 @@ | @@ -0,0 +1,52 @@ | ||
| 1 | +describe NoticeRefingerprinter do | ||
| 2 | + let(:app) { Fabricate(:app) } | ||
| 3 | + let(:backtrace) do | ||
| 4 | + Fabricate(:backtrace) | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + before do | ||
| 8 | + notices | ||
| 9 | + end | ||
| 10 | + | ||
| 11 | + context 'identical backtraces' do | ||
| 12 | + let(:notices) do | ||
| 13 | + 5.times.map do | ||
| 14 | + notice = Fabricate(:notice, backtrace: backtrace, app: app) | ||
| 15 | + notice.save! | ||
| 16 | + notice | ||
| 17 | + end | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + it 'has only one err' do | ||
| 21 | + described_class.run | ||
| 22 | + expect(Err.count).to eq(1) | ||
| 23 | + end | ||
| 24 | + end | ||
| 25 | + | ||
| 26 | + context 'minor backtrace differences' do | ||
| 27 | + let(:notices) do | ||
| 28 | + line_numbers = [1, 1, 2, 2, 3] | ||
| 29 | + 5.times.map do | ||
| 30 | + b = backtrace.clone | ||
| 31 | + b.lines[5][:number] = line_numbers.shift | ||
| 32 | + b.save! | ||
| 33 | + notice = Fabricate(:notice, backtrace: b, app: app) | ||
| 34 | + notice.save! | ||
| 35 | + end | ||
| 36 | + end | ||
| 37 | + | ||
| 38 | + it 'has three errs with default fingerprinter' do | ||
| 39 | + described_class.run | ||
| 40 | + expect(Err.count).to eq(3) | ||
| 41 | + end | ||
| 42 | + | ||
| 43 | + it 'has one err when limiting backtrace line count' do | ||
| 44 | + fingerprinter = app.notice_fingerprinter | ||
| 45 | + fingerprinter.backtrace_lines = 4 | ||
| 46 | + fingerprinter.save! | ||
| 47 | + | ||
| 48 | + described_class.run | ||
| 49 | + expect(Err.count).to eq(1) | ||
| 50 | + end | ||
| 51 | + end | ||
| 52 | +end |
spec/interactors/problem_merge_spec.rb
| @@ -47,7 +47,7 @@ describe ProblemMerge do | @@ -47,7 +47,7 @@ describe ProblemMerge do | ||
| 47 | end | 47 | end |
| 48 | 48 | ||
| 49 | it 'update problem cache' do | 49 | it 'update problem cache' do |
| 50 | - expect(ProblemUpdaterCache).to receive(:new).with(problem).and_return(double(:update => true)) | 50 | + expect(problem).to receive(:recache) |
| 51 | problem_merge.merge | 51 | problem_merge.merge |
| 52 | end | 52 | end |
| 53 | 53 |
| @@ -0,0 +1,38 @@ | @@ -0,0 +1,38 @@ | ||
| 1 | +describe ProblemRecacher do | ||
| 2 | + let(:app) { Fabricate(:app) } | ||
| 3 | + let(:backtrace) do | ||
| 4 | + Fabricate(:backtrace) | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + before do | ||
| 8 | + notices | ||
| 9 | + | ||
| 10 | + NoticeRefingerprinter.run | ||
| 11 | + described_class.run | ||
| 12 | + end | ||
| 13 | + | ||
| 14 | + context 'minor backtrace differences' do | ||
| 15 | + let(:notices) do | ||
| 16 | + line_numbers = [1, 1, 2, 2, 3] | ||
| 17 | + 5.times.map do | ||
| 18 | + b = backtrace.clone | ||
| 19 | + b.lines[5][:number] = line_numbers.shift | ||
| 20 | + b.save! | ||
| 21 | + notice = Fabricate(:notice, backtrace: b, app: app) | ||
| 22 | + notice.save! | ||
| 23 | + notice | ||
| 24 | + end | ||
| 25 | + end | ||
| 26 | + | ||
| 27 | + it 'has three problems for the five notices' do | ||
| 28 | + expect(Notice.count).to eq(5) | ||
| 29 | + expect(Problem.count).to eq(3) | ||
| 30 | + end | ||
| 31 | + | ||
| 32 | + it 'the problems have the right cached attributes' do | ||
| 33 | + problem = notices.first.reload.problem | ||
| 34 | + | ||
| 35 | + expect(problem.notices_count).to eq(2) | ||
| 36 | + end | ||
| 37 | + end | ||
| 38 | +end |
spec/interactors/problem_updater_cache_spec.rb
| @@ -1,145 +0,0 @@ | @@ -1,145 +0,0 @@ | ||
| 1 | -describe ProblemUpdaterCache do | ||
| 2 | - let(:problem) { Fabricate(:problem_with_errs) } | ||
| 3 | - let(:first_errs) { problem.errs } | ||
| 4 | - let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | ||
| 5 | - | ||
| 6 | - describe "#update" do | ||
| 7 | - context "without notice pass args" do | ||
| 8 | - before do | ||
| 9 | - problem.update_attribute(:notices_count, 0) | ||
| 10 | - end | ||
| 11 | - | ||
| 12 | - it 'update the notice_count' do | ||
| 13 | - expect { | ||
| 14 | - ProblemUpdaterCache.new(problem).update | ||
| 15 | - }.to change{ | ||
| 16 | - problem.notices_count | ||
| 17 | - }.from(0).to(1) | ||
| 18 | - end | ||
| 19 | - | ||
| 20 | - context "with only one notice" do | ||
| 21 | - before do | ||
| 22 | - problem.update_attributes!(:messages => {}) | ||
| 23 | - ProblemUpdaterCache.new(problem).update | ||
| 24 | - end | ||
| 25 | - | ||
| 26 | - it 'update information about this notice' do | ||
| 27 | - expect(problem.message).to eq notice.message | ||
| 28 | - expect(problem.where).to eq notice.where | ||
| 29 | - end | ||
| 30 | - | ||
| 31 | - it 'update first_notice_at' do | ||
| 32 | - expect(problem.first_notice_at).to eq notice.reload.created_at | ||
| 33 | - end | ||
| 34 | - | ||
| 35 | - it 'update last_notice_at' do | ||
| 36 | - expect(problem.last_notice_at).to eq notice.reload.created_at | ||
| 37 | - end | ||
| 38 | - | ||
| 39 | - it 'update stats messages' do | ||
| 40 | - expect(problem.messages).to eq({ | ||
| 41 | - Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 1} | ||
| 42 | - }) | ||
| 43 | - end | ||
| 44 | - | ||
| 45 | - it 'update stats hosts' do | ||
| 46 | - expect(problem.hosts).to eq({ | ||
| 47 | - Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 1} | ||
| 48 | - }) | ||
| 49 | - end | ||
| 50 | - | ||
| 51 | - it 'update stats user_agents' do | ||
| 52 | - expect(problem.user_agents).to eq({ | ||
| 53 | - Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 1} | ||
| 54 | - }) | ||
| 55 | - end | ||
| 56 | - end | ||
| 57 | - | ||
| 58 | - context "with several notices" do | ||
| 59 | - let!(:notice_2) { Fabricate(:notice, :err => first_errs.first) } | ||
| 60 | - let!(:notice_3) { Fabricate(:notice, :err => first_errs.first) } | ||
| 61 | - before do | ||
| 62 | - problem.update_attributes!(:messages => {}) | ||
| 63 | - ProblemUpdaterCache.new(problem).update | ||
| 64 | - end | ||
| 65 | - it 'update information about this notice' do | ||
| 66 | - expect(problem.message).to eq notice.message | ||
| 67 | - expect(problem.where).to eq notice.where | ||
| 68 | - end | ||
| 69 | - | ||
| 70 | - it 'update first_notice_at' do | ||
| 71 | - expect(problem.first_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) | ||
| 72 | - end | ||
| 73 | - | ||
| 74 | - it 'update last_notice_at' do | ||
| 75 | - expect(problem.last_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) | ||
| 76 | - end | ||
| 77 | - | ||
| 78 | - it 'update stats messages' do | ||
| 79 | - expect(problem.messages).to eq({ | ||
| 80 | - Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 3} | ||
| 81 | - }) | ||
| 82 | - end | ||
| 83 | - | ||
| 84 | - it 'update stats hosts' do | ||
| 85 | - expect(problem.hosts).to eq({ | ||
| 86 | - Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 3} | ||
| 87 | - }) | ||
| 88 | - end | ||
| 89 | - | ||
| 90 | - it 'update stats user_agents' do | ||
| 91 | - expect(problem.user_agents).to eq({ | ||
| 92 | - Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 3} | ||
| 93 | - }) | ||
| 94 | - end | ||
| 95 | - | ||
| 96 | - end | ||
| 97 | - end | ||
| 98 | - | ||
| 99 | - context "with notice pass in args" do | ||
| 100 | - | ||
| 101 | - before do | ||
| 102 | - ProblemUpdaterCache.new(problem, notice).update | ||
| 103 | - end | ||
| 104 | - | ||
| 105 | - it 'increase notices_count by 1' do | ||
| 106 | - expect { | ||
| 107 | - ProblemUpdaterCache.new(problem, notice).update | ||
| 108 | - }.to change{ | ||
| 109 | - problem.notices_count | ||
| 110 | - }.by(1) | ||
| 111 | - end | ||
| 112 | - | ||
| 113 | - it 'update information about this notice' do | ||
| 114 | - expect(problem.message).to eq notice.message | ||
| 115 | - expect(problem.where).to eq notice.where | ||
| 116 | - end | ||
| 117 | - | ||
| 118 | - it 'update first_notice_at' do | ||
| 119 | - expect(problem.first_notice_at).to eq notice.created_at | ||
| 120 | - end | ||
| 121 | - | ||
| 122 | - it 'update last_notice_at' do | ||
| 123 | - expect(problem.last_notice_at).to eq notice.created_at | ||
| 124 | - end | ||
| 125 | - | ||
| 126 | - it 'inc stats messages' do | ||
| 127 | - expect(problem.messages).to eq({ | ||
| 128 | - Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 2} | ||
| 129 | - }) | ||
| 130 | - end | ||
| 131 | - | ||
| 132 | - it 'inc stats hosts' do | ||
| 133 | - expect(problem.hosts).to eq({ | ||
| 134 | - Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 2} | ||
| 135 | - }) | ||
| 136 | - end | ||
| 137 | - | ||
| 138 | - it 'inc stats user_agents' do | ||
| 139 | - expect(problem.user_agents).to eq({ | ||
| 140 | - Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 2} | ||
| 141 | - }) | ||
| 142 | - end | ||
| 143 | - end | ||
| 144 | - end | ||
| 145 | -end |
spec/models/error_report_spec.rb
| @@ -42,20 +42,6 @@ describe ErrorReport do | @@ -42,20 +42,6 @@ describe ErrorReport do | ||
| 42 | end | 42 | end |
| 43 | end | 43 | end |
| 44 | 44 | ||
| 45 | - describe "#fingerprint_strategy" do | ||
| 46 | - it "should be possible to change how fingerprints are generated" do | ||
| 47 | - def error_report.fingerprint_strategy | ||
| 48 | - Class.new do | ||
| 49 | - def self.generate(*args) | ||
| 50 | - 'fingerprintzzz' | ||
| 51 | - end | ||
| 52 | - end | ||
| 53 | - end | ||
| 54 | - | ||
| 55 | - expect(error_report.error.fingerprint).to eq('fingerprintzzz') | ||
| 56 | - end | ||
| 57 | - end | ||
| 58 | - | ||
| 59 | describe "#generate_notice!" do | 45 | describe "#generate_notice!" do |
| 60 | it "save a notice" do | 46 | it "save a notice" do |
| 61 | expect { | 47 | expect { |
spec/models/fingerprint/md5_spec.rb
| @@ -1,39 +0,0 @@ | @@ -1,39 +0,0 @@ | ||
| 1 | -describe Fingerprint::MD5, type: 'model' do | ||
| 2 | - context 'being created' do | ||
| 3 | - let(:backtrace) do | ||
| 4 | - Backtrace.find_or_create([ | ||
| 5 | - { | ||
| 6 | - "number"=>"17", | ||
| 7 | - "file"=>"[GEM_ROOT]/gems/activesupport/lib/active_support/callbacks.rb", | ||
| 8 | - "method"=>"_run__2497084960985961383__process_action__2062871603614456254__callbacks" | ||
| 9 | - } | ||
| 10 | - ]) | ||
| 11 | - end | ||
| 12 | - let(:notice1) { Fabricate.build(:notice, :backtrace => backtrace) } | ||
| 13 | - let(:notice2) { Fabricate.build(:notice, :backtrace => backtrace_2) } | ||
| 14 | - | ||
| 15 | - context "with same backtrace" do | ||
| 16 | - let(:backtrace_2) do | ||
| 17 | - new_lines = backtrace.lines.dup | ||
| 18 | - new_lines.last[:method] = '_run__FRAGMENT__process_action__FRAGMENT__callbacks' | ||
| 19 | - Backtrace.find_or_create(new_lines) | ||
| 20 | - end | ||
| 21 | - | ||
| 22 | - it "normalizes the fingerprint of generated methods" do | ||
| 23 | - expect(Fingerprint::MD5.generate(notice1, "api key")).to eql Fingerprint::MD5.generate(notice2, "api key") | ||
| 24 | - end | ||
| 25 | - end | ||
| 26 | - | ||
| 27 | - context "with same backtrace where FRAGMENT has not been extracted" do | ||
| 28 | - let(:backtrace_2) do | ||
| 29 | - new_lines = backtrace.lines.dup | ||
| 30 | - new_lines.last[:method] = '_run__998857585768765__process_action__1231231312321313__callbacks' | ||
| 31 | - Backtrace.find_or_create(new_lines) | ||
| 32 | - end | ||
| 33 | - | ||
| 34 | - it "normalizes the fingerprint of generated methods" do | ||
| 35 | - expect(Fingerprint::MD5.generate(notice1, "api key")).to eql Fingerprint::MD5.generate(notice2, "api key") | ||
| 36 | - end | ||
| 37 | - end | ||
| 38 | - end | ||
| 39 | -end |
spec/models/fingerprint/sha1_spec.rb
| @@ -1,79 +0,0 @@ | @@ -1,79 +0,0 @@ | ||
| 1 | -describe Fingerprint::Sha1, type: 'model' do | ||
| 2 | - context '#generate' do | ||
| 3 | - let(:backtrace) { | ||
| 4 | - Backtrace.find_or_create([ | ||
| 5 | - {"number"=>"425", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"_run__2115867319__process_action__262109504__callbacks"}, | ||
| 6 | - {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"send"}, | ||
| 7 | - {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"_run_process_action_callbacks"} | ||
| 8 | - ]) | ||
| 9 | - } | ||
| 10 | - let(:notice1) { Fabricate.build(:notice, :backtrace => backtrace) } | ||
| 11 | - let(:notice2) { Fabricate.build(:notice, :backtrace => backtrace_2) } | ||
| 12 | - | ||
| 13 | - context "with same backtrace" do | ||
| 14 | - let(:backtrace_2) { backtrace } | ||
| 15 | - it 'should create the same fingerprint for two notices' do | ||
| 16 | - expect(Fingerprint::Sha1.generate(notice1, "api key")).to eq Fingerprint::Sha1.generate(notice2, "api key") | ||
| 17 | - end | ||
| 18 | - end | ||
| 19 | - | ||
| 20 | - context "with different backtrace with only last line change" do | ||
| 21 | - let(:backtrace_2) { | ||
| 22 | - new_lines = backtrace.lines.dup | ||
| 23 | - new_lines.last[:number] = 401 | ||
| 24 | - Backtrace.find_or_create backtrace.lines | ||
| 25 | - } | ||
| 26 | - it 'should not same fingerprint' do | ||
| 27 | - expect( | ||
| 28 | - Fingerprint::Sha1.generate(notice1, "api key") | ||
| 29 | - ).not_to eql Fingerprint::Sha1.generate(notice2, "api key") | ||
| 30 | - end | ||
| 31 | - end | ||
| 32 | - | ||
| 33 | - context 'with messages differing in object string memory addresses' do | ||
| 34 | - let(:backtrace_2) { backtrace } | ||
| 35 | - | ||
| 36 | - before do | ||
| 37 | - notice1.message = "NoMethodError: undefined method `foo' for #<ActiveSupport::HashWithIndifferentAccess:0x007f6bfe3287e8>" | ||
| 38 | - notice2.message = "NoMethodError: undefined method `foo' for #<ActiveSupport::HashWithIndifferentAccess:0x007f6bfd9f5338>" | ||
| 39 | - end | ||
| 40 | - | ||
| 41 | - its 'fingerprints should be equal' do | ||
| 42 | - expect(Fingerprint::Sha1.generate(notice1, 'api key')).to eq Fingerprint::Sha1.generate(notice2, 'api key') | ||
| 43 | - end | ||
| 44 | - end | ||
| 45 | - | ||
| 46 | - context 'with different messages at same stacktrace' do | ||
| 47 | - let(:backtrace_2) { backtrace } | ||
| 48 | - | ||
| 49 | - before do | ||
| 50 | - notice1.message = "NoMethodError: undefined method `bar' for #<ActiveSupport::HashWithIndifferentAccess:0x007f6bfe3287e8>" | ||
| 51 | - notice2.message = "NoMethodError: undefined method `bar' for nil:NilClass" | ||
| 52 | - end | ||
| 53 | - | ||
| 54 | - its 'fingerprints should not be equal' do | ||
| 55 | - expect(Fingerprint::Sha1.generate(notice1, 'api key')).to_not eq Fingerprint::Sha1.generate(notice2, 'api key') | ||
| 56 | - end | ||
| 57 | - end | ||
| 58 | - end | ||
| 59 | - | ||
| 60 | - describe '#unified_message' do | ||
| 61 | - subject{ Fingerprint::Sha1.new(double('notice', message: message), 'api key').unified_message } | ||
| 62 | - | ||
| 63 | - context "full error message" do | ||
| 64 | - let(:message) { "NoMethodError: undefined method `foo' for #<ActiveSupport::HashWithIndifferentAccess:0x007f6bfe3287e8>" } | ||
| 65 | - | ||
| 66 | - it 'removes memory address from object strings' do | ||
| 67 | - is_expected.to eq "NoMethodError: undefined method `foo' for #<ActiveSupport::HashWithIndifferentAccess>" | ||
| 68 | - end | ||
| 69 | - end | ||
| 70 | - | ||
| 71 | - context "multiple object strings in message" do | ||
| 72 | - let(:message) { "#<ActiveSupport::HashWithIndifferentAccess:0x007f6bfe3287e8> #<Object:0x007fa2b33d9458>" } | ||
| 73 | - | ||
| 74 | - it 'removes memory addresses globally' do | ||
| 75 | - is_expected.to eq "#<ActiveSupport::HashWithIndifferentAccess> #<Object>" | ||
| 76 | - end | ||
| 77 | - end | ||
| 78 | - end | ||
| 79 | -end |
| @@ -0,0 +1,64 @@ | @@ -0,0 +1,64 @@ | ||
| 1 | +describe NoticeFingerprinter, type: 'model' do | ||
| 2 | + let(:fingerprinter) { described_class.new } | ||
| 3 | + let(:notice) { Fabricate(:notice) } | ||
| 4 | + let(:backtrace) { Fabricate(:backtrace) } | ||
| 5 | + | ||
| 6 | + context '#generate' do | ||
| 7 | + it 'generates the same fingerprint for the same notice' do | ||
| 8 | + f1 = fingerprinter.generate('123', notice, backtrace) | ||
| 9 | + f2 = fingerprinter.generate('123', notice, backtrace) | ||
| 10 | + expect(f1).to eq(f2) | ||
| 11 | + end | ||
| 12 | + | ||
| 13 | + %w(error_class message component action environment_name).each do |i| | ||
| 14 | + it "affects the fingerprint when #{i} is false" do | ||
| 15 | + f1 = fingerprinter.generate('123', notice, backtrace) | ||
| 16 | + f2 = fingerprinter.generate('123', notice, backtrace) | ||
| 17 | + | ||
| 18 | + fingerprinter.send((i << '=').to_sym, false) | ||
| 19 | + f3 = fingerprinter.generate('123', notice, backtrace) | ||
| 20 | + | ||
| 21 | + expect(f1).to eq(f2) | ||
| 22 | + expect(f1).to_not eq(f3) | ||
| 23 | + end | ||
| 24 | + end | ||
| 25 | + | ||
| 26 | + it 'affects the fingerprint with different backtrace_lines config' do | ||
| 27 | + f1 = fingerprinter.generate('123', notice, backtrace) | ||
| 28 | + f2 = fingerprinter.generate('123', notice, backtrace) | ||
| 29 | + | ||
| 30 | + fingerprinter.backtrace_lines = 2 | ||
| 31 | + f3 = fingerprinter.generate('123', notice, backtrace) | ||
| 32 | + | ||
| 33 | + expect(f1).to eq(f2) | ||
| 34 | + expect(f1).to_not eq(f3) | ||
| 35 | + end | ||
| 36 | + | ||
| 37 | + context 'two backtraces have the same first two lines' do | ||
| 38 | + let(:backtrace1) { Fabricate(:backtrace) } | ||
| 39 | + let(:backtrace2) { Fabricate(:backtrace) } | ||
| 40 | + | ||
| 41 | + before do | ||
| 42 | + backtrace1.lines[0] = backtrace2.lines[0] | ||
| 43 | + backtrace1.lines[1] = backtrace2.lines[1] | ||
| 44 | + backtrace1.lines[2] = { number: 1, file: 'a', method: :b } | ||
| 45 | + end | ||
| 46 | + | ||
| 47 | + it 'has the same fingerprint when only considering two lines' do | ||
| 48 | + fingerprinter.backtrace_lines = 2 | ||
| 49 | + f1 = fingerprinter.generate('123', notice, backtrace1) | ||
| 50 | + f2 = fingerprinter.generate('123', notice, backtrace2) | ||
| 51 | + | ||
| 52 | + expect(f1).to eq(f2) | ||
| 53 | + end | ||
| 54 | + | ||
| 55 | + it 'has a different fingerprint when considering three lines' do | ||
| 56 | + fingerprinter.backtrace_lines = 3 | ||
| 57 | + f1 = fingerprinter.generate('123', notice, backtrace1) | ||
| 58 | + f2 = fingerprinter.generate('123', notice, backtrace2) | ||
| 59 | + | ||
| 60 | + expect(f1).to_not eq(f2) | ||
| 61 | + end | ||
| 62 | + end | ||
| 63 | + end | ||
| 64 | +end |
spec/models/problem_spec.rb
| @@ -397,4 +397,100 @@ describe Problem, type: 'model' do | @@ -397,4 +397,100 @@ describe Problem, type: 'model' do | ||
| 397 | end | 397 | end |
| 398 | end | 398 | end |
| 399 | end | 399 | end |
| 400 | + | ||
| 401 | + describe '#recache' do | ||
| 402 | + let(:problem) { Fabricate(:problem_with_errs) } | ||
| 403 | + let(:first_errs) { problem.errs } | ||
| 404 | + let!(:notice) { Fabricate(:notice, :err => first_errs.first) } | ||
| 405 | + | ||
| 406 | + before do | ||
| 407 | + problem.update_attribute(:notices_count, 0) | ||
| 408 | + end | ||
| 409 | + | ||
| 410 | + it 'update the notice_count' do | ||
| 411 | + expect { | ||
| 412 | + problem.recache | ||
| 413 | + }.to change{ | ||
| 414 | + problem.notices_count | ||
| 415 | + }.from(0).to(1) | ||
| 416 | + end | ||
| 417 | + | ||
| 418 | + context "with only one notice" do | ||
| 419 | + before do | ||
| 420 | + problem.update_attributes!(:messages => {}) | ||
| 421 | + problem.recache | ||
| 422 | + end | ||
| 423 | + | ||
| 424 | + it 'update information about this notice' do | ||
| 425 | + expect(problem.message).to eq notice.message | ||
| 426 | + expect(problem.where).to eq notice.where | ||
| 427 | + end | ||
| 428 | + | ||
| 429 | + it 'update first_notice_at' do | ||
| 430 | + expect(problem.first_notice_at).to eq notice.reload.created_at | ||
| 431 | + end | ||
| 432 | + | ||
| 433 | + it 'update last_notice_at' do | ||
| 434 | + expect(problem.last_notice_at).to eq notice.reload.created_at | ||
| 435 | + end | ||
| 436 | + | ||
| 437 | + it 'update stats messages' do | ||
| 438 | + expect(problem.messages).to eq({ | ||
| 439 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 1} | ||
| 440 | + }) | ||
| 441 | + end | ||
| 442 | + | ||
| 443 | + it 'update stats hosts' do | ||
| 444 | + expect(problem.hosts).to eq({ | ||
| 445 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 1} | ||
| 446 | + }) | ||
| 447 | + end | ||
| 448 | + | ||
| 449 | + it 'update stats user_agents' do | ||
| 450 | + expect(problem.user_agents).to eq({ | ||
| 451 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 1} | ||
| 452 | + }) | ||
| 453 | + end | ||
| 454 | + end | ||
| 455 | + | ||
| 456 | + context "with several notices" do | ||
| 457 | + let!(:notice_2) { Fabricate(:notice, :err => first_errs.first) } | ||
| 458 | + let!(:notice_3) { Fabricate(:notice, :err => first_errs.first) } | ||
| 459 | + before do | ||
| 460 | + problem.update_attributes!(:messages => {}) | ||
| 461 | + problem.recache | ||
| 462 | + end | ||
| 463 | + | ||
| 464 | + it 'update information about this notice' do | ||
| 465 | + expect(problem.message).to eq notice.message | ||
| 466 | + expect(problem.where).to eq notice.where | ||
| 467 | + end | ||
| 468 | + | ||
| 469 | + it 'update first_notice_at' do | ||
| 470 | + expect(problem.first_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) | ||
| 471 | + end | ||
| 472 | + | ||
| 473 | + it 'update last_notice_at' do | ||
| 474 | + expect(problem.last_notice_at.to_i).to be_within(2).of(notice.created_at.to_i) | ||
| 475 | + end | ||
| 476 | + | ||
| 477 | + it 'update stats messages' do | ||
| 478 | + expect(problem.messages).to eq({ | ||
| 479 | + Digest::MD5.hexdigest(notice.message) => {'value' => notice.message, 'count' => 3} | ||
| 480 | + }) | ||
| 481 | + end | ||
| 482 | + | ||
| 483 | + it 'update stats hosts' do | ||
| 484 | + expect(problem.hosts).to eq({ | ||
| 485 | + Digest::MD5.hexdigest(notice.host) => {'value' => notice.host, 'count' => 3} | ||
| 486 | + }) | ||
| 487 | + end | ||
| 488 | + | ||
| 489 | + it 'update stats user_agents' do | ||
| 490 | + expect(problem.user_agents).to eq({ | ||
| 491 | + Digest::MD5.hexdigest(notice.user_agent_string) => {'value' => notice.user_agent_string, 'count' => 3} | ||
| 492 | + }) | ||
| 493 | + end | ||
| 494 | + end | ||
| 495 | + end | ||
| 400 | end | 496 | end |