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
.env.default
... ... @@ -15,6 +15,8 @@ ERRBIT_PER_APP_EMAIL_AT_NOTICES=false
15 15 ERRBIT_NOTIFY_AT_NOTICES='[0]'
16 16 ERRBIT_PER_APP_NOTIFY_AT_NOTICES=false
17 17 MONGO_URL='mongodb://localhost'
  18 +ERRBIT_LOG_LEVEL=info
  19 +ERRBIT_LOG_LOCATION=STDOUT
18 20 GITHUB_URL='https://github.com'
19 21 GITHUB_AUTHENTICATION=true
20 22 GITHUB_ACCESS_SCOPE='[repo]'
... ...
.travis.yml
... ... @@ -9,8 +9,12 @@ env:
9 9 - RAILS_ENV=test COVERAGE=true JRUBY_OPTS=--debug
10 10 sudo: false
11 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 18 script: bundle exec rspec
15 19 matrix:
16 20 allow_failures:
... ...
Gemfile
... ... @@ -8,10 +8,8 @@ gem 'actionmailer', RAILS_VERSION
8 8 gem 'actionpack', RAILS_VERSION
9 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 13 gem 'devise'
16 14 gem 'haml'
17 15 gem 'htmlentities'
... ... @@ -78,7 +76,7 @@ group :development do
78 76 end
79 77  
80 78 group :test do
81   - gem 'rspec'
  79 + gem 'rspec', '~> 3.3'
82 80 gem 'rspec-rails', '~> 3.0', require: false
83 81 gem 'rspec-activemodel-mocks'
84 82 gem 'rspec-its'
... ... @@ -87,7 +85,6 @@ group :test do
87 85 gem 'capybara'
88 86 gem 'poltergeist'
89 87 gem 'launchy'
90   - gem 'database_cleaner'
91 88 gem 'email_spec'
92 89 gem 'timecop'
93 90 gem 'test-unit', require: 'test/unit'
... ...
Gemfile.lock
... ... @@ -44,8 +44,8 @@ GEM
44 44 rack (>= 0.9.0)
45 45 binding_of_caller (0.7.2)
46 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 49 builder (3.2.2)
50 50 byebug (4.0.5)
51 51 columnize (= 0.9.0)
... ... @@ -81,7 +81,6 @@ GEM
81 81 coffee-script-source (1.9.1.1)
82 82 colorize (0.7.7)
83 83 columnize (0.9.0)
84   - connection_pool (2.2.0)
85 84 coveralls (0.8.2)
86 85 json (~> 1.8)
87 86 rest-client (>= 1.6.8, < 2)
... ... @@ -90,7 +89,6 @@ GEM
90 89 thor (~> 0.19.1)
91 90 css_parser (1.3.6)
92 91 addressable
93   - database_cleaner (1.4.1)
94 92 debug_inspector (0.0.2)
95 93 decent_exposure (2.3.2)
96 94 devise (3.5.1)
... ... @@ -175,25 +173,18 @@ GEM
175 173 mimemagic (0.3.0)
176 174 mini_portile (0.6.2)
177 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 179 activemodel (~> 4.0)
180   - moped (~> 2.0.0)
  180 + mongo (= 2.1.0.beta)
181 181 origin (~> 2.1)
182 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 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 188 multi_xml (0.5.5)
198 189 multipart-post (2.0.0)
199 190 net-scp (1.2.1)
... ... @@ -363,7 +354,7 @@ GEM
363 354 thread_safe (0.3.5-java)
364 355 tilt (1.4.1)
365 356 timecop (0.7.4)
366   - tins (1.5.4)
  357 + tins (1.6.0)
367 358 tzinfo (1.2.2)
368 359 thread_safe (~> 0.1)
369 360 uglifier (2.7.1)
... ... @@ -410,7 +401,6 @@ DEPENDENCIES
410 401 capybara
411 402 coffee-rails
412 403 coveralls
413   - database_cleaner
414 404 decent_exposure
415 405 devise
416 406 dotenv-rails
... ... @@ -431,10 +421,8 @@ DEPENDENCIES
431 421 kaminari (>= 0.14.1)
432 422 launchy
433 423 meta_request
434   - mongoid (~> 4.0.0)
  424 + mongoid (= 5.0.0.beta)
435 425 mongoid-rspec
436   - mongoid_rails_migrations
437   - moped (~> 2.0.2)
438 426 omniauth-github
439 427 pjax_rails
440 428 poltergeist
... ... @@ -447,7 +435,7 @@ DEPENDENCIES
447 435 rails_autolink
448 436 railties (~> 4.1.11)
449 437 ri_cal
450   - rspec
  438 + rspec (~> 3.3)
451 439 rspec-activemodel-mocks
452 440 rspec-its
453 441 rspec-rails (~> 3.0)
... ...
README.md
... ... @@ -71,7 +71,7 @@ updates and notifications.
71 71 The list of requirements to install Errbit are:
72 72  
73 73 * Ruby 2.1.0 or higher
74   -* MongoDB 2.2.0 or higher
  74 +* MongoDB 2.6.0 or higher
75 75  
76 76 Installation
77 77 ------------
... ... @@ -188,6 +188,8 @@ When upgrading Errbit, please run:
188 188 git pull origin master # assuming origin is the github.com/errbit/errbit repo
189 189 bundle install
190 190 rake db:migrate
  191 +rake db:mongoid:create_indexes
  192 +rake db:mongoid:remove_undefined_indexes
191 193 rake assets:precompile
192 194 ```
193 195  
... ...
app/controllers/notices_controller.rb
... ... @@ -2,7 +2,8 @@ class NoticesController &lt; ApplicationController
2 2  
3 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 8 rescue_from ParamsError, :with => :bad_params
8 9  
... ...
app/controllers/problems_controller.rb
... ... @@ -22,7 +22,7 @@ class ProblemsController &lt; ApplicationController
22 22 }
23 23  
24 24 expose(:problem) {
25   - app.problems.find(params[:id])
  25 + ProblemDecorator.new app.problems.find(params[:id])
26 26 }
27 27  
28 28 expose(:all_errs) {
... ... @@ -50,8 +50,9 @@ class ProblemsController &lt; ApplicationController
50 50 def index; end
51 51  
52 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 56 @comment = Comment.new
56 57 end
57 58  
... ...
app/decorators/backtrace_decorator.rb 0 → 100644
... ... @@ -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 @@
  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 @@
  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 @@
  1 +class ProblemDecorator < Draper::Decorator
  2 + decorates_association :notices
  3 + delegate_all
  4 +end
... ...
app/helpers/backtrace_line_helper.rb
1 1 module BacktraceLineHelper
2   - def link_to_source_file(line, &block)
  2 + def link_to_source_file(line, app, &block)
3 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 5 end
6 6  
7 7 private
8   - def link_to_in_app_source_file(line, text)
  8 + def link_to_in_app_source_file(line, app, text)
9 9 return unless line.in_app?
10 10 if line.file_name =~ /\.js$/
11   - link_to_hosted_javascript(line, text)
  11 + link_to_hosted_javascript(line, app, text)
12 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 15 end
16 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 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 25 end
26 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 31 link_to(text || line.file_name, href, :target => '_blank')
36 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 37 link_to(text || line.file_name, href, :target => '_blank')
42 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 43 link_to(text || line.file_name, href, :target => '_blank')
48 44 end
49 45  
... ...
app/interactors/resolved_problem_clearer.rb
... ... @@ -27,6 +27,6 @@ class ResolvedProblemClearer
27 27 end
28 28  
29 29 def repair_database
30   - Mongoid.default_session.command :repairDatabase => 1
  30 + Mongoid.default_client.command :repairDatabase => 1
31 31 end
32 32 end
... ...
app/mailers/mailer.rb
... ... @@ -13,11 +13,11 @@ class Mailer &lt; ActionMailer::Base
13 13 'Precedence' => 'bulk',
14 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 21 count = count > 1 ? "(#{count}) " : ""
22 22  
23 23 errbit_headers 'App' => @app.name,
... ... @@ -30,7 +30,7 @@ class Mailer &lt; ActionMailer::Base
30 30  
31 31 def deploy_notification(deploy)
32 32 @deploy = deploy
33   - @app = deploy.app
  33 + @app = AppDecorator.new deploy.app
34 34  
35 35 errbit_headers 'App' => @app.name,
36 36 'Environment' => @deploy.environment,
... ... @@ -44,7 +44,7 @@ class Mailer &lt; ActionMailer::Base
44 44 def comment_notification(comment)
45 45 @comment = comment
46 46 @user = comment.user
47   - @problem = comment.err
  47 + @problem = ProblemDecorator.new comment.err
48 48 @notice = @problem.notices.first
49 49 @app = @problem.app
50 50  
... ...
app/models/app.rb
... ... @@ -57,8 +57,14 @@ class App
57 57 def find_or_create_err!(attrs)
58 58 Err.where(
59 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 68 end
63 69  
64 70 # Mongoid Bug: find(id) on association proxies returns an Enumerator
... ... @@ -178,7 +184,9 @@ class App
178 184 protected
179 185  
180 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 190 end
183 191  
184 192 def generate_api_key
... ...
app/models/backtrace.rb
... ... @@ -2,35 +2,28 @@ class Backtrace
2 2 include Mongoid::Document
3 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 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 23 end
30 24  
31 25 private
32 26 def generate_fingerprint
33   - self.fingerprint = Digest::SHA1.hexdigest(lines.map(&:to_s).join)
  27 + self.fingerprint = self.class.generate_fingerprint(lines)
34 28 end
35   -
36 29 end
... ...
app/models/backtrace_line.rb
... ... @@ -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   -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 33 end
34 34  
35 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 39 end
38 40  
39 41 def deliver_email
... ...
app/models/error_report.rb
... ... @@ -15,8 +15,16 @@ require &#39;hoptoad_notifier&#39;
15 15 # * <tt>:notifier</tt> - information to identify the source of the error report
16 16 #
17 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 29 cattr_accessor :fingerprint_strategy do
22 30 Fingerprint::Sha1
... ... @@ -40,24 +48,59 @@ class ErrorReport
40 48 end
41 49  
42 50 def backtrace
43   - @normalized_backtrace ||= Backtrace.find_or_create(raw: @backtrace)
  51 + @normalized_backtrace ||= Backtrace.find_or_create(@backtrace)
44 52 end
45 53  
46 54 def generate_notice!
47 55 return unless valid?
48 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 67 @notice = Notice.new(
50 68 message: message,
51 69 error_class: error_class,
52   - backtrace_id: backtrace.id,
  70 + backtrace: backtrace,
53 71 request: request,
54 72 server_environment: server_environment,
55 73 notifier: notifier,
56 74 user_attributes: user_attributes,
57 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 104 end
62 105  
63 106 ##
... ...
app/models/notice.rb
... ... @@ -20,13 +20,10 @@ class Notice
20 20 index(:created_at => 1)
21 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 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 28 scope :ordered, ->{ order_by(:created_at.asc) }
32 29 scope :reverse_ordered, ->{ order_by(:created_at.desc) }
... ... @@ -106,26 +103,6 @@ class Notice
106 103 request['session'] || {}
107 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 107 # TODO: Move on decorator maybe
131 108 #
... ... @@ -143,20 +120,8 @@ class Notice
143 120  
144 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 125 end
161 126  
162 127 def sanitize
... ... @@ -165,7 +130,6 @@ class Notice
165 130 end
166 131 end
167 132  
168   -
169 133 def sanitize_hash(h)
170 134 h.recurse do |h|
171 135 h.inject({}) do |h,(k,v)|
... ... @@ -178,25 +142,4 @@ class Notice
178 142 end
179 143 end
180 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 145 end
... ...
app/models/notification_service.rb
... ... @@ -26,7 +26,7 @@ class NotificationService
26 26 else
27 27 Fields = []
28 28 end
29   -
  29 +
30 30 def notify_at_notices
31 31 Errbit::Config.per_app_notify_at_notices ? super : Errbit::Config.notify_at_notices
32 32 end
... ...
app/models/problem.rb
... ... @@ -6,8 +6,15 @@ class Problem
6 6 include Mongoid::Document
7 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 18 field :last_deploy_at, :type => Time
12 19 field :resolved, :type => Boolean, :default => false
13 20 field :resolved_at, :type => Time
... ... @@ -35,6 +42,14 @@ class Problem
35 42 index :resolved_at => 1
36 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 53 belongs_to :app
39 54 has_many :errs, :inverse_of => :problem, :dependent => :destroy
40 55 has_many :comments, :inverse_of => :err, :dependent => :destroy
... ... @@ -63,6 +78,83 @@ class Problem
63 78 env.present? ? where(:environment => env) : scoped
64 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 158 def url
67 159 Rails.application.routes.url_helpers.app_problem_url(app, self,
68 160 :host => Errbit::Config.host,
... ... @@ -98,16 +190,21 @@ class Problem
98 190 def unmerge!
99 191 attrs = {:error_class => error_class, :environment => environment}
100 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 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 209 def self.ordered_by(sort, order)
113 210 case sort
... ... @@ -124,20 +221,10 @@ class Problem
124 221 where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}])
125 222 end
126 223  
127   -
128   - def reset_cached_attributes
129   - ProblemUpdaterCache.new(self).update
130   - end
131   -
132 224 def cache_app_attributes
133 225 if app
134 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 228 end
142 229 end
143 230  
... ... @@ -145,14 +232,6 @@ class Problem
145 232 self.message = self.message[0, 1000] if self.message
146 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 235 def issue_type
157 236 # Return issue_type if configured, but fall back to detecting app's issue tracker
158 237 attributes['issue_type'] ||=
... ... @@ -160,13 +239,7 @@ class Problem
160 239 end
161 240  
162 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 243 end
171 244  
172 245 private
... ...
app/views/issue_trackers/issue.md.erb
... ... @@ -27,7 +27,7 @@
27 27  
28 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 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 16  
17 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 20 <% end %>
21 21 <% end %>
... ...
app/views/mailer/comment_notification.text.erb
... ... @@ -21,8 +21,8 @@ WHERE:
21 21  
22 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 26 <% end %>
27 27  
28 28  
... ...
app/views/mailer/err_notification.html.haml
... ... @@ -27,9 +27,10 @@
27 27 %p.heading WHERE:
28 28 %p.monospace
29 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 32 %p.backtrace
32   - = link_to_source_file(line) do
  33 + = line.link_to_source_file(@app) do
33 34 = line.to_s
34 35 %br
35 36 - if @notice.app_version.present?
... ... @@ -59,11 +60,11 @@
59 60 %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":"
60 61 %td= auto_link(value.to_s)
61 62 %br
62   - - if @notice.backtrace_lines.any?
  63 + - if @notice.backtrace.lines.any?
63 64 %br
64 65 %p.heading FULL BACKTRACE:
65   - - @notice.backtrace_lines.each do |line|
  66 + - @notice.backtrace.lines.each do |line|
66 67 %p.backtrace
67   - = link_to_source_file(line) do
  68 + = link_to_source_file(line, @app) do
68 69 = line.to_s
69 70 %br
... ...
app/views/mailer/err_notification.text.erb
... ... @@ -14,7 +14,8 @@ WHERE:
14 14  
15 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 19 <%= line %>
19 20 <% end %>
20 21  
... ... @@ -51,7 +52,7 @@ USER:
51 52  
52 53 BACKTRACE:
53 54  
54   -<% @notice.backtrace_lines.each do |line| %>
  55 +<% @notice.backtrace.lines.each do |line| %>
55 56 <%= line %>
56 57 <% end %>
57 58  
... ...
app/views/notices/_backtrace_line.html.haml
1 1 %tr{:class => defined?(row_class) && row_class}
2 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 4 %span.path>= raw line.decorated_path
5 5 %span.file>= line.file_name
6 6 - if line.number.present?
... ...
app/views/problems/show.html.haml
... ... @@ -68,7 +68,7 @@
68 68  
69 69 #backtrace
70 70 %h3 Backtrace
71   - = render 'notices/backtrace', :lines => @notice.backtrace_lines
  71 + = render 'notices/backtrace', :lines => @notice.backtrace.lines
72 72  
73 73 - if @notice.user_attributes.present?
74 74 #user_attributes
... ...
config/application.rb
... ... @@ -2,7 +2,7 @@ require File.expand_path(&#39;../boot&#39;, __FILE__)
2 2  
3 3 require 'action_controller/railtie'
4 4 require 'action_mailer/railtie'
5   -require 'mongoid/railtie'
  5 +# require 'mongoid/railtie'
6 6 require 'sprockets/railtie'
7 7  
8 8 # Require the gems listed in Gemfile, including any gems
... ... @@ -19,9 +19,6 @@ module Errbit
19 19 config.autoload_paths += [Rails.root.join('lib')]
20 20  
21 21 config.before_initialize do
22   - # Load up Errbit::Config with values from the environment
23   - require Rails.root.join('config/load')
24   -
25 22 config.secret_key_base = Errbit::Config.secret_key_base
26 23 config.serve_static_assets = Errbit::Config.serve_static_assets
27 24 end
... ...
config/environment.rb
1 1 # Load the Rails application.
2 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 15 # Initialize the Rails application.
5 16 Rails.application.initialize!
... ...
config/initializers/mongo.rb
config/load.rb
1 1 # load default ENV values (without overwriting any existing value)
2 2 Dotenv.load('.env.default')
3 3  
  4 +require_relative '../lib/configurator'
  5 +
4 6 # map config keys to environment variables
5 7 #
6 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 21 per_app_email_at_notices: ['ERRBIT_PER_APP_EMAIL_AT_NOTICES'],
20 22 notify_at_notices: ['ERRBIT_NOTIFY_AT_NOTICES'],
21 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 27 serve_static_assets: ['SERVE_STATIC_ASSETS'],
24 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 6 Mongoid.configure do |config|
2 7 uri = if Errbit::Config.mongo_url == 'mongodb://localhost'
3 8 "mongodb://localhost/errbit_#{Rails.env}"
... ... @@ -6,7 +11,7 @@ Mongoid.configure do |config|
6 11 end
7 12  
8 13 config.load_configuration({
9   - sessions: {
  14 + clients: {
10 15 default: {
11 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 52 fill_in 'app_name', :with => 'My new app'
53 53 click_on I18n.t('apps.new.add_app')
54 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 59 click_on I18n.t('shared.navigation.apps')
... ... @@ -62,8 +62,8 @@ feature &quot;Create an application&quot; do
62 62 fill_in 'app_name', :with => 'My new app 2'
63 63 click_on I18n.t('apps.edit.update')
64 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 68 end
69 69  
... ...
spec/controllers/problems_controller_spec.rb
... ... @@ -138,15 +138,13 @@ describe ProblemsController, type: &#39;controller&#39; do
138 138 end
139 139  
140 140 it "searches problems for given string" do
141   - get :search, :search => "Most important"
  141 + get :search, :search => "\"Most important\""
142 142 expect(controller.problems).to include(@problem1)
143 143 expect(controller.problems).to_not include(@problem2)
144 144 end
145 145 end
146 146  
147 147 describe "GET /apps/:app_id/problems/:id" do
148   - #render_views
149   -
150 148 context 'when logged in as an admin' do
151 149 before do
152 150 sign_in admin
... ... @@ -250,8 +248,8 @@ describe ProblemsController, type: &#39;controller&#39; do
250 248 before { sign_in admin }
251 249  
252 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 253 let(:issue_tracker) do
256 254 Fabricate(:issue_tracker).tap do |t|
257 255 t.instance_variable_set(:@tracker, ErrbitPlugin::MockIssueTracker.new(t.options))
... ...
spec/fabricators/err_fabricator.rb
... ... @@ -10,15 +10,19 @@ Fabricator :notice do
10 10 server_environment { {'environment-name' => 'production'} }
11 11 request {{ 'component' => 'foo', 'action' => 'bar' }}
12 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 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 28 end
24   -
... ...
spec/fabricators/problem_fabricator.rb
... ... @@ -23,8 +23,7 @@ end
23 23  
24 24 Fabricator(:problem_resolved, :from => :problem) do
25 25 after_create do |pr|
26   - Fabricate(:notice,
27   - :err => Fabricate(:err, :problem => pr))
  26 + Fabricate(:notice, :err => Fabricate(:err, :problem => pr))
28 27 pr.resolve!
29 28 end
30 29 end
... ...
spec/interactors/resolved_problem_clearer_spec.rb
... ... @@ -19,16 +19,16 @@ describe ResolvedProblemClearer do
19 19 }
20 20 end
21 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 24 resolved_problem_clearer.execute
25 25 end
26 26 end
27 27  
28 28 context "with problem resolve" do
29 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 32 problems.first.resolve!
33 33 problems.second.resolve!
34 34 end
... ... @@ -44,7 +44,7 @@ describe ResolvedProblemClearer do
44 44 end
45 45  
46 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 48 resolved_problem_clearer.execute
49 49 end
50 50 end
... ...
spec/mailers/mailer_spec.rb
1 1 shared_examples "a notification email" do
2 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 4 end
5 5  
6 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 8 end
9 9  
10 10 it "should have Precedence header" do
11   - expect(@email).to have_header('Precedence', 'bulk')
  11 + expect(email).to have_header('Precedence', 'bulk')
12 12 end
13 13  
14 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 16 end
17 17  
18 18 it "should have X-Auto-Response-Suppress header" do
19 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 21 end
22 22  
23 23 it "should send the email" do
  24 + email
24 25 expect(ActionMailer::Base.deliveries.size).to eq 1
25 26 end
26 27 end
... ... @@ -30,44 +31,63 @@ describe Mailer do
30 31 include EmailSpec::Helpers
31 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 44 :asset_host => "http://example.com",
41 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 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 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 73 end
54 74  
55 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 77 end
58 78  
59 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 81 end
62 82  
63 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 85 end
66 86  
67 87 context 'with a very long message' do
68 88 let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) }
69 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 91 end
72 92 end
73 93 end
... ...
spec/models/backtrace_line_normalizer_spec.rb
... ... @@ -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   -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 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 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 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 23 end
48 24 end
49 25 end
... ...
spec/models/error_report_spec.rb
... ... @@ -17,259 +17,310 @@ module Airbrake
17 17 end
18 18  
19 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 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 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 51 end
56 52 end
57   -
58   - expect(error_report.error.fingerprint).to eq('fingerprintzzz')
59 53 end
  54 +
  55 + expect(error_report.error.fingerprint).to eq('fingerprintzzz')
60 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 75 expect {
65 76 error_report.generate_notice!
66 77 }.to change {
67 78 app.reload.problems.count
68 79 }.by(1)
69 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 138 end
142 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 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 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 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 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 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 260 end
210 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 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 276 end
226 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 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 294 end
  295 +
245 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 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 324 end
274 325 end
275 326 end
... ...
spec/models/fingerprint/md5_spec.rb
1 1 describe Fingerprint::MD5, type: 'model' do
2 2 context 'being created' do
3 3 let(:backtrace) do
4   - Backtrace.create(:raw => [
  4 + Backtrace.find_or_create([
5 5 {
6 6 "number"=>"17",
7 7 "file"=>"[GEM_ROOT]/gems/activesupport/lib/active_support/callbacks.rb",
... ... @@ -14,10 +14,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do
14 14  
15 15 context "with same backtrace" do
16 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 20 end
22 21  
23 22 it "normalizes the fingerprint of generated methods" do
... ... @@ -27,10 +26,9 @@ describe Fingerprint::MD5, type: &#39;model&#39; do
27 26  
28 27 context "with same backtrace where FRAGMENT has not been extracted" do
29 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 32 end
35 33  
36 34 it "normalizes the fingerprint of generated methods" do
... ...
spec/models/fingerprint/sha1_spec.rb
1 1 describe Fingerprint::Sha1, type: 'model' do
2 2 context '#generate' do
3 3 let(:backtrace) {
4   - Backtrace.create(:raw => [
  4 + Backtrace.find_or_create([
5 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 6 {"number"=>"404", "file"=>"[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb", "method"=>"send"},
7 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 19  
20 20 context "with different backtrace with only last line change" do
21 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 26 it 'should not same fingerprint' do
29 27 expect(
... ...
spec/models/notice_observer_spec.rb
1 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 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 37 before do
6 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 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 46 custom_thresholds.each do |threshold|
16 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 51 expect(Mailer).to receive(:err_notification).
19 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 56 end
22 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 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 70 expect(HoptoadNotifier).to receive(:notify)
47   - Fabricate(:notice, :err => @err)
  71 + ErrorReport.new(notice_attrs).generate_notice!
48 72 end
49 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 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 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 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 127 end
94 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 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 144 end
115 145 end
116 146  
117 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 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 160 end
134 161 end
135 162  
136 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 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 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 178 end
155 179  
156 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 186 end
163 187  
164 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 195 end
171 196 end
172 197 end
... ...
spec/models/notice_spec.rb
... ... @@ -3,7 +3,7 @@ describe Notice, type: &#39;model&#39; do
3 3 it 'requires a backtrace' do
4 4 notice = Fabricate.build(:notice, :backtrace => nil)
5 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 7 end
8 8  
9 9 it 'requires the server_environment' do
... ...
spec/models/problem_spec.rb
... ... @@ -40,10 +40,10 @@ describe Problem, type: &#39;model&#39; do
40 40 expect(problem).to_not be_nil
41 41  
42 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 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 47 end
48 48 end
49 49  
... ... @@ -266,12 +266,6 @@ describe Problem, type: &#39;model&#39; do
266 266 expect(@problem.messages).to eq ({})
267 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 269 it "removing a notice removes string from #messages" do
276 270 Fabricate(:notice, :err => @err, :message => 'ERR 1')
277 271 expect {
... ... @@ -299,12 +293,6 @@ describe Problem, type: &#39;model&#39; do
299 293 expect(@problem.hosts).to eq ({})
300 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 296 it "removing a notice removes string from #hosts" do
309 297 Fabricate(:notice, :err => @err, :request => {'url' => "http://example.com/resource/12"})
310 298 expect {
... ... @@ -325,12 +313,6 @@ describe Problem, type: &#39;model&#39; do
325 313 expect(@problem.user_agents).to eq ({})
326 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 316 it "removing a notice removes string from #user_agents" do
335 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 318 expect {
... ...
spec/spec_helper.rb
1 1 # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2 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 6 if ENV['COVERAGE']
6 7 require 'coveralls'
... ... @@ -21,7 +22,6 @@ require File.expand_path(&quot;../../config/environment&quot;, __FILE__)
21 22 require 'rspec/rails'
22 23 require 'rspec/its'
23 24 require 'email_spec'
24   -require 'database_cleaner'
25 25 require 'xmpp4r'
26 26 require 'xmpp4r/muc'
27 27 require 'mongoid-rspec'
... ... @@ -31,6 +31,8 @@ require &#39;errbit_plugin/mock_issue_tracker&#39;
31 31 # Requires supporting files with custom matchers and macros, etc,
32 32 # in ./support/ and its subdirectories.
33 33 Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
  34 +Mongoid::Config.truncate!
  35 +Mongoid::Tasks::Database.create_indexes
34 36  
35 37 RSpec.configure do |config|
36 38 config.include Devise::TestHelpers, :type => :controller
... ... @@ -38,8 +40,7 @@ RSpec.configure do |config|
38 40 config.alias_example_to :fit, :focused => true
39 41  
40 42 config.before(:each) do
41   - DatabaseCleaner[:mongoid].strategy = :truncation
42   - DatabaseCleaner.clean
  43 + Mongoid::Config.truncate!
43 44 end
44 45  
45 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 6 }
7 7  
8 8 before do
9   - allow(view).to receive(:problem).and_return(problem)
  9 + allow(view).to receive(:problem).and_return(ProblemDecorator.new(problem))
10 10 end
11 11  
12 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 6 }
7 7  
8 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 11 end
11 12  
12 13 it "has the problem url" do
... ...