Commit 4cf6b44fd88178c7c46650547b951daccf25c773

Authored by Stephen Crosby
2 parents 24db1d16 ddb32e10
Exists in master and in 1 other branch production

Merge pull request #946 from stevecrozz/configurable_backtraces

configurable notice fingerprinting
@@ -20,3 +20,4 @@ bundle @@ -20,3 +20,4 @@ bundle
20 coverage 20 coverage
21 *# 21 *#
22 .ruby-version 22 .ruby-version
  23 +NOTES
@@ -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.
app/controllers/site_config_controller.rb 0 → 100644
@@ -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
app/interactors/notice_refingerprinter.rb 0 → 100644
@@ -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
app/interactors/problem_recacher.rb 0 → 100644
@@ -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
app/models/notice_fingerprinter.rb 0 → 100644
@@ -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
app/models/site_config.rb 0 → 100644
@@ -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
app/views/site_config/index.html.haml 0 → 100644
@@ -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
spec/controllers/site_config_controller_spec.rb 0 → 100644
@@ -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/backtrace_fabricator.rb 0 → 100644
@@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
  1 +Fabricator :backtrace do
  2 + lines(:count => 99) do
  3 + {
  4 + number: rand(999),
  5 + file: "/path/to/file/#{SecureRandom.hex(4)}.rb",
  6 + method: ActiveSupport.methods.shuffle.first
  7 + }
  8 + end
  9 +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  
spec/fabricators/notice_fabricator.rb 0 → 100644
@@ -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
spec/interactors/notice_refingerprinter_spec.rb 0 → 100644
@@ -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
spec/interactors/problem_recacher_spec.rb 0 → 100644
@@ -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  
spec/models/notice_fingerprinter_spec.rb 0 → 100644
@@ -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: &#39;model&#39; do @@ -397,4 +397,100 @@ describe Problem, type: &#39;model&#39; 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