Commit 740b7309749a19d69b0098f863c4ca45820f43a8

Authored by Stephen Crosby
2 parents bdf4aa57 17ac3ee5
Exists in master and in 1 other branch production

Merge pull request #917 from stevecrozz/mongoid5

Mongoid5 and performance improvements
Showing 53 changed files with 871 additions and 754 deletions   Show diff stats
@@ -15,6 +15,8 @@ ERRBIT_PER_APP_EMAIL_AT_NOTICES=false @@ -15,6 +15,8 @@ ERRBIT_PER_APP_EMAIL_AT_NOTICES=false
15 ERRBIT_NOTIFY_AT_NOTICES='[0]' 15 ERRBIT_NOTIFY_AT_NOTICES='[0]'
16 ERRBIT_PER_APP_NOTIFY_AT_NOTICES=false 16 ERRBIT_PER_APP_NOTIFY_AT_NOTICES=false
17 MONGO_URL='mongodb://localhost' 17 MONGO_URL='mongodb://localhost'
  18 +ERRBIT_LOG_LEVEL=info
  19 +ERRBIT_LOG_LOCATION=STDOUT
18 GITHUB_URL='https://github.com' 20 GITHUB_URL='https://github.com'
19 GITHUB_AUTHENTICATION=true 21 GITHUB_AUTHENTICATION=true
20 GITHUB_ACCESS_SCOPE='[repo]' 22 GITHUB_ACCESS_SCOPE='[repo]'
@@ -9,8 +9,12 @@ env: @@ -9,8 +9,12 @@ env:
9 - RAILS_ENV=test COVERAGE=true JRUBY_OPTS=--debug 9 - RAILS_ENV=test COVERAGE=true JRUBY_OPTS=--debug
10 sudo: false 10 sudo: false
11 cache: bundler 11 cache: bundler
12 -services: mongodb  
13 -before_script: bundle exec rake errbit:bootstrap 12 +before_script:
  13 + - wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-2.6.10.tgz -O /tmp/mongodb.tgz
  14 + - tar -xvf /tmp/mongodb.tgz
  15 + - mkdir /tmp/data
  16 + - ${PWD}/mongodb-linux-x86_64-2.6.10/bin/mongod --dbpath /tmp/data --bind_ip 127.0.0.1 --auth &> /dev/null &
  17 + - bundle exec rake errbit:bootstrap
14 script: bundle exec rspec 18 script: bundle exec rspec
15 matrix: 19 matrix:
16 allow_failures: 20 allow_failures:
@@ -8,10 +8,8 @@ gem 'actionmailer', RAILS_VERSION @@ -8,10 +8,8 @@ gem 'actionmailer', RAILS_VERSION
8 gem 'actionpack', RAILS_VERSION 8 gem 'actionpack', RAILS_VERSION
9 gem 'railties', RAILS_VERSION 9 gem 'railties', RAILS_VERSION
10 10
11 -gem 'moped', '~> 2.0.2'  
12 -gem 'mongoid', '~> 4.0.0' 11 +gem 'mongoid', '5.0.0.beta'
13 12
14 -gem 'mongoid_rails_migrations'  
15 gem 'devise' 13 gem 'devise'
16 gem 'haml' 14 gem 'haml'
17 gem 'htmlentities' 15 gem 'htmlentities'
@@ -78,7 +76,7 @@ group :development do @@ -78,7 +76,7 @@ group :development do
78 end 76 end
79 77
80 group :test do 78 group :test do
81 - gem 'rspec' 79 + gem 'rspec', '~> 3.3'
82 gem 'rspec-rails', '~> 3.0', require: false 80 gem 'rspec-rails', '~> 3.0', require: false
83 gem 'rspec-activemodel-mocks' 81 gem 'rspec-activemodel-mocks'
84 gem 'rspec-its' 82 gem 'rspec-its'
@@ -87,7 +85,6 @@ group :test do @@ -87,7 +85,6 @@ group :test do
87 gem 'capybara' 85 gem 'capybara'
88 gem 'poltergeist' 86 gem 'poltergeist'
89 gem 'launchy' 87 gem 'launchy'
90 - gem 'database_cleaner'  
91 gem 'email_spec' 88 gem 'email_spec'
92 gem 'timecop' 89 gem 'timecop'
93 gem 'test-unit', require: 'test/unit' 90 gem 'test-unit', require: 'test/unit'
@@ -44,8 +44,8 @@ GEM @@ -44,8 +44,8 @@ GEM
44 rack (>= 0.9.0) 44 rack (>= 0.9.0)
45 binding_of_caller (0.7.2) 45 binding_of_caller (0.7.2)
46 debug_inspector (>= 0.0.1) 46 debug_inspector (>= 0.0.1)
47 - bson (3.1.1)  
48 - bson (3.1.1-java) 47 + bson (3.2.0)
  48 + bson (3.2.0-java)
49 builder (3.2.2) 49 builder (3.2.2)
50 byebug (4.0.5) 50 byebug (4.0.5)
51 columnize (= 0.9.0) 51 columnize (= 0.9.0)
@@ -81,7 +81,6 @@ GEM @@ -81,7 +81,6 @@ GEM
81 coffee-script-source (1.9.1.1) 81 coffee-script-source (1.9.1.1)
82 colorize (0.7.7) 82 colorize (0.7.7)
83 columnize (0.9.0) 83 columnize (0.9.0)
84 - connection_pool (2.2.0)  
85 coveralls (0.8.2) 84 coveralls (0.8.2)
86 json (~> 1.8) 85 json (~> 1.8)
87 rest-client (>= 1.6.8, < 2) 86 rest-client (>= 1.6.8, < 2)
@@ -90,7 +89,6 @@ GEM @@ -90,7 +89,6 @@ GEM
90 thor (~> 0.19.1) 89 thor (~> 0.19.1)
91 css_parser (1.3.6) 90 css_parser (1.3.6)
92 addressable 91 addressable
93 - database_cleaner (1.4.1)  
94 debug_inspector (0.0.2) 92 debug_inspector (0.0.2)
95 decent_exposure (2.3.2) 93 decent_exposure (2.3.2)
96 devise (3.5.1) 94 devise (3.5.1)
@@ -175,25 +173,18 @@ GEM @@ -175,25 +173,18 @@ GEM
175 mimemagic (0.3.0) 173 mimemagic (0.3.0)
176 mini_portile (0.6.2) 174 mini_portile (0.6.2)
177 minitest (5.7.0) 175 minitest (5.7.0)
178 - mongoid (4.0.2) 176 + mongo (2.1.0.beta)
  177 + bson (~> 3.0)
  178 + mongoid (5.0.0.beta)
179 activemodel (~> 4.0) 179 activemodel (~> 4.0)
180 - moped (~> 2.0.0) 180 + mongo (= 2.1.0.beta)
181 origin (~> 2.1) 181 origin (~> 2.1)
182 tzinfo (>= 0.3.37) 182 tzinfo (>= 0.3.37)
183 - mongoid-rspec (2.2.0)  
184 - mongoid (~> 4.0.0) 183 + mongoid-rspec (1.10.0)
  184 + mongoid (>= 3.0.1)
185 rake 185 rake
186 - rspec (~> 3.1)  
187 - mongoid_rails_migrations (1.0.1)  
188 - activesupport (>= 3.2.0)  
189 - bundler (>= 1.0.0)  
190 - rails (>= 3.2.0)  
191 - railties (>= 3.2.0)  
192 - moped (2.0.6)  
193 - bson (~> 3.0)  
194 - connection_pool (~> 2.0)  
195 - optionable (~> 0.2.0)  
196 - multi_json (1.11.1) 186 + rspec (>= 2.14)
  187 + multi_json (1.11.2)
197 multi_xml (0.5.5) 188 multi_xml (0.5.5)
198 multipart-post (2.0.0) 189 multipart-post (2.0.0)
199 net-scp (1.2.1) 190 net-scp (1.2.1)
@@ -363,7 +354,7 @@ GEM @@ -363,7 +354,7 @@ GEM
363 thread_safe (0.3.5-java) 354 thread_safe (0.3.5-java)
364 tilt (1.4.1) 355 tilt (1.4.1)
365 timecop (0.7.4) 356 timecop (0.7.4)
366 - tins (1.5.4) 357 + tins (1.6.0)
367 tzinfo (1.2.2) 358 tzinfo (1.2.2)
368 thread_safe (~> 0.1) 359 thread_safe (~> 0.1)
369 uglifier (2.7.1) 360 uglifier (2.7.1)
@@ -410,7 +401,6 @@ DEPENDENCIES @@ -410,7 +401,6 @@ DEPENDENCIES
410 capybara 401 capybara
411 coffee-rails 402 coffee-rails
412 coveralls 403 coveralls
413 - database_cleaner  
414 decent_exposure 404 decent_exposure
415 devise 405 devise
416 dotenv-rails 406 dotenv-rails
@@ -431,10 +421,8 @@ DEPENDENCIES @@ -431,10 +421,8 @@ DEPENDENCIES
431 kaminari (>= 0.14.1) 421 kaminari (>= 0.14.1)
432 launchy 422 launchy
433 meta_request 423 meta_request
434 - mongoid (~> 4.0.0) 424 + mongoid (= 5.0.0.beta)
435 mongoid-rspec 425 mongoid-rspec
436 - mongoid_rails_migrations  
437 - moped (~> 2.0.2)  
438 omniauth-github 426 omniauth-github
439 pjax_rails 427 pjax_rails
440 poltergeist 428 poltergeist
@@ -447,7 +435,7 @@ DEPENDENCIES @@ -447,7 +435,7 @@ DEPENDENCIES
447 rails_autolink 435 rails_autolink
448 railties (~> 4.1.11) 436 railties (~> 4.1.11)
449 ri_cal 437 ri_cal
450 - rspec 438 + rspec (~> 3.3)
451 rspec-activemodel-mocks 439 rspec-activemodel-mocks
452 rspec-its 440 rspec-its
453 rspec-rails (~> 3.0) 441 rspec-rails (~> 3.0)
@@ -71,7 +71,7 @@ updates and notifications. @@ -71,7 +71,7 @@ updates and notifications.
71 The list of requirements to install Errbit are: 71 The list of requirements to install Errbit are:
72 72
73 * Ruby 2.1.0 or higher 73 * Ruby 2.1.0 or higher
74 -* MongoDB 2.2.0 or higher 74 +* MongoDB 2.6.0 or higher
75 75
76 Installation 76 Installation
77 ------------ 77 ------------
@@ -188,6 +188,8 @@ When upgrading Errbit, please run: @@ -188,6 +188,8 @@ When upgrading Errbit, please run:
188 git pull origin master # assuming origin is the github.com/errbit/errbit repo 188 git pull origin master # assuming origin is the github.com/errbit/errbit repo
189 bundle install 189 bundle install
190 rake db:migrate 190 rake db:migrate
  191 +rake db:mongoid:create_indexes
  192 +rake db:mongoid:remove_undefined_indexes
191 rake assets:precompile 193 rake assets:precompile
192 ``` 194 ```
193 195
app/controllers/notices_controller.rb
@@ -2,7 +2,8 @@ class NoticesController &lt; ApplicationController @@ -2,7 +2,8 @@ class NoticesController &lt; ApplicationController
2 2
3 class ParamsError < StandardError; end 3 class ParamsError < StandardError; end
4 4
5 - skip_before_action :authenticate_user!, :only => :create 5 + skip_before_action :authenticate_user!, only: :create
  6 + skip_before_filter :verify_authenticity_token, only: :create
6 7
7 rescue_from ParamsError, :with => :bad_params 8 rescue_from ParamsError, :with => :bad_params
8 9
app/controllers/problems_controller.rb
@@ -22,7 +22,7 @@ class ProblemsController &lt; ApplicationController @@ -22,7 +22,7 @@ class ProblemsController &lt; ApplicationController
22 } 22 }
23 23
24 expose(:problem) { 24 expose(:problem) {
25 - app.problems.find(params[:id]) 25 + ProblemDecorator.new app.problems.find(params[:id])
26 } 26 }
27 27
28 expose(:all_errs) { 28 expose(:all_errs) {
@@ -50,8 +50,9 @@ class ProblemsController &lt; ApplicationController @@ -50,8 +50,9 @@ class ProblemsController &lt; ApplicationController
50 def index; end 50 def index; end
51 51
52 def show 52 def show
53 - @notices = problem.notices.reverse_ordered.page(params[:notice]).per(1)  
54 - @notice = @notices.first 53 + @notices = problem.object.notices.reverse_ordered
  54 + .page(params[:notice]).per(1)
  55 + @notice = NoticeDecorator.new @notices.first
55 @comment = Comment.new 56 @comment = Comment.new
56 end 57 end
57 58
app/decorators/backtrace_decorator.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +class BacktraceDecorator < Draper::Decorator
  2 + def lines
  3 + @lines ||= object.lines.map { |line| BacktraceLineDecorator.new line }
  4 + end
  5 +end
app/decorators/backtrace_line_decorator.rb 0 → 100644
@@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
  1 +class BacktraceLineDecorator < Draper::Decorator
  2 + EMPTY_STRING = ''.freeze
  3 +
  4 + def in_app?
  5 + object[:file].match Backtrace::IN_APP_PATH
  6 + end
  7 +
  8 + def number
  9 + object[:number]
  10 + end
  11 +
  12 + def column
  13 + object[:column]
  14 + end
  15 +
  16 + def file
  17 + object[:file]
  18 + end
  19 +
  20 + def method
  21 + object[:method]
  22 + end
  23 +
  24 + def file_relative
  25 + file.to_s.sub(Backtrace::IN_APP_PATH, EMPTY_STRING)
  26 + end
  27 +
  28 + def file_name
  29 + File.basename file
  30 + end
  31 +
  32 + def to_s
  33 + column = object.try(:[], :column)
  34 + "#{file_relative}:#{number}#{column.present? ? ":#{column}" : ''}"
  35 + end
  36 +
  37 + def link_to_source_file(app, &block)
  38 + text = h.capture_haml(&block)
  39 + link_to_in_app_source_file(app, text) || text
  40 + end
  41 +
  42 + def path
  43 + File.dirname(object[:file]).gsub(/^\.$/, '') + "/"
  44 + end
  45 +
  46 + def decorated_path
  47 + file_relative.sub(Backtrace::GEMS_PATH, "<strong>\\1</strong>")
  48 + end
  49 +
  50 + private
  51 + def link_to_in_app_source_file(app, text)
  52 + return unless in_app?
  53 + if file_name =~ /\.js$/
  54 + link_to_hosted_javascript(app, text)
  55 + else
  56 + link_to_repo_source_file(app, text) ||
  57 + link_to_issue_tracker_file(app, text)
  58 + end
  59 + end
  60 +
  61 + def link_to_repo_source_file(app, text)
  62 + link_to_github(app, text) || link_to_bitbucket(app, text)
  63 + end
  64 +
  65 + def link_to_hosted_javascript(app, text)
  66 + if app.asset_host?
  67 + h.link_to(text, "#{app.asset_host}/#{file_relative}", :target => '_blank')
  68 + end
  69 + end
  70 +
  71 + def link_to_github(app, text = nil)
  72 + return unless app.github_repo?
  73 + href = "%s#L%s" % [app.github_url_to_file(decorated_path + file_name), number]
  74 + h.link_to(text || file_name, href, :target => '_blank')
  75 + end
  76 +
  77 + def link_to_bitbucket(app, text = nil)
  78 + return unless app.bitbucket_repo?
  79 + href = "%s#cl-%s" % [app.bitbucket_url_to_file(decorated_path + file_name), number]
  80 + h.link_to(text || file_name, href, :target => '_blank')
  81 + end
  82 +
  83 + def link_to_issue_tracker_file(app, text = nil)
  84 + return unless app.issue_tracker && app.issue_tracker.respond_to?(:url_to_file)
  85 + href = app.issue_tracker.url_to_file(file_relative, number)
  86 + h.link_to(text || file_name, href, :target => '_blank')
  87 + end
  88 +end
app/decorators/notice_decorator.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +class NoticeDecorator < Draper::Decorator
  2 + decorates_association :backtrace
  3 + delegate_all
  4 +end
app/decorators/problem_decorator.rb 0 → 100644
@@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
  1 +class ProblemDecorator < Draper::Decorator
  2 + decorates_association :notices
  3 + delegate_all
  4 +end
app/helpers/backtrace_line_helper.rb
1 module BacktraceLineHelper 1 module BacktraceLineHelper
2 - def link_to_source_file(line, &block) 2 + def link_to_source_file(line, app, &block)
3 text = capture_haml(&block) 3 text = capture_haml(&block)
4 - link_to_in_app_source_file(line, text) || link_to_external_source_file(text) 4 + link_to_in_app_source_file(line, app, text) || text
5 end 5 end
6 6
7 private 7 private
8 - def link_to_in_app_source_file(line, text) 8 + def link_to_in_app_source_file(line, app, text)
9 return unless line.in_app? 9 return unless line.in_app?
10 if line.file_name =~ /\.js$/ 10 if line.file_name =~ /\.js$/
11 - link_to_hosted_javascript(line, text) 11 + link_to_hosted_javascript(line, app, text)
12 else 12 else
13 - link_to_repo_source_file(line, text) ||  
14 - link_to_issue_tracker_file(line, text) 13 + link_to_repo_source_file(line, app, text) ||
  14 + link_to_issue_tracker_file(line, app, text)
15 end 15 end
16 end 16 end
17 17
18 - def link_to_repo_source_file(line, text)  
19 - link_to_github(line, text) || link_to_bitbucket(line, text) 18 + def link_to_repo_source_file(line, app, text)
  19 + link_to_github(line, app, text) || link_to_bitbucket(line, app, text)
20 end 20 end
21 21
22 - def link_to_hosted_javascript(line, text)  
23 - if line.app.asset_host?  
24 - link_to(text, "#{line.app.asset_host}/#{line.file_relative}", :target => '_blank') 22 + def link_to_hosted_javascript(line, app, text)
  23 + if app.asset_host?
  24 + link_to(text, "#{app.asset_host}/#{line.file_relative}", :target => '_blank')
25 end 25 end
26 end 26 end
27 27
28 - def link_to_external_source_file(text)  
29 - text  
30 - end  
31 -  
32 - def link_to_github(line, text = nil)  
33 - return unless line.app.github_repo?  
34 - href = "%s#L%s" % [line.app.github_url_to_file(line.decorated_path + line.file_name), line.number] 28 + def link_to_github(line, app, text = nil)
  29 + return unless app.github_repo?
  30 + href = "%s#L%s" % [app.github_url_to_file(line.decorated_path + line.file_name), line.number]
35 link_to(text || line.file_name, href, :target => '_blank') 31 link_to(text || line.file_name, href, :target => '_blank')
36 end 32 end
37 33
38 - def link_to_bitbucket(line, text = nil)  
39 - return unless line.app.bitbucket_repo?  
40 - href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.decorated_path + line.file_name), line.number] 34 + def link_to_bitbucket(line, app, text = nil)
  35 + return unless app.bitbucket_repo?
  36 + href = "%s#cl-%s" % [app.bitbucket_url_to_file(line.decorated_path + line.file_name), line.number]
41 link_to(text || line.file_name, href, :target => '_blank') 37 link_to(text || line.file_name, href, :target => '_blank')
42 end 38 end
43 39
44 - def link_to_issue_tracker_file(line, text = nil)  
45 - return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file)  
46 - href = line.app.issue_tracker.url_to_file(line.file_relative, line.number) 40 + def link_to_issue_tracker_file(line, app, text = nil)
  41 + return unless app.issue_tracker && app.issue_tracker.respond_to?(:url_to_file)
  42 + href = app.issue_tracker.url_to_file(line.file_relative, line.number)
47 link_to(text || line.file_name, href, :target => '_blank') 43 link_to(text || line.file_name, href, :target => '_blank')
48 end 44 end
49 45
app/interactors/resolved_problem_clearer.rb
@@ -27,6 +27,6 @@ class ResolvedProblemClearer @@ -27,6 +27,6 @@ class ResolvedProblemClearer
27 end 27 end
28 28
29 def repair_database 29 def repair_database
30 - Mongoid.default_session.command :repairDatabase => 1 30 + Mongoid.default_client.command :repairDatabase => 1
31 end 31 end
32 end 32 end
app/mailers/mailer.rb
@@ -13,11 +13,11 @@ class Mailer &lt; ActionMailer::Base @@ -13,11 +13,11 @@ class Mailer &lt; ActionMailer::Base
13 'Precedence' => 'bulk', 13 'Precedence' => 'bulk',
14 'Auto-Submitted' => 'auto-generated' 14 'Auto-Submitted' => 'auto-generated'
15 15
16 - def err_notification(notice)  
17 - @notice = notice  
18 - @app = notice.app 16 + def err_notification(error_report)
  17 + @notice = NoticeDecorator.new error_report.notice
  18 + @app = AppDecorator.new error_report.app
19 19
20 - count = @notice.similar_count 20 + count = error_report.problem.notices_count
21 count = count > 1 ? "(#{count}) " : "" 21 count = count > 1 ? "(#{count}) " : ""
22 22
23 errbit_headers 'App' => @app.name, 23 errbit_headers 'App' => @app.name,
@@ -30,7 +30,7 @@ class Mailer &lt; ActionMailer::Base @@ -30,7 +30,7 @@ class Mailer &lt; ActionMailer::Base
30 30
31 def deploy_notification(deploy) 31 def deploy_notification(deploy)
32 @deploy = deploy 32 @deploy = deploy
33 - @app = deploy.app 33 + @app = AppDecorator.new deploy.app
34 34
35 errbit_headers 'App' => @app.name, 35 errbit_headers 'App' => @app.name,
36 'Environment' => @deploy.environment, 36 'Environment' => @deploy.environment,
@@ -44,7 +44,7 @@ class Mailer &lt; ActionMailer::Base @@ -44,7 +44,7 @@ class Mailer &lt; ActionMailer::Base
44 def comment_notification(comment) 44 def comment_notification(comment)
45 @comment = comment 45 @comment = comment
46 @user = comment.user 46 @user = comment.user
47 - @problem = comment.err 47 + @problem = ProblemDecorator.new comment.err
48 @notice = @problem.notices.first 48 @notice = @problem.notices.first
49 @app = @problem.app 49 @app = @problem.app
50 50
app/models/app.rb
@@ -57,8 +57,14 @@ class App @@ -57,8 +57,14 @@ class App
57 def find_or_create_err!(attrs) 57 def find_or_create_err!(attrs)
58 Err.where( 58 Err.where(
59 :fingerprint => attrs[:fingerprint] 59 :fingerprint => attrs[:fingerprint]
60 - ).first ||  
61 - problems.create!(attrs.slice(:error_class, :environment)).errs.create!(attrs.slice(:fingerprint, :problem_id)) 60 + ).first || (
  61 + problem = problems.create!(
  62 + error_class: attrs[:error_class],
  63 + environment: attrs[:environment],
  64 + app_name: name
  65 + )
  66 + problem.errs.create!(attrs.slice(:fingerprint, :problem_id))
  67 + )
62 end 68 end
63 69
64 # Mongoid Bug: find(id) on association proxies returns an Enumerator 70 # Mongoid Bug: find(id) on association proxies returns an Enumerator
@@ -178,7 +184,9 @@ class App @@ -178,7 +184,9 @@ class App
178 protected 184 protected
179 185
180 def store_cached_attributes_on_problems 186 def store_cached_attributes_on_problems
181 - problems.each(&:cache_app_attributes) 187 + Problem.where(:app_id => id).update_all(
  188 + app_name: name
  189 + )
182 end 190 end
183 191
184 def generate_api_key 192 def generate_api_key
app/models/backtrace.rb
@@ -2,35 +2,28 @@ class Backtrace @@ -2,35 +2,28 @@ class Backtrace
2 include Mongoid::Document 2 include Mongoid::Document
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
4 4
5 - field :fingerprint  
6 - index :fingerprint => 1  
7 -  
8 - has_many :notices  
9 - has_one :notice  
10 -  
11 - embeds_many :lines, :class_name => "BacktraceLine" 5 + IN_APP_PATH = %r{^\[PROJECT_ROOT\](?!(\/vendor))/?}
  6 + GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)}
12 7
13 - after_initialize :generate_fingerprint 8 + field :fingerprint
  9 + field :lines
14 10
15 - delegate :app, :to => :notice 11 + index :fingerprint => 1
16 12
17 - def self.find_or_create(attributes = {})  
18 - new(attributes).similar || create(attributes)  
19 - end 13 + def self.find_or_create(lines)
  14 + fingerprint = generate_fingerprint(lines)
20 15
21 - def similar  
22 - Backtrace.where(:fingerprint => fingerprint).first 16 + where(fingerprint: fingerprint).find_one_and_update(
  17 + { '$setOnInsert' => { fingerprint: fingerprint, lines: lines } },
  18 + { return_document: :after, upsert: true })
23 end 19 end
24 20
25 - def raw=(raw)  
26 - raw.compact.each do |raw_line|  
27 - lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call)  
28 - end 21 + def self.generate_fingerprint(lines)
  22 + Digest::SHA1.hexdigest(lines.map(&:to_s).join)
29 end 23 end
30 24
31 private 25 private
32 def generate_fingerprint 26 def generate_fingerprint
33 - self.fingerprint = Digest::SHA1.hexdigest(lines.map(&:to_s).join) 27 + self.fingerprint = self.class.generate_fingerprint(lines)
34 end 28 end
35 -  
36 end 29 end
app/models/backtrace_line.rb
@@ -1,42 +0,0 @@ @@ -1,42 +0,0 @@
1 -class BacktraceLine  
2 - include Mongoid::Document  
3 - IN_APP_PATH = %r{^\[PROJECT_ROOT\](?!(\/vendor))/?}  
4 - GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)}  
5 -  
6 - field :number, :type => Integer  
7 - field :column, :type => Integer  
8 - field :file  
9 - field :method  
10 -  
11 - embedded_in :backtrace  
12 -  
13 - scope :in_app, ->{ where(:file => IN_APP_PATH) }  
14 -  
15 - delegate :app, :to => :backtrace  
16 -  
17 - def to_s  
18 - "#{file_relative}:#{number}" << (column.present? ? ":#{column}" : "")  
19 - end  
20 -  
21 - def in_app?  
22 - !!(file =~ IN_APP_PATH)  
23 - end  
24 -  
25 - def path  
26 - File.dirname(file).gsub(/^\.$/, '') + "/"  
27 - end  
28 -  
29 - def file_relative  
30 - file.to_s.sub(IN_APP_PATH, '')  
31 - end  
32 -  
33 - def file_name  
34 - File.basename file  
35 - end  
36 -  
37 - def decorated_path  
38 - path.sub(BacktraceLine::IN_APP_PATH, '').  
39 - sub(BacktraceLine::GEMS_PATH, "<strong>\\1</strong>")  
40 - end  
41 -  
42 -end  
app/models/backtrace_line_normalizer.rb
@@ -1,32 +0,0 @@ @@ -1,32 +0,0 @@
1 -class BacktraceLineNormalizer  
2 - def initialize(raw_line)  
3 - @raw_line = raw_line || {}  
4 - end  
5 -  
6 - def call  
7 - @raw_line.merge 'file' => normalized_file, 'method' => normalized_method  
8 - end  
9 -  
10 - private  
11 - def normalized_file  
12 - if @raw_line['file'].blank?  
13 - "[unknown source]"  
14 - else  
15 - file = @raw_line['file'].to_s  
16 - # Detect lines from gem  
17 - file.gsub!(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems')  
18 - # Strip any query strings  
19 - file.gsub!(/\?[^\?]*$/, '')  
20 - @raw_line['file'] = file  
21 - end  
22 - end  
23 -  
24 - def normalized_method  
25 - if @raw_line['method'].blank?  
26 - "[unknown method]"  
27 - else  
28 - @raw_line['method'].to_s.gsub(/[0-9_]{10,}+/, "__FRAGMENT__")  
29 - end  
30 - end  
31 -  
32 -end  
app/models/deploy.rb
@@ -33,7 +33,9 @@ class Deploy @@ -33,7 +33,9 @@ class Deploy
33 end 33 end
34 34
35 def store_cached_attributes_on_problems 35 def store_cached_attributes_on_problems
36 - Problem.where(:app_id => app.id).each(&:cache_app_attributes) 36 + Problem.where(:app_id => app.id).update_all(
  37 + last_deploy_at: created_at
  38 + )
37 end 39 end
38 40
39 def deliver_email 41 def deliver_email
app/models/error_report.rb
@@ -15,8 +15,16 @@ require &#39;hoptoad_notifier&#39; @@ -15,8 +15,16 @@ require &#39;hoptoad_notifier&#39;
15 # * <tt>:notifier</tt> - information to identify the source of the error report 15 # * <tt>:notifier</tt> - information to identify the source of the error report
16 # 16 #
17 class ErrorReport 17 class ErrorReport
18 - attr_reader :error_class, :message, :request, :server_environment, :api_key,  
19 - :notifier, :user_attributes, :framework, :notice 18 + attr_reader :api_key
  19 + attr_reader :error_class
  20 + attr_reader :framework
  21 + attr_reader :message
  22 + attr_reader :notice
  23 + attr_reader :notifier
  24 + attr_reader :problem
  25 + attr_reader :request
  26 + attr_reader :server_environment
  27 + attr_reader :user_attributes
20 28
21 cattr_accessor :fingerprint_strategy do 29 cattr_accessor :fingerprint_strategy do
22 Fingerprint::Sha1 30 Fingerprint::Sha1
@@ -40,24 +48,59 @@ class ErrorReport @@ -40,24 +48,59 @@ class ErrorReport
40 end 48 end
41 49
42 def backtrace 50 def backtrace
43 - @normalized_backtrace ||= Backtrace.find_or_create(raw: @backtrace) 51 + @normalized_backtrace ||= Backtrace.find_or_create(@backtrace)
44 end 52 end
45 53
46 def generate_notice! 54 def generate_notice!
47 return unless valid? 55 return unless valid?
48 return @notice if @notice 56 return @notice if @notice
  57 +
  58 + make_notice
  59 + error.notices << @notice
  60 + cache_attributes_on_problem
  61 + email_notification
  62 + services_notification
  63 + @notice
  64 + end
  65 +
  66 + def make_notice
49 @notice = Notice.new( 67 @notice = Notice.new(
50 message: message, 68 message: message,
51 error_class: error_class, 69 error_class: error_class,
52 - backtrace_id: backtrace.id, 70 + backtrace: backtrace,
53 request: request, 71 request: request,
54 server_environment: server_environment, 72 server_environment: server_environment,
55 notifier: notifier, 73 notifier: notifier,
56 user_attributes: user_attributes, 74 user_attributes: user_attributes,
57 framework: framework 75 framework: framework
58 ) 76 )
59 - error.notices << @notice  
60 - @notice 77 + end
  78 +
  79 + # Update problem cache with information about this notice
  80 + def cache_attributes_on_problem
  81 + @problem = Problem.cache_notice(@error.problem_id, @notice)
  82 + end
  83 +
  84 + # Send email notification if needed
  85 + def email_notification
  86 + return false unless app.emailable?
  87 + return false unless app.email_at_notices.include?(@problem.notices_count)
  88 + Mailer.err_notification(self).deliver
  89 + rescue => e
  90 + HoptoadNotifier.notify(e)
  91 + end
  92 +
  93 + def should_notify?
  94 + app.notification_service.notify_at_notices.include?(0) ||
  95 + app.notification_service.notify_at_notices.include?(@problem.notices_count)
  96 + end
  97 +
  98 + # Launch all notification define on the app associate to this notice
  99 + def services_notification
  100 + return true unless app.notification_service_configured? and should_notify?
  101 + app.notification_service.create_notification(problem)
  102 + rescue => e
  103 + HoptoadNotifier.notify(e)
61 end 104 end
62 105
63 ## 106 ##
app/models/notice.rb
@@ -20,13 +20,10 @@ class Notice @@ -20,13 +20,10 @@ class Notice
20 index(:created_at => 1) 20 index(:created_at => 1)
21 index(:err_id => 1, :created_at => 1, :_id => 1) 21 index(:err_id => 1, :created_at => 1, :_id => 1)
22 22
23 - after_create :cache_attributes_on_problem, :unresolve_problem  
24 - after_create :email_notification  
25 - after_create :services_notification  
26 before_save :sanitize 23 before_save :sanitize
27 - before_destroy :decrease_counter_cache, :remove_cached_attributes_from_problem 24 + before_destroy :problem_recache
28 25
29 - validates_presence_of :backtrace, :server_environment, :notifier 26 + validates_presence_of :backtrace_id, :server_environment, :notifier
30 27
31 scope :ordered, ->{ order_by(:created_at.asc) } 28 scope :ordered, ->{ order_by(:created_at.asc) }
32 scope :reverse_ordered, ->{ order_by(:created_at.desc) } 29 scope :reverse_ordered, ->{ order_by(:created_at.desc) }
@@ -106,26 +103,6 @@ class Notice @@ -106,26 +103,6 @@ class Notice
106 request['session'] || {} 103 request['session'] || {}
107 end 104 end
108 105
109 - def in_app_backtrace_lines  
110 - backtrace_lines.in_app  
111 - end  
112 -  
113 - def similar_count  
114 - problem.notices_count  
115 - end  
116 -  
117 - def emailable?  
118 - app.email_at_notices.include?(similar_count)  
119 - end  
120 -  
121 - def should_email?  
122 - app.emailable? && emailable?  
123 - end  
124 -  
125 - def should_notify?  
126 - app.notification_service.notify_at_notices.include?(0) || app.notification_service.notify_at_notices.include?(similar_count)  
127 - end  
128 -  
129 ## 106 ##
130 # TODO: Move on decorator maybe 107 # TODO: Move on decorator maybe
131 # 108 #
@@ -143,20 +120,8 @@ class Notice @@ -143,20 +120,8 @@ class Notice
143 120
144 protected 121 protected
145 122
146 - def decrease_counter_cache  
147 - problem.inc(notices_count: -1) if err  
148 - end  
149 -  
150 - def remove_cached_attributes_from_problem  
151 - problem.remove_cached_notice_attributes(self) if err  
152 - end  
153 -  
154 - def unresolve_problem  
155 - problem.update_attributes!(:resolved => false, :resolved_at => nil, :notices_count => 1) if problem.resolved?  
156 - end  
157 -  
158 - def cache_attributes_on_problem  
159 - ProblemUpdaterCache.new(problem, self).update 123 + def problem_recache
  124 + problem.uncache_notice(self)
160 end 125 end
161 126
162 def sanitize 127 def sanitize
@@ -165,7 +130,6 @@ class Notice @@ -165,7 +130,6 @@ class Notice
165 end 130 end
166 end 131 end
167 132
168 -  
169 def sanitize_hash(h) 133 def sanitize_hash(h)
170 h.recurse do |h| 134 h.recurse do |h|
171 h.inject({}) do |h,(k,v)| 135 h.inject({}) do |h,(k,v)|
@@ -178,25 +142,4 @@ class Notice @@ -178,25 +142,4 @@ class Notice
178 end 142 end
179 end 143 end
180 end 144 end
181 -  
182 - private  
183 -  
184 - ##  
185 - # Send email notification if needed  
186 - def email_notification  
187 - return true unless should_email?  
188 - Mailer.err_notification(self).deliver  
189 - rescue => e  
190 - HoptoadNotifier.notify(e)  
191 - end  
192 -  
193 - ##  
194 - # Launch all notification define on the app associate to this notice  
195 - def services_notification  
196 - return true unless app.notification_service_configured? and should_notify?  
197 - app.notification_service.create_notification(problem)  
198 - rescue => e  
199 - HoptoadNotifier.notify(e)  
200 - end  
201 -  
202 end 145 end
app/models/notification_service.rb
@@ -26,7 +26,7 @@ class NotificationService @@ -26,7 +26,7 @@ class NotificationService
26 else 26 else
27 Fields = [] 27 Fields = []
28 end 28 end
29 - 29 +
30 def notify_at_notices 30 def notify_at_notices
31 Errbit::Config.per_app_notify_at_notices ? super : Errbit::Config.notify_at_notices 31 Errbit::Config.per_app_notify_at_notices ? super : Errbit::Config.notify_at_notices
32 end 32 end
app/models/problem.rb
@@ -6,8 +6,15 @@ class Problem @@ -6,8 +6,15 @@ class Problem
6 include Mongoid::Document 6 include Mongoid::Document
7 include Mongoid::Timestamps 7 include Mongoid::Timestamps
8 8
9 - field :last_notice_at, :type => DateTime, :default => Proc.new { Time.now }  
10 - field :first_notice_at, :type => DateTime, :default => Proc.new { Time.now } 9 + CACHED_NOTICE_ATTRIBUTES = {
  10 + messages: :message,
  11 + hosts: :host,
  12 + user_agents: :user_agent_string
  13 + }.freeze
  14 +
  15 +
  16 + field :last_notice_at, :type => ActiveSupport::TimeWithZone, :default => Proc.new { Time.now }
  17 + field :first_notice_at, :type => ActiveSupport::TimeWithZone, :default => Proc.new { Time.now }
11 field :last_deploy_at, :type => Time 18 field :last_deploy_at, :type => Time
12 field :resolved, :type => Boolean, :default => false 19 field :resolved, :type => Boolean, :default => false
13 field :resolved_at, :type => Time 20 field :resolved_at, :type => Time
@@ -35,6 +42,14 @@ class Problem @@ -35,6 +42,14 @@ class Problem
35 index :resolved_at => 1 42 index :resolved_at => 1
36 index :notices_count => 1 43 index :notices_count => 1
37 44
  45 + index({
  46 + error_class: "text",
  47 + where: "text",
  48 + message: "text",
  49 + app_name: "text",
  50 + environment: "text"
  51 + }, default_language: "english")
  52 +
38 belongs_to :app 53 belongs_to :app
39 has_many :errs, :inverse_of => :problem, :dependent => :destroy 54 has_many :errs, :inverse_of => :problem, :dependent => :destroy
40 has_many :comments, :inverse_of => :err, :dependent => :destroy 55 has_many :comments, :inverse_of => :err, :dependent => :destroy
@@ -63,6 +78,83 @@ class Problem @@ -63,6 +78,83 @@ class Problem
63 env.present? ? where(:environment => env) : scoped 78 env.present? ? where(:environment => env) : scoped
64 end 79 end
65 80
  81 + def self.cache_notice(id, notice)
  82 + # increment notice count
  83 + message_digest = Digest::MD5.hexdigest(notice.message)
  84 + host_digest = Digest::MD5.hexdigest(notice.host)
  85 + user_agent_digest = Digest::MD5.hexdigest(notice.user_agent_string)
  86 +
  87 + Problem.where('_id' => id).find_one_and_update({
  88 + '$set' => {
  89 + 'environment' => notice.environment_name,
  90 + 'error_class' => notice.error_class,
  91 + 'last_notice_at' => notice.created_at.utc,
  92 + 'message' => notice.message,
  93 + 'resolved' => false,
  94 + 'resolved_at' => nil,
  95 + 'where' => notice.where,
  96 + "messages.#{message_digest}.value" => notice.message,
  97 + "hosts.#{host_digest}.value" => notice.host,
  98 + "user_agents.#{user_agent_digest}.value" => notice.user_agent_string,
  99 + },
  100 + '$inc' => {
  101 + 'notices_count' => 1,
  102 + "messages.#{message_digest}.count" => 1,
  103 + "hosts.#{host_digest}.count" => 1,
  104 + "user_agents.#{user_agent_digest}.count" => 1,
  105 + }
  106 + }, return_document: :after)
  107 + end
  108 +
  109 + def uncache_notice(notice)
  110 + last_notice = notices.last
  111 +
  112 + atomically do |doc|
  113 + doc.set(
  114 + 'environment' => last_notice.environment_name,
  115 + 'error_class' => last_notice.error_class,
  116 + 'last_notice_at' => last_notice.created_at,
  117 + 'message' => last_notice.message,
  118 + 'where' => last_notice.where,
  119 + 'notices_count' => notices_count.to_i > 1 ? notices_count - 1 : 0
  120 + )
  121 +
  122 + CACHED_NOTICE_ATTRIBUTES.each do |k,v|
  123 + digest = Digest::MD5.hexdigest(notice.send(v))
  124 + field = "#{k}.#{digest}"
  125 +
  126 + if (doc[k].try(:[], digest).try(:[], :count)).to_i > 1
  127 + doc.inc("#{field}.count" => -1)
  128 + else
  129 + doc.unset(field)
  130 + end
  131 + end
  132 + end
  133 + end
  134 +
  135 + def recache
  136 + CACHED_NOTICE_ATTRIBUTES.each do |k,v|
  137 + # clear all cached attributes
  138 + send("#{k}=", {})
  139 +
  140 + # find only notices related to this problem
  141 + Notice.collection.find.aggregate([
  142 + { "$match" => { err_id: { "$in" => err_ids } } },
  143 + { "$group" => { _id: "$#{v}", count: {"$sum" => 1} } }
  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]
  150 + }
  151 + end
  152 + end
  153 +
  154 + self.notices_count = Notice.where({ err_id: { "$in" => err_ids }}).count
  155 + save
  156 + end
  157 +
66 def url 158 def url
67 Rails.application.routes.url_helpers.app_problem_url(app, self, 159 Rails.application.routes.url_helpers.app_problem_url(app, self,
68 :host => Errbit::Config.host, 160 :host => Errbit::Config.host,
@@ -98,16 +190,21 @@ class Problem @@ -98,16 +190,21 @@ class Problem
98 def unmerge! 190 def unmerge!
99 attrs = {:error_class => error_class, :environment => environment} 191 attrs = {:error_class => error_class, :environment => environment}
100 problem_errs = errs.to_a 192 problem_errs = errs.to_a
101 - problem_errs.shift  
102 - [self] + problem_errs.map(&:id).map do |err_id|  
103 - err = Err.find(err_id)  
104 - app.problems.create(attrs).tap do |new_problem|  
105 - err.update_attribute(:problem_id, new_problem.id)  
106 - new_problem.reset_cached_attributes  
107 - end 193 +
  194 + # associate and return all the problems
  195 + new_problems = [self]
  196 +
  197 + # create new problems for each err that needs one
  198 + (problem_errs[1..-1] || []).each do |err|
  199 + new_problems << app.problems.create(attrs)
  200 + err.update_attribute(:problem, new_problems.last)
108 end 201 end
109 - end  
110 202
  203 + # recache each new problem
  204 + new_problems.each(&:recache)
  205 +
  206 + new_problems
  207 + end
111 208
112 def self.ordered_by(sort, order) 209 def self.ordered_by(sort, order)
113 case sort 210 case sort
@@ -124,20 +221,10 @@ class Problem @@ -124,20 +221,10 @@ class Problem
124 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}]) 221 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}])
125 end 222 end
126 223
127 -  
128 - def reset_cached_attributes  
129 - ProblemUpdaterCache.new(self).update  
130 - end  
131 -  
132 def cache_app_attributes 224 def cache_app_attributes
133 if app 225 if app
134 self.app_name = app.name 226 self.app_name = app.name
135 - self.last_deploy_at = if (last_deploy = app.deploys.where(:environment => self.environment).last)  
136 - last_deploy.created_at.utc  
137 - end  
138 - collection.find('_id' => self.id)  
139 - .update({'$set' => {'app_name' => self.app_name,  
140 - 'last_deploy_at' => self.last_deploy_at.try(:utc)}}) 227 + self.last_deploy_at = app.last_deploy_at
141 end 228 end
142 end 229 end
143 230
@@ -145,14 +232,6 @@ class Problem @@ -145,14 +232,6 @@ class Problem
145 self.message = self.message[0, 1000] if self.message 232 self.message = self.message[0, 1000] if self.message
146 end 233 end
147 234
148 - def remove_cached_notice_attributes(notice)  
149 - update_attributes!(  
150 - :messages => attribute_count_descrease(:messages, notice.message),  
151 - :hosts => attribute_count_descrease(:hosts, notice.host),  
152 - :user_agents => attribute_count_descrease(:user_agents, notice.user_agent_string)  
153 - )  
154 - end  
155 -  
156 def issue_type 235 def issue_type
157 # Return issue_type if configured, but fall back to detecting app's issue tracker 236 # Return issue_type if configured, but fall back to detecting app's issue tracker
158 attributes['issue_type'] ||= 237 attributes['issue_type'] ||=
@@ -160,13 +239,7 @@ class Problem @@ -160,13 +239,7 @@ class Problem
160 end 239 end
161 240
162 def self.search(value) 241 def self.search(value)
163 - any_of(  
164 - {:error_class => /#{value}/i},  
165 - {:where => /#{value}/i},  
166 - {:message => /#{value}/i},  
167 - {:app_name => /#{value}/i},  
168 - {:environment => /#{value}/i}  
169 - ) 242 + Problem.where({'$text' => {'$search' => value}})
170 end 243 end
171 244
172 private 245 private
app/views/issue_trackers/issue.md.erb
@@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
27 27
28 ## Backtrace ## 28 ## Backtrace ##
29 ~~~ 29 ~~~
30 -<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>** 30 +<% notice.backtrace.lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
31 <% end %> 31 <% end %>
32 ~~~ 32 ~~~
33 33
app/views/issue_trackers/issue.txt.erb
@@ -16,6 +16,6 @@ Env: &lt;%= pretty_hash notice.env_vars %&gt; @@ -16,6 +16,6 @@ Env: &lt;%= pretty_hash notice.env_vars %&gt;
16 16
17 Backtrace 17 Backtrace
18 --------- 18 ---------
19 -<% notice.backtrace_lines.each do |line| %><%= sprintf('%5d: %s **%s', line.number, line.file_relative, line.method) %> 19 +<% notice.backtrace.lines.each do |line| %><%= sprintf('%5d: %s **%s', line.number, line.file_relative, line.method) %>
20 <% end %> 20 <% end %>
21 <% end %> 21 <% end %>
app/views/mailer/comment_notification.text.erb
@@ -21,8 +21,8 @@ WHERE: @@ -21,8 +21,8 @@ WHERE:
21 21
22 <%= @notice.where %> 22 <%= @notice.where %>
23 23
24 -<% @notice.in_app_backtrace_lines.each do |line| %>  
25 - <%= line %> 24 +<% @notice.backtrace.lines.each do |line| %>
  25 + <% next unless line.in_app? %><%= line %>
26 <% end %> 26 <% end %>
27 27
28 28
app/views/mailer/err_notification.html.haml
@@ -27,9 +27,10 @@ @@ -27,9 +27,10 @@
27 %p.heading WHERE: 27 %p.heading WHERE:
28 %p.monospace 28 %p.monospace
29 = @notice.where 29 = @notice.where
30 - - @notice.in_app_backtrace_lines.each do |line| 30 + - @notice.backtrace.lines.each do |line|
  31 + - next unless line.in_app?
31 %p.backtrace 32 %p.backtrace
32 - = link_to_source_file(line) do 33 + = line.link_to_source_file(@app) do
33 = line.to_s 34 = line.to_s
34 %br 35 %br
35 - if @notice.app_version.present? 36 - if @notice.app_version.present?
@@ -59,11 +60,11 @@ @@ -59,11 +60,11 @@
59 %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":" 60 %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":"
60 %td= auto_link(value.to_s) 61 %td= auto_link(value.to_s)
61 %br 62 %br
62 - - if @notice.backtrace_lines.any? 63 + - if @notice.backtrace.lines.any?
63 %br 64 %br
64 %p.heading FULL BACKTRACE: 65 %p.heading FULL BACKTRACE:
65 - - @notice.backtrace_lines.each do |line| 66 + - @notice.backtrace.lines.each do |line|
66 %p.backtrace 67 %p.backtrace
67 - = link_to_source_file(line) do 68 + = link_to_source_file(line, @app) do
68 = line.to_s 69 = line.to_s
69 %br 70 %br
app/views/mailer/err_notification.text.erb
@@ -14,7 +14,8 @@ WHERE: @@ -14,7 +14,8 @@ WHERE:
14 14
15 <%= @notice.where %> 15 <%= @notice.where %>
16 16
17 -<% @notice.in_app_backtrace_lines.each do |line| %> 17 +<% @notice.backtrace.lines.each do |line| %>
  18 + <% next unless line.in_app? %>
18 <%= line %> 19 <%= line %>
19 <% end %> 20 <% end %>
20 21
@@ -51,7 +52,7 @@ USER: @@ -51,7 +52,7 @@ USER:
51 52
52 BACKTRACE: 53 BACKTRACE:
53 54
54 -<% @notice.backtrace_lines.each do |line| %> 55 +<% @notice.backtrace.lines.each do |line| %>
55 <%= line %> 56 <%= line %>
56 <% end %> 57 <% end %>
57 58
app/views/notices/_backtrace_line.html.haml
1 %tr{:class => defined?(row_class) && row_class} 1 %tr{:class => defined?(row_class) && row_class}
2 %td.line{:class => line.in_app? && 'in-app' } 2 %td.line{:class => line.in_app? && 'in-app' }
3 - = link_to_source_file(line) do 3 + = line.link_to_source_file(app) do
4 %span.path>= raw line.decorated_path 4 %span.path>= raw line.decorated_path
5 %span.file>= line.file_name 5 %span.file>= line.file_name
6 - if line.number.present? 6 - if line.number.present?
app/views/problems/show.html.haml
@@ -68,7 +68,7 @@ @@ -68,7 +68,7 @@
68 68
69 #backtrace 69 #backtrace
70 %h3 Backtrace 70 %h3 Backtrace
71 - = render 'notices/backtrace', :lines => @notice.backtrace_lines 71 + = render 'notices/backtrace', :lines => @notice.backtrace.lines
72 72
73 - if @notice.user_attributes.present? 73 - if @notice.user_attributes.present?
74 #user_attributes 74 #user_attributes
config/application.rb
@@ -2,7 +2,7 @@ require File.expand_path(&#39;../boot&#39;, __FILE__) @@ -2,7 +2,7 @@ require File.expand_path(&#39;../boot&#39;, __FILE__)
2 2
3 require 'action_controller/railtie' 3 require 'action_controller/railtie'
4 require 'action_mailer/railtie' 4 require 'action_mailer/railtie'
5 -require 'mongoid/railtie' 5 +# require 'mongoid/railtie'
6 require 'sprockets/railtie' 6 require 'sprockets/railtie'
7 7
8 # Require the gems listed in Gemfile, including any gems 8 # Require the gems listed in Gemfile, including any gems
@@ -19,9 +19,6 @@ module Errbit @@ -19,9 +19,6 @@ module Errbit
19 config.autoload_paths += [Rails.root.join('lib')] 19 config.autoload_paths += [Rails.root.join('lib')]
20 20
21 config.before_initialize do 21 config.before_initialize do
22 - # Load up Errbit::Config with values from the environment  
23 - require Rails.root.join('config/load')  
24 -  
25 config.secret_key_base = Errbit::Config.secret_key_base 22 config.secret_key_base = Errbit::Config.secret_key_base
26 config.serve_static_assets = Errbit::Config.serve_static_assets 23 config.serve_static_assets = Errbit::Config.serve_static_assets
27 end 24 end
config/environment.rb
1 # Load the Rails application. 1 # Load the Rails application.
2 require File.expand_path('../application', __FILE__) 2 require File.expand_path('../application', __FILE__)
3 3
  4 +# Load up Errbit::Config with values from the environment
  5 +require Rails.root.join('config/load')
  6 +
  7 +if Errbit::Config.log_location == 'STDOUT'
  8 + Rails.logger = ActiveSupport::Logger.new STDOUT
  9 +else
  10 + Rails.logger = ActiveSupport::Logger.new Errbit::Config.log_location
  11 +end
  12 +
  13 +Rails.logger.level = Errbit::Config.log_level.to_sym
  14 +
4 # Initialize the Rails application. 15 # Initialize the Rails application.
5 Rails.application.initialize! 16 Rails.application.initialize!
config/initializers/mongo.rb
config/load.rb
1 # load default ENV values (without overwriting any existing value) 1 # load default ENV values (without overwriting any existing value)
2 Dotenv.load('.env.default') 2 Dotenv.load('.env.default')
3 3
  4 +require_relative '../lib/configurator'
  5 +
4 # map config keys to environment variables 6 # map config keys to environment variables
5 # 7 #
6 # We use the first non-nil environment variable in the list. If the last array 8 # We use the first non-nil environment variable in the list. If the last array
@@ -19,6 +21,8 @@ Errbit::Config = Configurator.run({ @@ -19,6 +21,8 @@ Errbit::Config = Configurator.run({
19 per_app_email_at_notices: ['ERRBIT_PER_APP_EMAIL_AT_NOTICES'], 21 per_app_email_at_notices: ['ERRBIT_PER_APP_EMAIL_AT_NOTICES'],
20 notify_at_notices: ['ERRBIT_NOTIFY_AT_NOTICES'], 22 notify_at_notices: ['ERRBIT_NOTIFY_AT_NOTICES'],
21 per_app_notify_at_notices: ['ERRBIT_PER_APP_NOTIFY_AT_NOTICES'], 23 per_app_notify_at_notices: ['ERRBIT_PER_APP_NOTIFY_AT_NOTICES'],
  24 + log_location: ['ERRBIT_LOG_LOCATION'],
  25 + log_level: ['ERRBIT_LOG_LEVEL'],
22 26
23 serve_static_assets: ['SERVE_STATIC_ASSETS'], 27 serve_static_assets: ['SERVE_STATIC_ASSETS'],
24 secret_key_base: ['SECRET_KEY_BASE'], 28 secret_key_base: ['SECRET_KEY_BASE'],
config/mongo.rb
  1 +log_level = Logger.const_get Errbit::Config.log_level.upcase
  2 +
  3 +Mongoid.logger.level = log_level
  4 +Mongo::Logger.level = log_level
  5 +
1 Mongoid.configure do |config| 6 Mongoid.configure do |config|
2 uri = if Errbit::Config.mongo_url == 'mongodb://localhost' 7 uri = if Errbit::Config.mongo_url == 'mongodb://localhost'
3 "mongodb://localhost/errbit_#{Rails.env}" 8 "mongodb://localhost/errbit_#{Rails.env}"
@@ -6,7 +11,7 @@ Mongoid.configure do |config| @@ -6,7 +11,7 @@ Mongoid.configure do |config|
6 end 11 end
7 12
8 config.load_configuration({ 13 config.load_configuration({
9 - sessions: { 14 + clients: {
10 default: { 15 default: {
11 uri: uri 16 uri: uri
12 } 17 }
spec/acceptance/app_regenerate_api_key_spec.rb
@@ -52,8 +52,8 @@ feature &quot;Create an application&quot; do @@ -52,8 +52,8 @@ feature &quot;Create an application&quot; do
52 fill_in 'app_name', :with => 'My new app' 52 fill_in 'app_name', :with => 'My new app'
53 click_on I18n.t('apps.new.add_app') 53 click_on I18n.t('apps.new.add_app')
54 page.has_content?(I18n.t('controllers.apps.flash.create.success')) 54 page.has_content?(I18n.t('controllers.apps.flash.create.success'))
55 - expect(App.where(:name => 'My new app').count).to eql 1  
56 - expect(App.where(:name => 'My new app 2').count).to eql 0 55 + expect(App.where(:name => 'My new app').count).to eq 1
  56 + expect(App.where(:name => 'My new app 2').count).to eq 0
57 57
58 58
59 click_on I18n.t('shared.navigation.apps') 59 click_on I18n.t('shared.navigation.apps')
@@ -62,8 +62,8 @@ feature &quot;Create an application&quot; do @@ -62,8 +62,8 @@ feature &quot;Create an application&quot; do
62 fill_in 'app_name', :with => 'My new app 2' 62 fill_in 'app_name', :with => 'My new app 2'
63 click_on I18n.t('apps.edit.update') 63 click_on I18n.t('apps.edit.update')
64 page.has_content?(I18n.t('controllers.apps.flash.update.success')) 64 page.has_content?(I18n.t('controllers.apps.flash.update.success'))
65 - expect(App.where(:name => 'My new app').count).to eql 0  
66 - expect(App.where(:name => 'My new app 2').count).to eql 1 65 + expect(App.where(:name => 'My new app').count).to eq 0
  66 + expect(App.where(:name => 'My new app 2').count).to eq 1
67 67
68 end 68 end
69 69
spec/controllers/problems_controller_spec.rb
@@ -138,15 +138,13 @@ describe ProblemsController, type: &#39;controller&#39; do @@ -138,15 +138,13 @@ describe ProblemsController, type: &#39;controller&#39; do
138 end 138 end
139 139
140 it "searches problems for given string" do 140 it "searches problems for given string" do
141 - get :search, :search => "Most important" 141 + get :search, :search => "\"Most important\""
142 expect(controller.problems).to include(@problem1) 142 expect(controller.problems).to include(@problem1)
143 expect(controller.problems).to_not include(@problem2) 143 expect(controller.problems).to_not include(@problem2)
144 end 144 end
145 end 145 end
146 146
147 describe "GET /apps/:app_id/problems/:id" do 147 describe "GET /apps/:app_id/problems/:id" do
148 - #render_views  
149 -  
150 context 'when logged in as an admin' do 148 context 'when logged in as an admin' do
151 before do 149 before do
152 sign_in admin 150 sign_in admin
@@ -250,8 +248,8 @@ describe ProblemsController, type: &#39;controller&#39; do @@ -250,8 +248,8 @@ describe ProblemsController, type: &#39;controller&#39; do
250 before { sign_in admin } 248 before { sign_in admin }
251 249
252 context "when app has a issue tracker" do 250 context "when app has a issue tracker" do
253 - let(:notice) { Fabricate :notice }  
254 - let(:problem) { notice.problem } 251 + let(:notice) { NoticeDecorator.new(Fabricate :notice) }
  252 + let(:problem) { ProblemDecorator.new(notice.problem) }
255 let(:issue_tracker) do 253 let(:issue_tracker) do
256 Fabricate(:issue_tracker).tap do |t| 254 Fabricate(:issue_tracker).tap do |t|
257 t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options)) 255 t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options))
spec/fabricators/err_fabricator.rb
@@ -10,15 +10,19 @@ Fabricator :notice do @@ -10,15 +10,19 @@ Fabricator :notice do
10 server_environment { {'environment-name' => 'production'} } 10 server_environment { {'environment-name' => 'production'} }
11 request {{ 'component' => 'foo', 'action' => 'bar' }} 11 request {{ 'component' => 'foo', 'action' => 'bar' }}
12 notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }} 12 notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }}
13 -end  
14 13
15 -Fabricator :backtrace do  
16 - lines(:count => 99) { Fabricate.build(:backtrace_line) } 14 + after_create do
  15 + Problem.cache_notice(err.problem_id, self)
  16 + problem.reload
  17 + end
17 end 18 end
18 19
19 -Fabricator :backtrace_line do  
20 - number { rand(999) }  
21 - file { "/path/to/file/#{SecureRandom.hex(4)}.rb" }  
22 - method(:method) { ActiveSupport.methods.shuffle.first } 20 +Fabricator :backtrace do
  21 + lines(:count => 99) do
  22 + {
  23 + number: rand(999),
  24 + file: "/path/to/file/#{SecureRandom.hex(4)}.rb",
  25 + method: ActiveSupport.methods.shuffle.first
  26 + }
  27 + end
23 end 28 end
24 -  
spec/fabricators/problem_fabricator.rb
@@ -23,8 +23,7 @@ end @@ -23,8 +23,7 @@ end
23 23
24 Fabricator(:problem_resolved, :from => :problem) do 24 Fabricator(:problem_resolved, :from => :problem) do
25 after_create do |pr| 25 after_create do |pr|
26 - Fabricate(:notice,  
27 - :err => Fabricate(:err, :problem => pr)) 26 + Fabricate(:notice, :err => Fabricate(:err, :problem => pr))
28 pr.resolve! 27 pr.resolve!
29 end 28 end
30 end 29 end
spec/interactors/resolved_problem_clearer_spec.rb
@@ -19,16 +19,16 @@ describe ResolvedProblemClearer do @@ -19,16 +19,16 @@ describe ResolvedProblemClearer do
19 } 19 }
20 end 20 end
21 it 'not repair database' do 21 it 'not repair database' do
22 - allow(Mongoid.default_session).to receive(:command).and_call_original  
23 - expect(Mongoid.default_session).to_not receive(:command).with({:repairDatabase => 1}) 22 + allow(Mongoid.default_client).to receive(:command).and_call_original
  23 + expect(Mongoid.default_client).to_not receive(:command).with({:repairDatabase => 1})
24 resolved_problem_clearer.execute 24 resolved_problem_clearer.execute
25 end 25 end
26 end 26 end
27 27
28 context "with problem resolve" do 28 context "with problem resolve" do
29 before do 29 before do
30 - allow(Mongoid.default_session).to receive(:command).and_call_original  
31 - allow(Mongoid.default_session).to receive(:command).with({:repairDatabase => 1}) 30 + allow(Mongoid.default_client).to receive(:command).and_call_original
  31 + allow(Mongoid.default_client).to receive(:command).with({:repairDatabase => 1})
32 problems.first.resolve! 32 problems.first.resolve!
33 problems.second.resolve! 33 problems.second.resolve!
34 end 34 end
@@ -44,7 +44,7 @@ describe ResolvedProblemClearer do @@ -44,7 +44,7 @@ describe ResolvedProblemClearer do
44 end 44 end
45 45
46 it 'repair database' do 46 it 'repair database' do
47 - expect(Mongoid.default_session).to receive(:command).with({:repairDatabase => 1}) 47 + expect(Mongoid.default_client).to receive(:command).with({:repairDatabase => 1})
48 resolved_problem_clearer.execute 48 resolved_problem_clearer.execute
49 end 49 end
50 end 50 end
spec/mailers/mailer_spec.rb
1 shared_examples "a notification email" do 1 shared_examples "a notification email" do
2 it "should have X-Mailer header" do 2 it "should have X-Mailer header" do
3 - expect(@email).to have_header('X-Mailer', 'Errbit') 3 + expect(email).to have_header('X-Mailer', 'Errbit')
4 end 4 end
5 5
6 it "should have X-Errbit-Host header" do 6 it "should have X-Errbit-Host header" do
7 - expect(@email).to have_header('X-Errbit-Host', Errbit::Config.host) 7 + expect(email).to have_header('X-Errbit-Host', Errbit::Config.host)
8 end 8 end
9 9
10 it "should have Precedence header" do 10 it "should have Precedence header" do
11 - expect(@email).to have_header('Precedence', 'bulk') 11 + expect(email).to have_header('Precedence', 'bulk')
12 end 12 end
13 13
14 it "should have Auto-Submitted header" do 14 it "should have Auto-Submitted header" do
15 - expect(@email).to have_header('Auto-Submitted', 'auto-generated') 15 + expect(email).to have_header('Auto-Submitted', 'auto-generated')
16 end 16 end
17 17
18 it "should have X-Auto-Response-Suppress header" do 18 it "should have X-Auto-Response-Suppress header" do
19 # http://msdn.microsoft.com/en-us/library/ee219609(v=EXCHG.80).aspx 19 # http://msdn.microsoft.com/en-us/library/ee219609(v=EXCHG.80).aspx
20 - expect(@email).to have_header('X-Auto-Response-Suppress', 'OOF, AutoReply') 20 + expect(email).to have_header('X-Auto-Response-Suppress', 'OOF, AutoReply')
21 end 21 end
22 22
23 it "should send the email" do 23 it "should send the email" do
  24 + email
24 expect(ActionMailer::Base.deliveries.size).to eq 1 25 expect(ActionMailer::Base.deliveries.size).to eq 1
25 end 26 end
26 end 27 end
@@ -30,44 +31,63 @@ describe Mailer do @@ -30,44 +31,63 @@ describe Mailer do
30 include EmailSpec::Helpers 31 include EmailSpec::Helpers
31 include EmailSpec::Matchers 32 include EmailSpec::Matchers
32 33
33 - let(:notice) { Fabricate(:notice, :message => "class < ActionController::Base") }  
34 - let!(:user) { Fabricate(:admin) } 34 + let(:notice) do
  35 + n = Fabricate(:notice, message: "class < ActionController::Base")
  36 + n.backtrace.lines.last[:file] = '[PROJECT_ROOT]/path/to/file.js'
  37 + # notice.backtrace.update_attributes(lines: lines)
  38 + n
  39 + end
35 40
36 - before do  
37 - ActionMailer::Base.deliveries = []  
38 - notice.backtrace.lines.last.update_attributes(:file => "[PROJECT_ROOT]/path/to/file.js")  
39 - notice.app.update_attributes( 41 + let(:app) do
  42 + a = notice.app
  43 + a.update_attributes(
40 :asset_host => "http://example.com", 44 :asset_host => "http://example.com",
41 :notify_all_users => true 45 :notify_all_users => true
42 ) 46 )
43 - notice.problem.update_attributes :notices_count => 3  
44 -  
45 - @email = Mailer.err_notification(notice).deliver 47 + a
  48 + end
  49 + let(:problem) do
  50 + p = notice.problem
  51 + p.notices_count = 3
  52 + p
  53 + end
  54 + let!(:user) { Fabricate(:admin) }
  55 + let(:error_report) do
  56 + instance_double(
  57 + 'ErrorReport',
  58 + notice: notice,
  59 + app: app,
  60 + problem: problem
  61 + )
  62 + end
  63 + let(:email) do
  64 + Mailer.err_notification(error_report).deliver
46 end 65 end
47 66
48 - it_should_behave_like "a notification email" 67 + before { email }
49 68
  69 + it_should_behave_like "a notification email"
50 70
51 it "should html-escape the notice's message for the html part" do 71 it "should html-escape the notice's message for the html part" do
52 - expect(@email).to have_body_text("class &lt; ActionController::Base") 72 + expect(email).to have_body_text("class &lt; ActionController::Base")
53 end 73 end
54 74
55 it "should have inline css" do 75 it "should have inline css" do
56 - expect(@email).to have_body_text('<p class="backtrace" style="') 76 + expect(email).to have_body_text('<p class="backtrace" style="')
57 end 77 end
58 78
59 it "should have links to source files" do 79 it "should have links to source files" do
60 - expect(@email).to have_body_text('<a href="http://example.com/path/to/file.js" target="_blank">path/to/file.js') 80 + expect(email).to have_body_text('<a href="http://example.com/path/to/file.js" target="_blank">path/to/file.js')
61 end 81 end
62 82
63 it "should have the error count in the subject" do 83 it "should have the error count in the subject" do
64 - expect(@email.subject).to match( /^\(3\) / ) 84 + expect(email.subject).to match( /^\(3\) / )
65 end 85 end
66 86
67 context 'with a very long message' do 87 context 'with a very long message' do
68 let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) } 88 let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) }
69 it "should truncate the long message" do 89 it "should truncate the long message" do
70 - expect(@email.subject).to match( / \d{47}\.{3}$/ ) 90 + expect(email.subject).to match( / \d{47}\.{3}$/ )
71 end 91 end
72 end 92 end
73 end 93 end
spec/models/backtrace_line_normalizer_spec.rb
@@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
1 -describe BacktraceLineNormalizer, type: 'model' do  
2 - subject { described_class.new(raw_line).call }  
3 -  
4 - describe "sanitize" do  
5 - context "unknown file and method" do  
6 - let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => nil } }  
7 -  
8 - it "should replace nil file with [unknown source]" do  
9 - expect(subject['file']).to eq "[unknown source]"  
10 - end  
11 -  
12 - it "should replace nil method with [unknown method]" do  
13 - expect(subject['method']).to eq "[unknown method]"  
14 - end  
15 - end  
16 -  
17 - context "in app file" do  
18 - let(:raw_line) { { 'number' => rand(999), 'file' => "[PROJECT_ROOT]/assets/file.js?body=1", 'method' => nil } }  
19 -  
20 - it "should strip query strings from files" do  
21 - expect(subject['file']).to eq "[PROJECT_ROOT]/assets/file.js"  
22 - end  
23 - end  
24 - end  
25 -end  
spec/models/backtrace_line_spec.rb
@@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
1 -describe BacktraceLine, type: 'model' do  
2 - subject { described_class.new(raw_line) }  
3 -  
4 - describe "root at the start of decorated filename" do  
5 - let(:raw_line) { { 'number' => rand(999), 'file' => '[PROJECT_ROOT]/app/controllers/pages_controller.rb', 'method' => ActiveSupport.methods.shuffle.first.to_s } }  
6 - it "should leave leading root symbol in filepath" do  
7 - expect(subject.decorated_path).to eq 'app/controllers/'  
8 - end  
9 - end  
10 -end  
spec/models/backtrace_spec.rb
1 describe Backtrace, type: 'model' do 1 describe Backtrace, type: 'model' do
2 - subject { described_class.new }  
3 -  
4 - its(:fingerprint) { should be_present }  
5 -  
6 - describe "#similar" do  
7 - context "no similar backtrace" do  
8 - its(:similar) { should be_nil } 2 + describe '.find_or_create' do
  3 + let(:lines) do
  4 + [
  5 + { 'number' => '123', 'file' => '/some/path/to.rb', 'method' => 'abc' },
  6 + { 'number' => '345', 'file' => '/path/to.rb', 'method' => 'dowhat' }
  7 + ]
9 end 8 end
  9 + let(:fingerprint) { Backtrace.generate_fingerprint(lines) }
10 10
11 - context "similar backtrace exist" do  
12 - let!(:similar_backtrace) {  
13 - b = Fabricate(:backtrace)  
14 - b.fingerprint = fingerprint  
15 - b.save!  
16 - b  
17 - }  
18 - let(:fingerprint) { "fingerprint" }  
19 -  
20 - before { allow(subject).to receive(:fingerprint).and_return(fingerprint) }  
21 -  
22 - its(:similar) { should == similar_backtrace }  
23 - end  
24 - end  
25 -  
26 - describe "find_or_create" do  
27 - subject { described_class.find_or_create(attributes) }  
28 - let(:attributes) { double :attributes }  
29 - let(:backtrace) { double :backtrace }  
30 -  
31 - before { allow(described_class).to receive(:new).and_return(backtrace) }  
32 -  
33 - context "no similar backtrace" do  
34 - before { allow(backtrace).to receive(:similar).and_return(nil) }  
35 - it "create new backtrace" do  
36 - expect(described_class).to receive(:create).with(attributes) 11 + it 'create new backtrace' do
  12 + backtrace = described_class.find_or_create(lines)
37 13
38 - described_class.find_or_create(attributes)  
39 - end 14 + expect(backtrace.lines).to eq(lines)
  15 + expect(backtrace.fingerprint).to eq(fingerprint)
40 end 16 end
41 17
42 - context "similar backtrace exist" do  
43 - let(:similar_backtrace) { double :similar_backtrace }  
44 - before { allow(backtrace).to receive(:similar).and_return(similar_backtrace) } 18 + it 'creates one backtrace for two identical ones' do
  19 + described_class.find_or_create(lines)
  20 + described_class.find_or_create(lines)
45 21
46 - it { should == similar_backtrace } 22 + expect(Backtrace.where(fingerprint: fingerprint).count).to eq(1)
47 end 23 end
48 end 24 end
49 end 25 end
spec/models/error_report_spec.rb
@@ -17,259 +17,310 @@ module Airbrake @@ -17,259 +17,310 @@ module Airbrake
17 end 17 end
18 18
19 describe ErrorReport do 19 describe ErrorReport do
20 - context "with notice without line of backtrace" do  
21 - let(:xml){  
22 - Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read  
23 - }  
24 -  
25 - let(:error_report) {  
26 - ErrorReport.new(xml)  
27 - }  
28 -  
29 - let!(:app) {  
30 - Fabricate(  
31 - :app,  
32 - :api_key => 'APIKEY'  
33 - )  
34 - }  
35 -  
36 - describe "#app" do  
37 - it 'find the good app' do  
38 - expect(error_report.app).to eq app  
39 - end 20 + let(:xml){
  21 + Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
  22 + }
  23 +
  24 + let(:error_report) { ErrorReport.new(xml) }
  25 +
  26 + let!(:app) {
  27 + Fabricate(
  28 + :app,
  29 + :api_key => 'APIKEY'
  30 + )
  31 + }
  32 +
  33 + describe "#app" do
  34 + it 'find the good app' do
  35 + expect(error_report.app).to eq app
40 end 36 end
  37 + end
41 38
42 - describe "#backtrace" do  
43 - it 'should have valid backtrace' do  
44 - expect(error_report.backtrace).to be_valid  
45 - end 39 + describe "#backtrace" do
  40 + it 'should have valid backtrace' do
  41 + expect(error_report.backtrace).to be_valid
46 end 42 end
  43 + end
47 44
48 - describe "#fingerprint_strategy" do  
49 - it "should be possible to change how fingerprints are generated" do  
50 - def error_report.fingerprint_strategy  
51 - Class.new do  
52 - def self.generate(*args)  
53 - 'fingerprintzzz'  
54 - end 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'
55 end 51 end
56 end 52 end
57 -  
58 - expect(error_report.error.fingerprint).to eq('fingerprintzzz')  
59 end 53 end
  54 +
  55 + expect(error_report.error.fingerprint).to eq('fingerprintzzz')
60 end 56 end
  57 + end
61 58
62 - describe "#generate_notice!" do  
63 - it "save a notice" do 59 + describe "#generate_notice!" do
  60 + it "save a notice" do
  61 + expect {
  62 + error_report.generate_notice!
  63 + }.to change {
  64 + app.reload.problems.count
  65 + }.by(1)
  66 + end
  67 +
  68 + context "with notice generate by Airbrake gem" do
  69 + let(:xml) { Airbrake::Notice.new(
  70 + :exception => Exception.new,
  71 + :api_key => 'APIKEY',
  72 + :project_root => Rails.root
  73 + ).to_xml }
  74 + it 'save a notice' do
64 expect { 75 expect {
65 error_report.generate_notice! 76 error_report.generate_notice!
66 }.to change { 77 }.to change {
67 app.reload.problems.count 78 app.reload.problems.count
68 }.by(1) 79 }.by(1)
69 end 80 end
  81 + end
70 82
71 - context "with notice generate by Airbrake gem" do  
72 - let(:xml) { Airbrake::Notice.new(  
73 - :exception => Exception.new,  
74 - :api_key => 'APIKEY',  
75 - :project_root => Rails.root  
76 - ).to_xml }  
77 - it 'save a notice' do  
78 - expect {  
79 - error_report.generate_notice!  
80 - }.to change {  
81 - app.reload.problems.count  
82 - }.by(1)  
83 - end  
84 - end 83 + describe "notice create" do
  84 + before { error_report.generate_notice! }
  85 + subject { error_report.notice }
  86 + its(:message) { 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' }
  87 + its(:framework) { should == 'Rails: 3.2.11' }
85 88
86 - describe "notice create" do  
87 - before { error_report.generate_notice! }  
88 - subject { error_report.notice }  
89 - its(:message) { 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' }  
90 - its(:framework) { should == 'Rails: 3.2.11' } 89 + it 'has complete backtrace' do
  90 + expect(subject.backtrace_lines.size).to eq 73
  91 + expect(subject.backtrace_lines.last['file']).to eq '[GEM_ROOT]/bin/rake'
  92 + end
91 93
92 - it 'has complete backtrace' do  
93 - expect(subject.backtrace_lines.size).to eq 73  
94 - expect(subject.backtrace_lines.last['file']).to eq '[GEM_ROOT]/bin/rake'  
95 - end  
96 - it 'has server_environement' do  
97 - expect(subject.server_environment['environment-name']).to eq 'development'  
98 - end 94 + it 'has server_environement' do
  95 + expect(subject.server_environment['environment-name']).to eq 'development'
  96 + end
99 97
100 - it 'has request' do  
101 - expect(subject.request['url']).to eq 'http://example.org/verify/cupcake=fistfight&lovebird=doomsayer'  
102 - expect(subject.request['params']['controller']).to eq 'application'  
103 - end 98 + it 'has request' do
  99 + expect(subject.request['url']).to eq 'http://example.org/verify/cupcake=fistfight&lovebird=doomsayer'
  100 + expect(subject.request['params']['controller']).to eq 'application'
  101 + end
104 102
105 - it 'has notifier' do  
106 - expect(subject.notifier['name']).to eq 'Hoptoad Notifier'  
107 - end 103 + it 'has notifier' do
  104 + expect(subject.notifier['name']).to eq 'Hoptoad Notifier'
  105 + end
108 106
109 - it 'get user_attributes' do  
110 - expect(subject.user_attributes['id']).to eq '123'  
111 - expect(subject.user_attributes['name']).to eq 'Mr. Bean'  
112 - expect(subject.user_attributes['email']).to eq 'mr.bean@example.com'  
113 - expect(subject.user_attributes['username']).to eq 'mrbean'  
114 - end 107 + it 'get user_attributes' do
  108 + expect(subject.user_attributes['id']).to eq '123'
  109 + expect(subject.user_attributes['name']).to eq 'Mr. Bean'
  110 + expect(subject.user_attributes['email']).to eq 'mr.bean@example.com'
  111 + expect(subject.user_attributes['username']).to eq 'mrbean'
  112 + end
115 113
116 - it 'valid env_vars' do  
117 - # XML: <var key="SCRIPT_NAME"/>  
118 - expect(subject.env_vars).to have_key('SCRIPT_NAME')  
119 - expect(subject.env_vars['SCRIPT_NAME']).to be_nil # blank ends up nil  
120 -  
121 - # XML representation:  
122 - # <var key="rack.session.options">  
123 - # <var key="secure">false</var>  
124 - # <var key="httponly">true</var>  
125 - # <var key="path">/</var>  
126 - # <var key="expire_after"/>  
127 - # <var key="domain"/>  
128 - # <var key="id"/>  
129 - # </var>  
130 - expected = {  
131 - 'secure' => 'false',  
132 - 'httponly' => 'true',  
133 - 'path' => '/',  
134 - 'expire_after' => nil,  
135 - 'domain' => nil,  
136 - 'id' => nil  
137 - }  
138 - expect(subject.env_vars).to have_key('rack_session_options')  
139 - expect(subject.env_vars['rack_session_options']).to eql(expected)  
140 - end 114 + it 'valid env_vars' do
  115 + # XML: <var key="SCRIPT_NAME"/>
  116 + expect(subject.env_vars).to have_key('SCRIPT_NAME')
  117 + expect(subject.env_vars['SCRIPT_NAME']).to be_nil # blank ends up nil
  118 +
  119 + # XML representation:
  120 + # <var key="rack.session.options">
  121 + # <var key="secure">false</var>
  122 + # <var key="httponly">true</var>
  123 + # <var key="path">/</var>
  124 + # <var key="expire_after"/>
  125 + # <var key="domain"/>
  126 + # <var key="id"/>
  127 + # </var>
  128 + expected = {
  129 + 'secure' => 'false',
  130 + 'httponly' => 'true',
  131 + 'path' => '/',
  132 + 'expire_after' => nil,
  133 + 'domain' => nil,
  134 + 'id' => nil
  135 + }
  136 + expect(subject.env_vars).to have_key('rack_session_options')
  137 + expect(subject.env_vars['rack_session_options']).to eql(expected)
141 end 138 end
142 end 139 end
  140 + end
143 141
144 - it 'save a notice assignes to err' do 142 + describe '#cache_attributes_on_problem' do
  143 + it 'sets the latest notice properties on the problem' do
145 error_report.generate_notice! 144 error_report.generate_notice!
146 - expect(error_report.notice.err).to be_a(Err) 145 + problem = error_report.problem.reload
  146 + notice = error_report.notice.reload
  147 +
  148 + expect(problem.environment).to eq('development')
  149 + expect(problem.error_class).to eq('HoptoadTestingException')
  150 + expect(problem.last_notice_at).to eq(notice.created_at)
  151 + expect(problem.message).to eq(notice.message)
  152 + expect(problem.where).to eq(notice.where)
147 end 153 end
148 154
149 - it 'memoize the notice' do  
150 - expect {  
151 - error_report.generate_notice!  
152 - error_report.generate_notice!  
153 - }.to change {  
154 - Notice.count  
155 - }.by(1) 155 + it 'unresolves the problem' do
  156 + error_report.generate_notice!
  157 + problem = error_report.problem
  158 + problem.update(
  159 + resolved_at: Time.now,
  160 + resolved: true
  161 + )
  162 +
  163 + error_report = ErrorReport.new(xml)
  164 + error_report.generate_notice!
  165 + problem.reload
  166 +
  167 + expect(problem.resolved_at).to be(nil)
  168 + expect(problem.resolved).to be(false)
156 end 169 end
157 170
158 - it 'find the correct err for the notice' do  
159 - err = Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)) 171 + it 'caches notice counts' do
  172 + error_report.generate_notice!
  173 + problem = error_report.problem
  174 + problem.reload
  175 +
  176 + expect(problem.notices_count).to be(1)
  177 + expect(problem.user_agents['382b0f5185773fa0f67a8ed8056c7759']['count']).to be(1)
  178 + expect(problem.messages['9449f087eee0499e2d9029ae3dacaf53']['count']).to be(1)
  179 + expect(problem.hosts['1bdf72e04d6b50c82a48c7e4dd38cc69']['count']).to be(1)
  180 + end
160 181
161 - allow(error_report).to receive(:fingerprint).and_return(err.fingerprint) 182 + it 'increments notice counts' do
  183 + error_report.generate_notice!
  184 + error_report = ErrorReport.new(xml)
  185 + error_report.generate_notice!
  186 + problem = error_report.problem
  187 + problem.reload
162 188
163 - expect {  
164 - error_report.generate_notice!  
165 - }.to change {  
166 - error_report.error.resolved?  
167 - }.from(true).to(false) 189 + expect(problem.notices_count).to be(2)
  190 + expect(problem.user_agents['382b0f5185773fa0f67a8ed8056c7759']['count']).to be(2)
  191 + expect(problem.messages['9449f087eee0499e2d9029ae3dacaf53']['count']).to be(2)
  192 + expect(problem.hosts['1bdf72e04d6b50c82a48c7e4dd38cc69']['count']).to be(2)
168 end 193 end
  194 + end
169 195
170 - context "with notification service configured" do  
171 - before do  
172 - app.notify_on_errs = true  
173 - app.watchers.build(:email => 'foo@example.com')  
174 - app.save  
175 - end  
176 - it 'send email' do  
177 - notice = error_report.generate_notice!  
178 - email = ActionMailer::Base.deliveries.last  
179 - expect(email.to).to include(app.watchers.first.email)  
180 - expect(email.subject).to include(notice.message.truncate(50))  
181 - expect(email.subject).to include("[#{app.name}]")  
182 - expect(email.subject).to include("[#{notice.environment_name}]")  
183 - end 196 + it 'save a notice assignes to err' do
  197 + error_report.generate_notice!
  198 + expect(error_report.notice.err).to be_a(Err)
  199 + end
184 200
185 - context "with xml without request section" do  
186 - let(:xml){  
187 - Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read  
188 - }  
189 - it "save a notice" do  
190 - expect {  
191 - error_report.generate_notice!  
192 - }.to change {  
193 - app.reload.problems.count  
194 - }.by(1)  
195 - end 201 + it 'memoize the notice' do
  202 + expect {
  203 + error_report.generate_notice!
  204 + error_report.generate_notice!
  205 + }.to change {
  206 + Notice.count
  207 + }.by(1)
  208 + end
  209 +
  210 + it 'find the correct err for the notice' do
  211 + error_report.generate_notice!
  212 + error_report.problem.resolve!
  213 +
  214 + expect {
  215 + ErrorReport.new(xml).generate_notice!
  216 + }.to change {
  217 + error_report.problem.reload.resolved?
  218 + }.from(true).to(false)
  219 + end
  220 +
  221 + context "with notification service configured" do
  222 + before do
  223 + app.notify_on_errs = true
  224 + app.watchers.build(:email => 'foo@example.com')
  225 + app.save
  226 + end
  227 +
  228 + it 'send email' do
  229 + notice = error_report.generate_notice!
  230 + email = ActionMailer::Base.deliveries.last
  231 + expect(email.to).to include(app.watchers.first.email)
  232 + expect(email.subject).to include(notice.message.truncate(50))
  233 + expect(email.subject).to include("[#{app.name}]")
  234 + expect(email.subject).to include("[#{notice.environment_name}]")
  235 + end
  236 +
  237 + context "with xml without request section" do
  238 + let(:xml){
  239 + Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read
  240 + }
  241 + it "save a notice" do
  242 + expect {
  243 + error_report.generate_notice!
  244 + }.to change {
  245 + app.reload.problems.count
  246 + }.by(1)
196 end 247 end
  248 + end
197 249
198 - context "with xml with only a single line of backtrace" do  
199 - let(:xml){  
200 - Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read  
201 - }  
202 - it "save a notice" do  
203 - expect {  
204 - error_report.generate_notice!  
205 - }.to change {  
206 - app.reload.problems.count  
207 - }.by(1)  
208 - end 250 + context "with xml with only a single line of backtrace" do
  251 + let(:xml){
  252 + Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read
  253 + }
  254 + it "save a notice" do
  255 + expect {
  256 + error_report.generate_notice!
  257 + }.to change {
  258 + app.reload.problems.count
  259 + }.by(1)
209 end 260 end
210 end 261 end
  262 + end
211 263
212 - describe "#valid?" do  
213 - context "with valid error report" do  
214 - it "return true" do  
215 - expect(error_report.valid?).to be true  
216 - end 264 + describe "#valid?" do
  265 + context "with valid error report" do
  266 + it "return true" do
  267 + expect(error_report.valid?).to be true
217 end 268 end
218 - context "with not valid api_key" do  
219 - before do  
220 - App.where(:api_key => app.api_key).delete_all  
221 - end  
222 - it "return false" do  
223 - expect(error_report.valid?).to be false  
224 - end 269 + end
  270 + context "with not valid api_key" do
  271 + before do
  272 + App.where(:api_key => app.api_key).delete_all
  273 + end
  274 + it "return false" do
  275 + expect(error_report.valid?).to be false
225 end 276 end
226 end 277 end
  278 + end
227 279
228 - describe "#notice" do  
229 - context "before generate_notice!" do  
230 - it 'return nil' do  
231 - expect(error_report.notice).to be nil  
232 - end 280 + describe "#notice" do
  281 + context "before generate_notice!" do
  282 + it 'return nil' do
  283 + expect(error_report.notice).to be nil
233 end 284 end
  285 + end
234 286
235 - context "after generate_notice!" do  
236 - before do  
237 - error_report.generate_notice!  
238 - end  
239 -  
240 - it 'return the notice' do  
241 - expect(error_report.notice).to be_a Notice  
242 - end 287 + context "after generate_notice!" do
  288 + before do
  289 + error_report.generate_notice!
  290 + end
243 291
  292 + it 'return the notice' do
  293 + expect(error_report.notice).to be_a Notice
244 end 294 end
  295 +
245 end 296 end
  297 + end
246 298
247 - describe "#should_keep?" do  
248 - context "with current app version not set" do  
249 - before do  
250 - error_report.app.current_app_version = nil  
251 - error_report.server_environment['app-version'] = '1.0'  
252 - end 299 + describe "#should_keep?" do
  300 + context "with current app version not set" do
  301 + before do
  302 + error_report.app.current_app_version = nil
  303 + error_report.server_environment['app-version'] = '1.0'
  304 + end
253 305
254 - it "return true" do  
255 - expect(error_report.should_keep?).to be true  
256 - end 306 + it "return true" do
  307 + expect(error_report.should_keep?).to be true
257 end 308 end
  309 + end
258 310
259 - context "with current app version set" do  
260 - before do  
261 - error_report.app.current_app_version = '1.0'  
262 - end 311 + context "with current app version set" do
  312 + before do
  313 + error_report.app.current_app_version = '1.0'
  314 + end
263 315
264 - it "return true if current or newer" do  
265 - error_report.server_environment['app-version'] = '1.0'  
266 - expect(error_report.should_keep?).to be true  
267 - end 316 + it "return true if current or newer" do
  317 + error_report.server_environment['app-version'] = '1.0'
  318 + expect(error_report.should_keep?).to be true
  319 + end
268 320
269 - it "return false if older" do  
270 - error_report.server_environment['app-version'] = '0.9'  
271 - expect(error_report.should_keep?).to be false  
272 - end 321 + it "return false if older" do
  322 + error_report.server_environment['app-version'] = '0.9'
  323 + expect(error_report.should_keep?).to be false
273 end 324 end
274 end 325 end
275 end 326 end
spec/models/fingerprint/md5_spec.rb
1 describe Fingerprint::MD5, type: 'model' do 1 describe Fingerprint::MD5, type: 'model' do
2 context 'being created' do 2 context 'being created' do
3 let(:backtrace) do 3 let(:backtrace) do
4 - Backtrace.create(:raw => [ 4 + Backtrace.find_or_create([
5 { 5 {
6 "number"=>"17", 6 "number"=>"17",
7 "file"=>"[GEM_ROOT]/gems/activesupport/lib/active_support/callbacks.rb", 7 "file"=>"[GEM_ROOT]/gems/activesupport/lib/active_support/callbacks.rb",
@@ -14,10 +14,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do @@ -14,10 +14,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do
14 14
15 context "with same backtrace" do 15 context "with same backtrace" do
16 let(:backtrace_2) do 16 let(:backtrace_2) do
17 - backtrace  
18 - backtrace.lines.last.method = '_run__FRAGMENT__process_action__FRAGMENT__callbacks'  
19 - backtrace.save  
20 - backtrace 17 + new_lines = backtrace.lines.dup
  18 + new_lines.last[:method] = '_run__FRAGMENT__process_action__FRAGMENT__callbacks'
  19 + Backtrace.find_or_create(new_lines)
21 end 20 end
22 21
23 it "normalizes the fingerprint of generated methods" do 22 it "normalizes the fingerprint of generated methods" do
@@ -27,10 +26,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do @@ -27,10 +26,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do
27 26
28 context "with same backtrace where FRAGMENT has not been extracted" do 27 context "with same backtrace where FRAGMENT has not been extracted" do
29 let(:backtrace_2) do 28 let(:backtrace_2) do
30 - backtrace  
31 - backtrace.lines.last.method = '_run__998857585768765__process_action__1231231312321313__callbacks'  
32 - backtrace.save  
33 - backtrace 29 + new_lines = backtrace.lines.dup
  30 + new_lines.last[:method] = '_run__998857585768765__process_action__1231231312321313__callbacks'
  31 + Backtrace.find_or_create(new_lines)
34 end 32 end
35 33
36 it "normalizes the fingerprint of generated methods" do 34 it "normalizes the fingerprint of generated methods" do
spec/models/fingerprint/sha1_spec.rb
1 describe Fingerprint::Sha1, type: 'model' do 1 describe Fingerprint::Sha1, type: 'model' do
2 context '#generate' do 2 context '#generate' do
3 let(:backtrace) { 3 let(:backtrace) {
4 - Backtrace.create(:raw => [ 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"}, 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"}, 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"} 7 {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"_run_process_action_callbacks"}
@@ -19,11 +19,9 @@ describe Fingerprint::Sha1, type: &#39;model&#39; do @@ -19,11 +19,9 @@ describe Fingerprint::Sha1, type: &#39;model&#39; do
19 19
20 context "with different backtrace with only last line change" do 20 context "with different backtrace with only last line change" do
21 let(:backtrace_2) { 21 let(:backtrace_2) {
22 - backtrace  
23 - backtrace.lines.last.number = 401  
24 - backtrace.send(:generate_fingerprint)  
25 - backtrace.save  
26 - backtrace 22 + new_lines = backtrace.lines.dup
  23 + new_lines.last[:number] = 401
  24 + Backtrace.find_or_create backtrace.lines
27 } 25 }
28 it 'should not same fingerprint' do 26 it 'should not same fingerprint' do
29 expect( 27 expect(
spec/models/notice_observer_spec.rb
1 describe "Callback on Notice", type: 'model' do 1 describe "Callback on Notice", type: 'model' do
2 - describe "email notifications (configured individually for each app)" do 2 + let(:notice_attrs_for) do
  3 + ->(api_key) do
  4 + {
  5 + error_class: "HoptoadTestingException",
  6 + message: "some message",
  7 + backtrace: [
  8 + {
  9 + "number"=>"425",
  10 + "file"=>"[GEM_ROOT]/callbacks.rb",
  11 + "method"=>"__callbacks"
  12 + }
  13 + ],
  14 + request: { "component" => "application" },
  15 + server_environment: {
  16 + "project-root" => "/path/to/sample/project",
  17 + "environment-name" => "development"
  18 + },
  19 + api_key: api_key,
  20 + notifier: {
  21 + "name"=>"Hoptoad Notifier",
  22 + "version"=>"2.3.2",
  23 + "url"=>"http://hoptoadapp.com"
  24 + },
  25 + framework: "Rails: 3.2.11"
  26 + }
  27 + end
  28 + end
  29 +
  30 + describe 'email notifications (configured individually for each app)' do
  31 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
3 custom_thresholds = [2, 4, 8, 16, 32, 64] 32 custom_thresholds = [2, 4, 8, 16, 32, 64]
  33 + let(:app) do
  34 + Fabricate(:app_with_watcher, email_at_notices: custom_thresholds)
  35 + end
4 36
5 before do 37 before do
6 Errbit::Config.per_app_email_at_notices = true 38 Errbit::Config.per_app_email_at_notices = true
7 - @app = Fabricate(:app_with_watcher, :email_at_notices => custom_thresholds)  
8 - @err = Fabricate(:err, :problem => Fabricate(:problem, :app => @app)) 39 + error_report = ErrorReport.new(notice_attrs)
  40 + error_report.generate_notice!
  41 + @problem = error_report.notice.err.problem
9 end 42 end
10 43
11 - after do  
12 - Errbit::Config.per_app_email_at_notices = false  
13 - end 44 + after { Errbit::Config.per_app_email_at_notices = false }
14 45
15 custom_thresholds.each do |threshold| 46 custom_thresholds.each do |threshold|
16 it "sends an email notification after #{threshold} notice(s)" do 47 it "sends an email notification after #{threshold} notice(s)" do
17 - allow(@err.problem).to receive(:notices_count).and_return(threshold) 48 + # set to just before the threshold
  49 + @problem.update_attributes notices_count: threshold - 1
  50 +
18 expect(Mailer).to receive(:err_notification). 51 expect(Mailer).to receive(:err_notification).
19 and_return(double('email', :deliver => true)) 52 and_return(double('email', :deliver => true))
20 - Fabricate(:notice, :err => @err) 53 +
  54 + error_report = ErrorReport.new(notice_attrs)
  55 + error_report.generate_notice!
21 end 56 end
22 end 57 end
23 - end  
24 58
25 - describe "email notifications for a resolved issue" do  
26 - before do  
27 - Errbit::Config.per_app_email_at_notices = true  
28 - @app = Fabricate(:app_with_watcher, :email_at_notices => [1])  
29 - @err = Fabricate(:err, :problem => Fabricate(:problem, :app => @app, :notices_count => 100))  
30 - end 59 + it "doesn't email after 5 notices" do
  60 + @problem.update_attributes notices_count: 5
31 61
32 - after do  
33 - Errbit::Config.per_app_email_at_notices = false  
34 - end 62 + expect(Mailer).to_not receive(:err_notification)
35 63
36 - it "should send email notification after 1 notice since an error has been resolved" do  
37 - @err.problem.resolve!  
38 - expect(Mailer).to receive(:err_notification).  
39 - and_return(double('email', :deliver => true))  
40 - Fabricate(:notice, :err => @err) 64 + error_report = ErrorReport.new(notice_attrs)
  65 + error_report.generate_notice!
41 end 66 end
42 - it 'self notify if mailer failed' do  
43 - @err.problem.resolve!  
44 - expect(Mailer).to receive(:err_notification).  
45 - and_raise(ArgumentError) 67 +
  68 + it 'notify self if mailer fails' do
  69 + expect(Mailer).to receive(:err_notification).and_raise(ArgumentError)
46 expect(HoptoadNotifier).to receive(:notify) 70 expect(HoptoadNotifier).to receive(:notify)
47 - Fabricate(:notice, :err => @err) 71 + ErrorReport.new(notice_attrs).generate_notice!
48 end 72 end
49 end 73 end
50 74
51 - describe "should send a notification if a notification service is configured with defaults" do  
52 - let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))}  
53 - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }  
54 - let(:backtrace) { Fabricate(:backtrace) }  
55 -  
56 - before do  
57 - Errbit::Config.per_app_email_at_notices = true 75 + describe 'email notifications for resolved issues' do
  76 + let(:notification_service) { Fabricate(:campfire_notification_service) }
  77 + let(:app) do
  78 + Fabricate(
  79 + :app_with_watcher,
  80 + notify_on_errs: true,
  81 + email_at_notices: [1,100]
  82 + )
58 end 83 end
  84 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
59 85
60 - after do  
61 - Errbit::Config.per_app_email_at_notices = false  
62 - end 86 + before { Errbit::Config.per_app_email_at_notices = true }
  87 + after { Errbit::Config.per_app_email_at_notices = false }
63 88
64 - it "should create a campfire notification" do  
65 - expect(app.notification_service).to receive(:create_notification) 89 + it 'sends email the first time after the error is resolved' do
  90 + error_report = ErrorReport.new(notice_attrs)
  91 + error_report.generate_notice!
  92 + err = error_report.notice.err
66 93
67 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
68 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' })  
69 - end  
70 - end 94 + err.problem.update_attributes notices_count: 99
  95 + err.problem.resolve!
71 96
72 - describe "send a notification if a notification service is configured with defaults but failed" do  
73 - let(:app) { Fabricate(:app_with_watcher,  
74 - :notify_on_errs => true,  
75 - :email_at_notices => [1, 100], :notification_service => Fabricate(:campfire_notification_service))}  
76 - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 99)) }  
77 - let(:backtrace) { Fabricate(:backtrace) } 97 + expect(Mailer).to receive(:err_notification)
  98 + .and_return(double('email', :deliver => true))
78 99
79 - before do  
80 - Errbit::Config.per_app_email_at_notices = true 100 + ErrorReport.new(notice_attrs).generate_notice!
81 end 101 end
  102 + end
82 103
83 - after do  
84 - Errbit::Config.per_app_email_at_notices = false 104 + describe 'send email when notification service is configured but fails' do
  105 + let(:notification_service) {Fabricate(:campfire_notification_service)}
  106 + let(:app) do
  107 + Fabricate(
  108 + :app_with_watcher,
  109 + notify_on_errs: true,
  110 + notification_service: notification_service
  111 + )
85 end 112 end
  113 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
  114 +
  115 + before { Errbit::Config.per_app_notify_at_notices = true }
  116 + after { Errbit::Config.per_app_notify_at_notices = false }
  117 +
  118 + it 'sends email' do
  119 + error_report = ErrorReport.new(notice_attrs)
86 120
87 - it "send email" do  
88 - expect(app.notification_service).to receive(:create_notification).and_raise(ArgumentError)  
89 - expect(Mailer).to receive(:err_notification).and_return(double(:deliver => true)) 121 + expect(error_report.app.notification_service)
  122 + .to receive(:create_notification).and_raise(ArgumentError)
  123 + expect(Mailer)
  124 + .to receive(:err_notification).and_return(double(:deliver => true))
90 125
91 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
92 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) 126 + error_report.generate_notice!
93 end 127 end
94 end 128 end
95 129
96 - describe "should not send a notification if a notification service is not configured" do  
97 - let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:notification_service))}  
98 - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }  
99 - let(:backtrace) { Fabricate(:backtrace) } 130 + describe 'should not send a notification if a notification service is not' \
  131 + 'configured' do
100 132
101 - before do  
102 - Errbit::Config.per_app_email_at_notices = true  
103 - end 133 + let(:notification_service) { Fabricate(:notification_service) }
  134 + let(:app) { Fabricate(:app, notification_service: notification_service )}
  135 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
104 136
105 - after do  
106 - Errbit::Config.per_app_email_at_notices = false  
107 - end 137 + before { Errbit::Config.per_app_notify_at_notices = true }
  138 + after { Errbit::Config.per_app_notify_at_notices = false }
108 139
109 it "should not create a campfire notification" do 140 it "should not create a campfire notification" do
110 - expect(app.notification_service).to_not receive(:create_notification)  
111 -  
112 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
113 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) 141 + error_report = ErrorReport.new(notice_attrs)
  142 + expect(error_report.app.notification_service).to_not receive(:create_notification)
  143 + error_report.generate_notice!
114 end 144 end
115 end 145 end
116 146
117 describe 'hipcat notifications' do 147 describe 'hipcat notifications' do
118 - let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:hipchat_notification_service))}  
119 - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) } 148 + let(:notification_service) { Fabricate(:hipchat_notification_service) }
  149 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
  150 + let(:app) { Fabricate(:app, notification_service: notification_service) }
120 151
121 - before do  
122 - Errbit::Config.per_app_email_at_notices = true  
123 - end  
124 -  
125 - after do  
126 - Errbit::Config.per_app_email_at_notices = false  
127 - end 152 + before { Errbit::Config.per_app_notify_at_notices = true }
  153 + after { Errbit::Config.per_app_notify_at_notices = false }
128 154
129 it 'creates a hipchat notification' do 155 it 'creates a hipchat notification' do
130 - expect(app.notification_service).to receive(:create_notification)  
131 -  
132 - Fabricate(:notice, :err => err) 156 + error_report = ErrorReport.new(notice_attrs)
  157 + expect(error_report.app.notification_service)
  158 + .to receive(:create_notification)
  159 + error_report.generate_notice!
133 end 160 end
134 end 161 end
135 162
136 describe "should send a notification at desired intervals" do 163 describe "should send a notification at desired intervals" do
137 - let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service, :notify_at_notices => [1,2]))}  
138 - let(:backtrace) { Fabricate(:backtrace) }  
139 -  
140 - before do  
141 - Errbit::Config.per_app_email_at_notices = true 164 + let(:notification_service) do
  165 + Fabricate(:campfire_notification_service, notify_at_notices: [1,2])
142 end 166 end
  167 + let(:app) { Fabricate(:app, notification_service: notification_service) }
  168 + let(:notice_attrs) { notice_attrs_for.call(app.api_key) }
143 169
144 - after do  
145 - Errbit::Config.per_app_email_at_notices = false  
146 - end 170 + before { Errbit::Config.per_app_notify_at_notices = true }
  171 + after { Errbit::Config.per_app_notify_at_notices = false }
147 172
148 it "should create a campfire notification on first notice" do 173 it "should create a campfire notification on first notice" do
149 - err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1))  
150 - expect(app.notification_service).to receive(:create_notification)  
151 -  
152 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
153 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) 174 + error_report = ErrorReport.new(notice_attrs)
  175 + expect(error_report.app.notification_service)
  176 + .to receive(:create_notification)
  177 + error_report.generate_notice! # one
154 end 178 end
155 179
156 it "should create a campfire notification on second notice" do 180 it "should create a campfire notification on second notice" do
157 - err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1))  
158 - expect(app.notification_service).to receive(:create_notification)  
159 -  
160 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
161 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) 181 + ErrorReport.new(notice_attrs).generate_notice! # one
  182 + error_report = ErrorReport.new(notice_attrs)
  183 + expect(error_report.app.notification_service)
  184 + .to receive(:create_notification)
  185 + error_report.generate_notice! # two
162 end 186 end
163 187
164 it "should not create a campfire notification on third notice" do 188 it "should not create a campfire notification on third notice" do
165 - err = Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 1))  
166 - expect(app.notification_service).to receive(:create_notification)  
167 -  
168 - Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},  
169 - :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }) 189 + ErrorReport.new(notice_attrs).generate_notice! # one
  190 + ErrorReport.new(notice_attrs).generate_notice! # two
  191 + error_report = ErrorReport.new(notice_attrs)
  192 + expect(error_report.app.notification_service)
  193 + .to_not receive(:create_notification)
  194 + error_report.generate_notice! # three
170 end 195 end
171 end 196 end
172 end 197 end
spec/models/notice_spec.rb
@@ -3,7 +3,7 @@ describe Notice, type: &#39;model&#39; do @@ -3,7 +3,7 @@ describe Notice, type: &#39;model&#39; do
3 it 'requires a backtrace' do 3 it 'requires a backtrace' do
4 notice = Fabricate.build(:notice, :backtrace => nil) 4 notice = Fabricate.build(:notice, :backtrace => nil)
5 expect(notice).to_not be_valid 5 expect(notice).to_not be_valid
6 - expect(notice.errors[:backtrace]).to include("can't be blank") 6 + expect(notice.errors[:backtrace_id]).to include("can't be blank")
7 end 7 end
8 8
9 it 'requires the server_environment' do 9 it 'requires the server_environment' do
spec/models/problem_spec.rb
@@ -40,10 +40,10 @@ describe Problem, type: &#39;model&#39; do @@ -40,10 +40,10 @@ describe Problem, type: &#39;model&#39; do
40 expect(problem).to_not be_nil 40 expect(problem).to_not be_nil
41 41
42 notice1 = Fabricate(:notice, :err => err) 42 notice1 = Fabricate(:notice, :err => err)
43 - expect(problem.last_notice_at).to eq notice1.created_at 43 + expect(problem.last_notice_at).to eq notice1.reload.created_at
44 44
45 notice2 = Fabricate(:notice, :err => err) 45 notice2 = Fabricate(:notice, :err => err)
46 - expect(problem.last_notice_at).to eq notice2.created_at 46 + expect(problem.last_notice_at).to eq notice2.reload.created_at
47 end 47 end
48 end 48 end
49 49
@@ -266,12 +266,6 @@ describe Problem, type: &#39;model&#39; do @@ -266,12 +266,6 @@ describe Problem, type: &#39;model&#39; do
266 expect(@problem.messages).to eq ({}) 266 expect(@problem.messages).to eq ({})
267 end 267 end
268 268
269 - it "adding a notice adds a string to #messages" do  
270 - expect {  
271 - Fabricate(:notice, :err => @err, :message => 'ERR 1')  
272 - }.to change(@problem, :messages).from({}).to({Digest::MD5.hexdigest('ERR 1') => {'value' => 'ERR 1', 'count' => 1}})  
273 - end  
274 -  
275 it "removing a notice removes string from #messages" do 269 it "removing a notice removes string from #messages" do
276 Fabricate(:notice, :err => @err, :message => 'ERR 1') 270 Fabricate(:notice, :err => @err, :message => 'ERR 1')
277 expect { 271 expect {
@@ -299,12 +293,6 @@ describe Problem, type: &#39;model&#39; do @@ -299,12 +293,6 @@ describe Problem, type: &#39;model&#39; do
299 expect(@problem.hosts).to eq ({}) 293 expect(@problem.hosts).to eq ({})
300 end 294 end
301 295
302 - it "adding a notice adds a string to #hosts" do  
303 - expect {  
304 - Fabricate(:notice, :err => @err, :request => {'url' => "http://example.com/resource/12"})  
305 - }.to change(@problem, :hosts).from({}).to({Digest::MD5.hexdigest('example.com') => {'value' => 'example.com', 'count' => 1}})  
306 - end  
307 -  
308 it "removing a notice removes string from #hosts" do 296 it "removing a notice removes string from #hosts" do
309 Fabricate(:notice, :err => @err, :request => {'url' => "http://example.com/resource/12"}) 297 Fabricate(:notice, :err => @err, :request => {'url' => "http://example.com/resource/12"})
310 expect { 298 expect {
@@ -325,12 +313,6 @@ describe Problem, type: &#39;model&#39; do @@ -325,12 +313,6 @@ describe Problem, type: &#39;model&#39; do
325 expect(@problem.user_agents).to eq ({}) 313 expect(@problem.user_agents).to eq ({})
326 end 314 end
327 315
328 - it "adding a notice adds a string to #user_agents" do  
329 - expect {  
330 - Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}})  
331 - }.to change(@problem, :user_agents).from({}).to({Digest::MD5.hexdigest('Chrome 10.0.648.204 (OS X 10.6.7)') => {'value' => 'Chrome 10.0.648.204 (OS X 10.6.7)', 'count' => 1}})  
332 - end  
333 -  
334 it "removing a notice removes string from #user_agents" do 316 it "removing a notice removes string from #user_agents" do
335 Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) 317 Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}})
336 expect { 318 expect {
spec/spec_helper.rb
1 # This file is copied to ~/spec when you run 'ruby script/generate rspec' 1 # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2 # from the project root directory. 2 # from the project root directory.
3 -ENV["RAILS_ENV"] ||= 'test' 3 +ENV["RAILS_ENV"] = 'test'
  4 +ENV["ERRBIT_LOG_LEVEL"] = 'fatal'
4 5
5 if ENV['COVERAGE'] 6 if ENV['COVERAGE']
6 require 'coveralls' 7 require 'coveralls'
@@ -21,7 +22,6 @@ require File.expand_path(&quot;../../config/environment&quot;, __FILE__) @@ -21,7 +22,6 @@ require File.expand_path(&quot;../../config/environment&quot;, __FILE__)
21 require 'rspec/rails' 22 require 'rspec/rails'
22 require 'rspec/its' 23 require 'rspec/its'
23 require 'email_spec' 24 require 'email_spec'
24 -require 'database_cleaner'  
25 require 'xmpp4r' 25 require 'xmpp4r'
26 require 'xmpp4r/muc' 26 require 'xmpp4r/muc'
27 require 'mongoid-rspec' 27 require 'mongoid-rspec'
@@ -31,6 +31,8 @@ require &#39;errbit_plugin/mock_issue_tracker&#39; @@ -31,6 +31,8 @@ require &#39;errbit_plugin/mock_issue_tracker&#39;
31 # Requires supporting files with custom matchers and macros, etc, 31 # Requires supporting files with custom matchers and macros, etc,
32 # in ./support/ and its subdirectories. 32 # in ./support/ and its subdirectories.
33 Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 33 Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
  34 +Mongoid::Config.truncate!
  35 +Mongoid::Tasks::Database.create_indexes
34 36
35 RSpec.configure do |config| 37 RSpec.configure do |config|
36 config.include Devise::TestHelpers, :type => :controller 38 config.include Devise::TestHelpers, :type => :controller
@@ -38,8 +40,7 @@ RSpec.configure do |config| @@ -38,8 +40,7 @@ RSpec.configure do |config|
38 config.alias_example_to :fit, :focused => true 40 config.alias_example_to :fit, :focused => true
39 41
40 config.before(:each) do 42 config.before(:each) do
41 - DatabaseCleaner[:mongoid].strategy = :truncation  
42 - DatabaseCleaner.clean 43 + Mongoid::Config.truncate!
43 end 44 end
44 45
45 config.include Haml, type: :helper 46 config.include Haml, type: :helper
spec/views/issue_trackers/issue.md.erb_spec.rb
@@ -6,7 +6,7 @@ describe &quot;issue_trackers/issue.md.erb&quot;, type: &#39;view&#39; do @@ -6,7 +6,7 @@ describe &quot;issue_trackers/issue.md.erb&quot;, type: &#39;view&#39; do
6 } 6 }
7 7
8 before do 8 before do
9 - allow(view).to receive(:problem).and_return(problem) 9 + allow(view).to receive(:problem).and_return(ProblemDecorator.new(problem))
10 end 10 end
11 11
12 it "has the problem url" do 12 it "has the problem url" do
spec/views/issue_trackers/issue.txt.erb_spec.rb
@@ -6,7 +6,8 @@ describe &quot;issue_trackers/issue.txt.erb&quot;, type: &#39;view&#39; do @@ -6,7 +6,8 @@ describe &quot;issue_trackers/issue.txt.erb&quot;, type: &#39;view&#39; do
6 } 6 }
7 7
8 before do 8 before do
9 - allow(view).to receive(:problem).and_return(problem) 9 + allow(view).to receive(:problem).and_return(
  10 + ProblemDecorator.new(problem))
10 end 11 end
11 12
12 it "has the problem url" do 13 it "has the problem url" do