Commit 89595a9aee8be6126ab1e3178bd23c7ee71389eb

Authored by Nathan Broadbent
2 parents f18852d4 8a9001fd
Exists in master and in 1 other branch production

Merge remote branch 'boblail/merge-rebase' into boblail. Tweaked some changed fi…

…les, fixed whitespace, upgraded RSpec to 2.6.0

Conflicts:
	app/helpers/errs_helper.rb
	app/mailers/mailer.rb
	app/models/notice.rb
	app/views/errs/_table.html.haml
	app/views/errs/show.html.haml
	app/views/notices/_summary.html.haml
	spec/models/issue_trackers/redmine_tracker_spec.rb
	spec/views/errs/show.html.haml_spec.rb
Showing 96 changed files with 1540 additions and 820 deletions   Show diff stats
@@ -4,6 +4,7 @@ log/*.log @@ -4,6 +4,7 @@ log/*.log
4 tmp/**/* 4 tmp/**/*
5 config/config.yml 5 config/config.yml
6 config/deploy.rb 6 config/deploy.rb
  7 +config/deploy
7 config/mongoid.yml 8 config/mongoid.yml
8 .rvmrc 9 .rvmrc
9 *~ 10 *~
@@ -24,7 +24,7 @@ platform :ruby do @@ -24,7 +24,7 @@ platform :ruby do
24 end 24 end
25 25
26 group :development, :test do 26 group :development, :test do
27 - gem 'rspec-rails', '~> 2.5' 27 + gem 'rspec-rails', '~> 2.6'
28 gem 'webmock', :require => false 28 gem 'webmock', :require => false
29 gem 'factory_girl_rails' 29 gem 'factory_girl_rails'
30 unless ENV['TRAVIS'] 30 unless ENV['TRAVIS']
@@ -34,7 +34,7 @@ group :development, :test do @@ -34,7 +34,7 @@ group :development, :test do
34 end 34 end
35 35
36 group :test do 36 group :test do
37 - gem 'rspec', '~> 2.5' 37 + gem 'rspec', '~> 2.6'
38 gem 'database_cleaner', '~> 0.6.0' 38 gem 'database_cleaner', '~> 0.6.0'
39 gem 'email_spec' 39 gem 'email_spec'
40 end 40 end
@@ -57,7 +57,7 @@ GEM @@ -57,7 +57,7 @@ GEM
57 bcrypt-ruby (~> 2.1.2) 57 bcrypt-ruby (~> 2.1.2)
58 orm_adapter (~> 0.0.3) 58 orm_adapter (~> 0.0.3)
59 warden (~> 1.0.3) 59 warden (~> 1.0.3)
60 - diff-lcs (1.1.2) 60 + diff-lcs (1.1.3)
61 email_spec (1.1.1) 61 email_spec (1.1.1)
62 rspec (~> 2.0) 62 rspec (~> 2.0)
63 erubis (2.6.6) 63 erubis (2.6.6)
@@ -158,19 +158,19 @@ GEM @@ -158,19 +158,19 @@ GEM
158 responders (0.6.4) 158 responders (0.6.4)
159 rest-client (1.5.1) 159 rest-client (1.5.1)
160 mime-types (>= 1.16) 160 mime-types (>= 1.16)
161 - rspec (2.5.0)  
162 - rspec-core (~> 2.5.0)  
163 - rspec-expectations (~> 2.5.0)  
164 - rspec-mocks (~> 2.5.0)  
165 - rspec-core (2.5.1)  
166 - rspec-expectations (2.5.0) 161 + rspec (2.6.0)
  162 + rspec-core (~> 2.6.0)
  163 + rspec-expectations (~> 2.6.0)
  164 + rspec-mocks (~> 2.6.0)
  165 + rspec-core (2.6.4)
  166 + rspec-expectations (2.6.0)
167 diff-lcs (~> 1.1.2) 167 diff-lcs (~> 1.1.2)
168 - rspec-mocks (2.5.0)  
169 - rspec-rails (2.5.0) 168 + rspec-mocks (2.6.0)
  169 + rspec-rails (2.6.1)
170 actionpack (~> 3.0) 170 actionpack (~> 3.0)
171 activesupport (~> 3.0) 171 activesupport (~> 3.0)
172 railties (~> 3.0) 172 railties (~> 3.0)
173 - rspec (~> 2.5.0) 173 + rspec (~> 2.6.0)
174 ruby-debug (0.10.4) 174 ruby-debug (0.10.4)
175 columnize (>= 0.1) 175 columnize (>= 0.1)
176 ruby-debug-base (~> 0.10.4.0) 176 ruby-debug-base (~> 0.10.4.0)
@@ -232,8 +232,8 @@ DEPENDENCIES @@ -232,8 +232,8 @@ DEPENDENCIES
232 pivotal-tracker 232 pivotal-tracker
233 rails (= 3.0.10) 233 rails (= 3.0.10)
234 redmine_client! 234 redmine_client!
235 - rspec (~> 2.5)  
236 - rspec-rails (~> 2.5) 235 + rspec (~> 2.6)
  236 + rspec-rails (~> 2.6)
237 ruby-debug 237 ruby-debug
238 ruby-debug19 238 ruby-debug19
239 ruby-fogbugz 239 ruby-fogbugz
app/controllers/application_controller.rb
@@ -10,11 +10,20 @@ class ApplicationController < ActionController::Base @@ -10,11 +10,20 @@ class ApplicationController < ActionController::Base
10 (location == root_path && App.count == 1) ? app_path(App.first) : location 10 (location == root_path && App.count == 1) ? app_path(App.first) : location
11 end 11 end
12 12
13 - protected 13 + rescue_from ActionController::RedirectBackError, :with => :redirect_to_root
  14 +
  15 +
  16 +protected
  17 +
  18 +
  19 + def require_admin!
  20 + redirect_to_root unless user_signed_in? && current_user.admin?
  21 + end
  22 +
  23 + def redirect_to_root
  24 + redirect_to(root_path)
  25 + end
14 26
15 - def require_admin!  
16 - redirect_to root_path unless user_signed_in? && current_user.admin?  
17 - end  
18 27
19 end 28 end
20 29
app/controllers/apps_controller.rb
1 class AppsController < InheritedResources::Base 1 class AppsController < InheritedResources::Base
2 -  
3 before_filter :require_admin!, :except => [:index, :show] 2 before_filter :require_admin!, :except => [:index, :show]
4 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update] 3 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
5 respond_to :html 4 respond_to :html
6 5
  6 +
7 def show 7 def show
8 respond_to do |format| 8 respond_to do |format|
9 format.html do 9 format.html do
10 @all_errs = !!params[:all_errs] 10 @all_errs = !!params[:all_errs]
11 11
12 - @errs = resource.errs  
13 - @errs = @errs.unresolved unless @all_errs  
14 - @errs = @errs.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) 12 + @problems = resource.problems
  13 + @problems = @problems.unresolved unless @all_errs
  14 + @problems = @problems.in_env(params[:environment]).ordered.paginate(:page => params[:page], :per_page => current_user.per_page)
15 15
  16 + @selected_problems = params[:problems] || []
16 @deploys = @app.deploys.order_by(:created_at.desc).limit(5) 17 @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
17 end 18 end
18 format.atom do 19 format.atom do
19 - @errs = resource.errs.unresolved.ordered 20 + @problems = resource.problems.unresolved.ordered
20 end 21 end
21 end 22 end
22 end 23 end
@@ -49,7 +50,7 @@ class AppsController &lt; InheritedResources::Base @@ -49,7 +50,7 @@ class AppsController &lt; InheritedResources::Base
49 # Caches the unresolved err counts while performing the sort. 50 # Caches the unresolved err counts while performing the sort.
50 @unresolved_counts = {} 51 @unresolved_counts = {}
51 @apps ||= end_of_association_chain.all.sort{|a,b| 52 @apps ||= end_of_association_chain.all.sort{|a,b|
52 - [a,b].each{|app| @unresolved_counts[app.id] ||= app.errs.unresolved.count } 53 + [a,b].each{|app| @unresolved_counts[app.id] ||= app.problems.unresolved.count }
53 @unresolved_counts[b.id] <=> @unresolved_counts[a.id] 54 @unresolved_counts[b.id] <=> @unresolved_counts[a.id]
54 } 55 }
55 end 56 end
app/controllers/deploys_controller.rb
@@ -18,9 +18,9 @@ class DeploysController &lt; ApplicationController @@ -18,9 +18,9 @@ class DeploysController &lt; ApplicationController
18 18
19 @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10) 19 @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10)
20 end 20 end
21 - 21 +
22 private 22 private
23 - 23 +
24 def default_deploy 24 def default_deploy
25 if params[:deploy] 25 if params[:deploy]
26 { 26 {
@@ -32,7 +32,7 @@ class DeploysController &lt; ApplicationController @@ -32,7 +32,7 @@ class DeploysController &lt; ApplicationController
32 } 32 }
33 end 33 end
34 end 34 end
35 - 35 +
36 # handle Heroku's HTTP post deployhook format 36 # handle Heroku's HTTP post deployhook format
37 def heroku_deploy 37 def heroku_deploy
38 { 38 {
@@ -42,5 +42,6 @@ class DeploysController &lt; ApplicationController @@ -42,5 +42,6 @@ class DeploysController &lt; ApplicationController
42 :revision => params[:head], 42 :revision => params[:head],
43 } 43 }
44 end 44 end
45 - 45 +
46 end 46 end
  47 +
app/controllers/errs_controller.rb
1 class ErrsController < ApplicationController 1 class ErrsController < ApplicationController
  2 + include ActionView::Helpers::TextHelper
  3 +
  4 + before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  5 + before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  6 + before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  7 +
2 8
3 - before_filter :find_app, :except => [:index, :all]  
4 - before_filter :find_err, :except => [:index, :all]  
5 9
6 def index 10 def index
7 app_scope = current_user.admin? ? App.all : current_user.apps 11 app_scope = current_user.admin? ? App.all : current_user.apps
8 - @errs = Err.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered 12 + @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered
  13 + @selected_problems = params[:problems] || []
9 respond_to do |format| 14 respond_to do |format|
10 format.html do 15 format.html do
11 - @errs = @errs.paginate(:page => params[:page], :per_page => current_user.per_page) 16 + @problems = @problems.paginate(:page => params[:page], :per_page => current_user.per_page)
12 end 17 end
13 format.atom 18 format.atom
14 end 19 end
15 end 20 end
16 21
  22 +
17 def all 23 def all
18 app_scope = current_user.admin? ? App.all : current_user.apps 24 app_scope = current_user.admin? ? App.all : current_user.apps
19 - @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) 25 + @problems = Problem.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page)
  26 + @selected_problems = params[:problems] || []
20 end 27 end
21 28
  29 +
22 def show 30 def show
23 - page = (params[:notice] || @err.notices_count) 31 + page = (params[:notice] || @problem.notices_count)
24 page = 1 if page.to_i.zero? 32 page = 1 if page.to_i.zero?
25 - @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1) 33 + @notices = @problem.notices.paginate(:page => page, :per_page => 1)
26 @notice = @notices.first 34 @notice = @notices.first
27 @comment = Comment.new 35 @comment = Comment.new
28 end 36 end
29 37
  38 +
30 def create_issue 39 def create_issue
31 set_tracker_params 40 set_tracker_params
32 41
33 if @app.issue_tracker 42 if @app.issue_tracker
34 - @app.issue_tracker.create_issue @err 43 + @app.issue_tracker.create_issue @problem
35 else 44 else
36 flash[:error] = "This app has no issue tracker setup." 45 flash[:error] = "This app has no issue tracker setup."
37 end 46 end
38 - redirect_to app_err_path(@app, @err) 47 + redirect_to app_err_path(@app, @problem)
39 rescue ActiveResource::ConnectionError => e 48 rescue ActiveResource::ConnectionError => e
40 Rails.logger.error e.to_s 49 Rails.logger.error e.to_s
41 flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later." 50 flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later."
42 - redirect_to app_err_path(@app, @err) 51 + redirect_to app_err_path(@app, @problem)
43 end 52 end
44 53
  54 +
45 def unlink_issue 55 def unlink_issue
46 - @err.update_attribute :issue_link, nil  
47 - redirect_to app_err_path(@app, @err) 56 + @problem.update_attribute :issue_link, nil
  57 + redirect_to app_err_path(@app, @problem)
48 end 58 end
49 59
  60 +
50 def resolve 61 def resolve
51 # Deal with bug in mongoid where find is returning an Enumberable obj 62 # Deal with bug in mongoid where find is returning an Enumberable obj
52 - @err = @err.first if @err.respond_to?(:first)  
53 -  
54 - @err.resolve! 63 + @problem = @problem.first if @problem.respond_to?(:first)
55 64
  65 + @problem.resolve!
56 flash[:success] = 'Great news everyone! The err has been resolved.' 66 flash[:success] = 'Great news everyone! The err has been resolved.'
57 -  
58 redirect_to :back 67 redirect_to :back
59 rescue ActionController::RedirectBackError 68 rescue ActionController::RedirectBackError
60 redirect_to app_path(@app) 69 redirect_to app_path(@app)
@@ -64,15 +73,16 @@ class ErrsController &lt; ApplicationController @@ -64,15 +73,16 @@ class ErrsController &lt; ApplicationController
64 def create_comment 73 def create_comment
65 @comment = Comment.new(params[:comment].merge(:user_id => current_user.id)) 74 @comment = Comment.new(params[:comment].merge(:user_id => current_user.id))
66 if @comment.valid? 75 if @comment.valid?
67 - @err.comments << @comment  
68 - @err.save 76 + @problem.comments << @comment
  77 + @problem.save
69 flash[:success] = "Comment saved!" 78 flash[:success] = "Comment saved!"
70 else 79 else
71 flash[:error] = "I'm sorry, your comment was blank! Try again?" 80 flash[:error] = "I'm sorry, your comment was blank! Try again?"
72 end 81 end
73 - redirect_to app_err_path(@app, @err) 82 + redirect_to app_err_path(@app, @problem)
74 end 83 end
75 84
  85 +
76 def destroy_comment 86 def destroy_comment
77 @comment = Comment.find(params[:comment_id]) 87 @comment = Comment.find(params[:comment_id])
78 if @comment.destroy 88 if @comment.destroy
@@ -80,29 +90,86 @@ class ErrsController &lt; ApplicationController @@ -80,29 +90,86 @@ class ErrsController &lt; ApplicationController
80 else 90 else
81 flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!" 91 flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!"
82 end 92 end
83 - redirect_to app_err_path(@app, @err) 93 + redirect_to app_err_path(@app, @problem)
  94 + end
  95 +
  96 +
  97 + def resolve_several
  98 + @selected_problems.each(&:resolve!)
  99 + flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved."
  100 + redirect_to :back
84 end 101 end
85 102
86 103
87 - protected 104 + def unresolve_several
  105 + @selected_problems.each(&:unresolve!)
  106 + flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved."
  107 + redirect_to :back
  108 + end
88 109
89 - def find_app  
90 - @app = App.find(params[:app_id])  
91 110
92 - # Mongoid Bug: could not chain: current_user.apps.find_by_id!  
93 - # apparently finding by 'watchers.email' and 'id' is broken  
94 - raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) 111 + def merge_several
  112 + if @selected_problems.length < 2
  113 + flash[:notice] = "You must select at least two errors to merge"
  114 + else
  115 + @merged_problem = Problem.merge!(@selected_problems)
  116 + flash[:notice] = "#{@selected_problems.count} errors have been merged."
95 end 117 end
  118 + redirect_to :back
  119 + end
96 120
97 - def find_err  
98 - @err = @app.errs.find(params[:id])  
99 - end  
100 121
101 - def set_tracker_params  
102 - IssueTracker.default_url_options[:host] = request.host  
103 - IssueTracker.default_url_options[:port] = request.port  
104 - IssueTracker.default_url_options[:protocol] = request.scheme 122 + def unmerge_several
  123 + all = @selected_problems.map(&:unmerge!).flatten
  124 + flash[:success] = "#{pluralize(all.length, 'err has', 'errs have')} been unmerged."
  125 + redirect_to :back
  126 + end
  127 +
  128 +
  129 + def destroy_several
  130 + @selected_problems.each(&:destroy)
  131 + flash[:notice] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been deleted."
  132 + redirect_to :back
  133 + end
  134 +
  135 +
  136 +protected
  137 +
  138 +
  139 + def find_app
  140 + @app = App.find(params[:app_id])
  141 +
  142 + # Mongoid Bug: could not chain: current_user.apps.find_by_id!
  143 + # apparently finding by 'watchers.email' and 'id' is broken
  144 + raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
  145 + end
  146 +
  147 +
  148 + def find_problem
  149 + @problem = @app.problems.find(params[:id])
  150 +
  151 + # Deal with bug in mogoid where find is returning an Enumberable obj
  152 + @problem = @problem.first if @problem.respond_to?(:first)
  153 + end
  154 +
  155 +
  156 + def set_tracker_params
  157 + IssueTracker.default_url_options[:host] = request.host
  158 + IssueTracker.default_url_options[:port] = request.port
  159 + IssueTracker.default_url_options[:protocol] = request.scheme
  160 + end
  161 +
  162 +
  163 + def find_selected_problems
  164 + err_ids = (params[:problems] || []).compact
  165 + if err_ids.empty?
  166 + flash[:notice] = "You have not selected any errors"
  167 + redirect_to :back
  168 + else
  169 + @selected_problems = Array(Problem.find(err_ids))
105 end 170 end
  171 + end
  172 +
106 173
107 end 174 end
108 175
app/controllers/notices_controller.rb
@@ -5,8 +5,9 @@ class NoticesController &lt; ApplicationController @@ -5,8 +5,9 @@ class NoticesController &lt; ApplicationController
5 5
6 def create 6 def create
7 # params[:data] if the notice came from a GET request, raw_post if it came via POST 7 # params[:data] if the notice came from a GET request, raw_post if it came via POST
8 - @notice = Notice.from_xml(params[:data] || request.raw_post) 8 + @notice = App.report_error!(params[:data] || request.raw_post)
9 respond_with @notice 9 respond_with @notice
10 end 10 end
11 11
12 end 12 end
  13 +
app/controllers/users_controller.rb
@@ -71,3 +71,4 @@ class UsersController &lt; ApplicationController @@ -71,3 +71,4 @@ class UsersController &lt; ApplicationController
71 end 71 end
72 72
73 end 73 end
  74 +
app/helpers/errs_helper.rb
1 module ErrsHelper 1 module ErrsHelper
2 - def last_notice_at err  
3 - err.last_notice_at || err.created_at 2 + def last_notice_at(problem)
  3 + problem.last_notice_at || problem.created_at
4 end 4 end
5 5
6 def err_confirm 6 def err_confirm
app/helpers/form_helper.rb
@@ -15,4 +15,4 @@ module FormHelper @@ -15,4 +15,4 @@ module FormHelper
15 (builder.object_name + field).gsub(/[\[\]]/,'_').squeeze('_') 15 (builder.object_name + field).gsub(/[\[\]]/,'_').squeeze('_')
16 end 16 end
17 17
18 -end  
19 \ No newline at end of file 18 \ No newline at end of file
  19 +end
app/helpers/hash_helper.rb
@@ -17,4 +17,4 @@ module HashHelper @@ -17,4 +17,4 @@ module HashHelper
17 pretty += "\n#{' '*nesting*tab_size}}" 17 pretty += "\n#{' '*nesting*tab_size}}"
18 end 18 end
19 19
20 -end  
21 \ No newline at end of file 20 \ No newline at end of file
  21 +end
app/helpers/navigation_helper.rb
@@ -34,4 +34,4 @@ module NavigationHelper @@ -34,4 +34,4 @@ module NavigationHelper
34 active 34 active
35 end 35 end
36 36
37 -end  
38 \ No newline at end of file 37 \ No newline at end of file
  38 +end
app/mailers/mailer.rb
@@ -7,10 +7,10 @@ class Mailer &lt; ActionMailer::Base @@ -7,10 +7,10 @@ class Mailer &lt; ActionMailer::Base
7 7
8 def err_notification(notice) 8 def err_notification(notice)
9 @notice = notice 9 @notice = notice
10 - @app = notice.err.app 10 + @app = notice.app
11 11
12 mail :to => @app.notification_recipients, 12 mail :to => @app.notification_recipients,
13 - :subject => "[#{@app.name}][#{@notice.err.environment}] #{@notice.err.message}" 13 + :subject => "[#{@app.name}][#{@notice.environment_name}] #{@notice.message}"
14 end 14 end
15 15
16 def deploy_notification(deploy) 16 def deploy_notification(deploy)
app/models/app.rb
@@ -13,6 +13,7 @@ class App @@ -13,6 +13,7 @@ class App
13 13
14 # Some legacy apps may have string as key instead of BSON::ObjectID 14 # Some legacy apps may have string as key instead of BSON::ObjectID
15 identity :type => String 15 identity :type => String
  16 +
16 # There seems to be a Mongoid bug making it impossible to use String identity with references_many feature: 17 # There seems to be a Mongoid bug making it impossible to use String identity with references_many feature:
17 # https://github.com/mongoid/mongoid/issues/703 18 # https://github.com/mongoid/mongoid/issues/703
18 # Using 32 character string as a workaround. 19 # Using 32 character string as a workaround.
@@ -23,7 +24,7 @@ class App @@ -23,7 +24,7 @@ class App
23 embeds_many :watchers 24 embeds_many :watchers
24 embeds_many :deploys 25 embeds_many :deploys
25 embeds_one :issue_tracker 26 embeds_one :issue_tracker
26 - has_many :errs, :inverse_of => :app, :dependent => :destroy 27 + has_many :problems, :inverse_of => :app, :dependent => :destroy
27 28
28 before_validation :generate_api_key, :on => :create 29 before_validation :generate_api_key, :on => :create
29 before_save :normalize_github_url 30 before_save :normalize_github_url
@@ -39,6 +40,50 @@ class App @@ -39,6 +40,50 @@ class App
39 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, 40 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
40 :reject_if => proc { |attrs| !IssueTracker.subclasses.map(&:to_s).include?(attrs[:type].to_s) } 41 :reject_if => proc { |attrs| !IssueTracker.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
41 42
  43 +
  44 + # Processes a new error report.
  45 + #
  46 + # Accepts either XML or a hash with the following attributes:
  47 + #
  48 + # * <tt>:klass</tt> - the class of error
  49 + # * <tt>:message</tt> - the error message
  50 + # * <tt>:backtrace</tt> - an array of stack trace lines
  51 + #
  52 + # * <tt>:request</tt> - a hash of values describing the request
  53 + # * <tt>:server_environment</tt> - a hash of values describing the server environment
  54 + #
  55 + # * <tt>:api_key</tt> - the API key with which the error was reported
  56 + # * <tt>:notifier</tt> - information to identify the source of the error report
  57 + #
  58 + def self.report_error!(*args)
  59 + report = ErrorReport.new(*args)
  60 + report.generate_notice!
  61 + end
  62 +
  63 +
  64 + # Processes a new error report.
  65 + #
  66 + # Accepts a hash with the following attributes:
  67 + #
  68 + # * <tt>:klass</tt> - the class of error
  69 + # * <tt>:message</tt> - the error message
  70 + # * <tt>:backtrace</tt> - an array of stack trace lines
  71 + #
  72 + # * <tt>:request</tt> - a hash of values describing the request
  73 + # * <tt>:server_environment</tt> - a hash of values describing the server environment
  74 + #
  75 + # * <tt>:notifier</tt> - information to identify the source of the error report
  76 + #
  77 + def report_error!(hash)
  78 + report = ErrorReport.new(hash.merge(:api_key => api_key))
  79 + report.generate_notice!
  80 + end
  81 +
  82 + def find_or_create_err!(attrs)
  83 + Err.where(attrs).first || problems.create!.errs.create!(attrs)
  84 + end
  85 +
  86 + # Mongoid Bug: find(id) on association proxies returns an Enumerator
42 def self.find_by_id!(app_id) 87 def self.find_by_id!(app_id)
43 find app_id 88 find app_id
44 end 89 end
@@ -51,6 +96,7 @@ class App @@ -51,6 +96,7 @@ class App
51 deploys.last && deploys.last.created_at 96 deploys.last && deploys.last.created_at
52 end 97 end
53 98
  99 +
54 # Legacy apps don't have notify_on_errs and notify_on_deploys params 100 # Legacy apps don't have notify_on_errs and notify_on_deploys params
55 def notify_on_errs 101 def notify_on_errs
56 !(self[:notify_on_errs] == false) 102 !(self[:notify_on_errs] == false)
@@ -62,6 +108,7 @@ class App @@ -62,6 +108,7 @@ class App
62 end 108 end
63 alias :notify_on_deploys? :notify_on_deploys 109 alias :notify_on_deploys? :notify_on_deploys
64 110
  111 +
65 def github_url? 112 def github_url?
66 self.github_url.present? 113 self.github_url.present?
67 end 114 end
app/models/deploy.rb
@@ -22,7 +22,7 @@ class Deploy @@ -22,7 +22,7 @@ class Deploy
22 end 22 end
23 23
24 def resolve_app_errs 24 def resolve_app_errs
25 - app.errs.unresolved.in_env(environment).each {|err| err.resolve!} 25 + app.problems.unresolved.in_env(environment).each {|problem| problem.resolve!}
26 end 26 end
27 27
28 def short_revision 28 def short_revision
app/models/err.rb
  1 +# Represents a set of Notices which can be automatically
  2 +# determined to refer to the same Error (Errbit groups
  3 +# notices into errs by a notice's fingerprint.)
  4 +
1 class Err 5 class Err
2 include Mongoid::Document 6 include Mongoid::Document
3 include Mongoid::Timestamps 7 include Mongoid::Timestamps
@@ -7,53 +11,18 @@ class Err @@ -7,53 +11,18 @@ class Err
7 field :action 11 field :action
8 field :environment 12 field :environment
9 field :fingerprint 13 field :fingerprint
10 - field :last_notice_at, :type => DateTime  
11 - field :resolved, :type => Boolean, :default => false  
12 - field :issue_link, :type => String  
13 - field :notices_count, :type => Integer, :default => 0  
14 - field :message  
15 14
16 - index :last_notice_at  
17 - index :app_id  
18 - index :notices 15 + belongs_to :problem
  16 + index :problem_id
19 17
20 - belongs_to :app  
21 - has_many :notices  
22 - has_many :comments, :inverse_of => :err, :dependent => :destroy 18 + has_many :notices, :inverse_of => :err, :dependent => :destroy
23 19
24 validates_presence_of :klass, :environment 20 validates_presence_of :klass, :environment
25 21
26 - scope :resolved, where(:resolved => true)  
27 - scope :unresolved, where(:resolved => false)  
28 - scope :ordered, order_by(:last_notice_at.desc)  
29 - scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}  
30 -  
31 - def self.in_env(env)  
32 - env.present? ? where(:environment => env) : scoped  
33 - end  
34 -  
35 - def self.for(attrs)  
36 - app = attrs.delete(:app)  
37 - app.errs.where(attrs).first || app.errs.create!(attrs)  
38 - end  
39 -  
40 - def resolve!  
41 - self.update_attributes!(:resolved => true)  
42 - end  
43 -  
44 - def unresolved?  
45 - !resolved?  
46 - end  
47 -  
48 - def where  
49 - where = component.dup  
50 - where << "##{action}" if action.present?  
51 - where  
52 - end  
53 -  
54 - def message  
55 - super || klass  
56 - end 22 + delegate :app,
  23 + :resolved?,
  24 + :to => :problem
  25 +
57 26
58 end 27 end
59 28
app/models/error_report.rb 0 → 100644
@@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
  1 +require 'digest/md5'
  2 +
  3 +class ErrorReport
  4 + attr_reader :klass, :message, :backtrace, :request, :server_environment, :api_key, :notifier
  5 +
  6 + def initialize(xml_or_attributes)
  7 + @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
  8 + @attributes.each{|k, v| instance_variable_set(:"@#{k}", v) }
  9 + end
  10 +
  11 + def fingerprint
  12 + @fingerprint ||= Digest::MD5.hexdigest(backtrace[0].to_s)
  13 + end
  14 +
  15 + def rails_env
  16 + server_environment['environment-name'] || 'development'
  17 + end
  18 +
  19 + def component
  20 + request['component'] || 'unknown'
  21 + end
  22 +
  23 + def action
  24 + request['action']
  25 + end
  26 +
  27 + def app
  28 + @app ||= App.find_by_api_key!(api_key)
  29 + end
  30 +
  31 + def generate_notice!
  32 + notice = Notice.new(
  33 + :message => message,
  34 + :backtrace => backtrace,
  35 + :request => request,
  36 + :server_environment => server_environment,
  37 + :notifier => notifier)
  38 +
  39 + err = app.find_or_create_err!(
  40 + :klass => klass,
  41 + :component => component,
  42 + :action => action,
  43 + :environment => rails_env,
  44 + :fingerprint => fingerprint)
  45 +
  46 + err.notices << notice
  47 + notice
  48 + end
  49 +end
  50 +
app/models/issue_tracker.rb
@@ -20,8 +20,8 @@ class IssueTracker @@ -20,8 +20,8 @@ class IssueTracker
20 # Subclasses are responsible for overwriting this method. 20 # Subclasses are responsible for overwriting this method.
21 def check_params; true; end 21 def check_params; true; end
22 22
23 - def issue_title err  
24 - "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" 23 + def issue_title(problem)
  24 + "[#{ problem.environment }][#{ problem.where }] #{problem.message.to_s.truncate(100)}"
25 end 25 end
26 26
27 # Allows us to set the issue tracker class from a single form. 27 # Allows us to set the issue tracker class from a single form.
app/models/issue_trackers/fogbugz_tracker.rb
@@ -22,19 +22,19 @@ class IssueTrackers::FogbugzTracker &lt; IssueTracker @@ -22,19 +22,19 @@ class IssueTrackers::FogbugzTracker &lt; IssueTracker
22 end 22 end
23 end 23 end
24 24
25 - def create_issue(err) 25 + def create_issue(problem)
26 fogbugz = Fogbugz::Interface.new(:email => username, :password => password, :uri => "https://#{account}.fogbugz.com") 26 fogbugz = Fogbugz::Interface.new(:email => username, :password => password, :uri => "https://#{account}.fogbugz.com")
27 fogbugz.authenticate 27 fogbugz.authenticate
28 28
29 issue = {} 29 issue = {}
30 - issue['sTitle'] = issue_title err 30 + issue['sTitle'] = issue_title problem
31 issue['sArea'] = project_id 31 issue['sArea'] = project_id
32 issue['sEvent'] = body_template.result(binding) 32 issue['sEvent'] = body_template.result(binding)
33 issue['sTags'] = ['errbit'].join(',') 33 issue['sTags'] = ['errbit'].join(',')
34 issue['cols'] = ['ixBug'].join(',') 34 issue['cols'] = ['ixBug'].join(',')
35 35
36 fb_resp = fogbugz.command(:new, issue) 36 fb_resp = fogbugz.command(:new, issue)
37 - err.update_attribute :issue_link, "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}" 37 + problem.update_attribute :issue_link, "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}"
38 end 38 end
39 39
40 def body_template 40 def body_template
app/models/issue_trackers/lighthouse_tracker.rb
@@ -18,20 +18,20 @@ class IssueTrackers::LighthouseTracker &lt; IssueTracker @@ -18,20 +18,20 @@ class IssueTrackers::LighthouseTracker &lt; IssueTracker
18 end 18 end
19 end 19 end
20 20
21 - def create_issue(err) 21 + def create_issue(problem)
22 Lighthouse.account = account 22 Lighthouse.account = account
23 Lighthouse.token = api_token 23 Lighthouse.token = api_token
24 # updating lighthouse account 24 # updating lighthouse account
25 Lighthouse::Ticket.site 25 Lighthouse::Ticket.site
26 26
27 ticket = Lighthouse::Ticket.new(:project_id => project_id) 27 ticket = Lighthouse::Ticket.new(:project_id => project_id)
28 - ticket.title = issue_title err 28 + ticket.title = issue_title problem
29 29
30 ticket.body = body_template.result(binding) 30 ticket.body = body_template.result(binding)
31 31
32 ticket.tags << "errbit" 32 ticket.tags << "errbit"
33 ticket.save! 33 ticket.save!
34 - err.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '') 34 + problem.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '')
35 end 35 end
36 36
37 def body_template 37 def body_template
app/models/issue_trackers/mingle_tracker.rb
@@ -27,21 +27,21 @@ class IssueTrackers::MingleTracker &lt; IssueTracker @@ -27,21 +27,21 @@ class IssueTrackers::MingleTracker &lt; IssueTracker
27 end 27 end
28 end 28 end
29 29
30 - def create_issue(err) 30 + def create_issue(problem)
31 properties = ticket_properties_hash 31 properties = ticket_properties_hash
32 basic_auth = account.gsub(/https?:\/\//, "https://#{username}:#{password}@") 32 basic_auth = account.gsub(/https?:\/\//, "https://#{username}:#{password}@")
33 Mingle.set_site "#{basic_auth}/api/v1/projects/#{project_id}/" 33 Mingle.set_site "#{basic_auth}/api/v1/projects/#{project_id}/"
34 34
35 card = Mingle::Card.new 35 card = Mingle::Card.new
36 card.card_type_name = properties.delete("card_type") 36 card.card_type_name = properties.delete("card_type")
37 - card.name = issue_title(err) 37 + card.name = issue_title(problem)
38 card.description = body_template.result(binding) 38 card.description = body_template.result(binding)
39 properties.each do |property, value| 39 properties.each do |property, value|
40 card.send("cp_#{property}=", value) 40 card.send("cp_#{property}=", value)
41 end 41 end
42 42
43 card.save! 43 card.save!
44 - err.update_attribute :issue_link, URI.parse("#{account}/projects/#{project_id}/cards/#{card.id}").to_s 44 + problem.update_attribute :issue_link, URI.parse("#{account}/projects/#{project_id}/cards/#{card.id}").to_s
45 end 45 end
46 46
47 def body_template 47 def body_template
app/models/issue_trackers/pivotal_labs_tracker.rb
@@ -13,12 +13,12 @@ class IssueTrackers::PivotalLabsTracker &lt; IssueTracker @@ -13,12 +13,12 @@ class IssueTrackers::PivotalLabsTracker &lt; IssueTracker
13 end 13 end
14 end 14 end
15 15
16 - def create_issue(err) 16 + def create_issue(problem)
17 PivotalTracker::Client.token = api_token 17 PivotalTracker::Client.token = api_token
18 PivotalTracker::Client.use_ssl = true 18 PivotalTracker::Client.use_ssl = true
19 project = PivotalTracker::Project.find project_id.to_i 19 project = PivotalTracker::Project.find project_id.to_i
20 - story = project.stories.create :name => issue_title(err), :story_type => 'bug', :description => body_template.result(binding)  
21 - err.update_attribute :issue_link, "https://www.pivotaltracker.com/story/show/#{story.id}" 20 + story = project.stories.create :name => issue_title(problem), :story_type => 'bug', :description => body_template.result(binding)
  21 + problem.update_attribute :issue_link, "https://www.pivotaltracker.com/story/show/#{story.id}"
22 end 22 end
23 23
24 def body_template 24 def body_template
app/models/issue_trackers/redmine_tracker.rb
@@ -25,7 +25,7 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker @@ -25,7 +25,7 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
25 end 25 end
26 end 26 end
27 27
28 - def create_issue(err) 28 + def create_issue(problem)
29 token = api_token 29 token = api_token
30 acc = account 30 acc = account
31 RedmineClient::Base.configure do 31 RedmineClient::Base.configure do
@@ -33,10 +33,10 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker @@ -33,10 +33,10 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
33 self.site = acc 33 self.site = acc
34 end 34 end
35 issue = RedmineClient::Issue.new(:project_id => project_id) 35 issue = RedmineClient::Issue.new(:project_id => project_id)
36 - issue.subject = issue_title err 36 + issue.subject = issue_title problem
37 issue.description = body_template.result(binding) 37 issue.description = body_template.result(binding)
38 issue.save! 38 issue.save!
39 - err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}") 39 + problem.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}")
40 end 40 end
41 41
42 def url_to_file(file_path, line_number = nil) 42 def url_to_file(file_path, line_number = nil)
app/models/notice.rb
@@ -10,53 +10,47 @@ class Notice @@ -10,53 +10,47 @@ class Notice
10 field :server_environment, :type => Hash 10 field :server_environment, :type => Hash
11 field :request, :type => Hash 11 field :request, :type => Hash
12 field :notifier, :type => Hash 12 field :notifier, :type => Hash
  13 + field :klass
13 14
14 belongs_to :err 15 belongs_to :err
15 index :err_id 16 index :err_id
  17 + index :created_at
16 18
17 - after_create :cache_last_notice_at 19 + after_create :increase_counter_cache, :cache_attributes_on_problem, :unresolve_problem
18 after_create :deliver_notification, :if => :should_notify? 20 after_create :deliver_notification, :if => :should_notify?
19 - before_create :increase_counter_cache, :cache_message  
20 before_save :sanitize 21 before_save :sanitize
21 before_destroy :decrease_counter_cache 22 before_destroy :decrease_counter_cache
22 23
23 validates_presence_of :backtrace, :server_environment, :notifier 24 validates_presence_of :backtrace, :server_environment, :notifier
24 25
25 scope :ordered, order_by(:created_at.asc) 26 scope :ordered, order_by(:created_at.asc)
26 - index :created_at 27 + scope :for_errs, lambda {|errs| where(:err_id.in => errs.all.map(&:id))}
27 28
28 - def self.from_xml(hoptoad_xml)  
29 - hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml)  
30 - app = App.find_by_api_key!(hoptoad_notice['api-key'])  
31 -  
32 - hoptoad_notice['request'] ||= {}  
33 - hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank?  
34 - hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank?  
35 -  
36 - err = Err.for({  
37 - :app => app,  
38 - :klass => hoptoad_notice['error']['class'],  
39 - :component => hoptoad_notice['request']['component'],  
40 - :action => hoptoad_notice['request']['action'],  
41 - :environment => hoptoad_notice['server-environment']['environment-name'],  
42 - :fingerprint => hoptoad_notice['fingerprint']  
43 - })  
44 - err.update_attributes(:resolved => false) if err.resolved?  
45 -  
46 - err.notices.create!({  
47 - :message => hoptoad_notice['error']['message'],  
48 - :backtrace => [hoptoad_notice['error']['backtrace']['line']].flatten,  
49 - :server_environment => hoptoad_notice['server-environment'],  
50 - :request => hoptoad_notice['request'],  
51 - :notifier => hoptoad_notice['notifier']  
52 - })  
53 - end 29 + delegate :app, :problem, :to => :err
54 30
55 def user_agent 31 def user_agent
56 agent_string = env_vars['HTTP_USER_AGENT'] 32 agent_string = env_vars['HTTP_USER_AGENT']
57 agent_string.blank? ? nil : UserAgent.parse(agent_string) 33 agent_string.blank? ? nil : UserAgent.parse(agent_string)
58 end 34 end
59 35
  36 + def environment_name
  37 + server_environment['server-environment'] || server_environment['environment-name']
  38 + end
  39 +
  40 + def component
  41 + request['component']
  42 + end
  43 +
  44 + def action
  45 + request['action']
  46 + end
  47 +
  48 + def where
  49 + where = component.to_s.dup
  50 + where << "##{action}" if action.present?
  51 + where
  52 + end
  53 +
60 def self.in_app_backtrace_line?(line) 54 def self.in_app_backtrace_line?(line)
61 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))}) 55 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})
62 end 56 end
@@ -81,10 +75,6 @@ class Notice @@ -81,10 +75,6 @@ class Notice
81 Mailer.err_notification(self).deliver 75 Mailer.err_notification(self).deliver
82 end 76 end
83 77
84 - def cache_last_notice_at  
85 - err.update_attributes(:last_notice_at => created_at)  
86 - end  
87 -  
88 # Backtrace containing only files from the app itself (ignore gems) 78 # Backtrace containing only files from the app itself (ignore gems)
89 def app_backtrace 79 def app_backtrace
90 backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") } 80 backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") }
@@ -93,20 +83,24 @@ class Notice @@ -93,20 +83,24 @@ class Notice
93 protected 83 protected
94 84
95 def should_notify? 85 def should_notify?
96 - err.app.notify_on_errs? && (Errbit::Config.per_app_email_at_notices && err.app.email_at_notices || Errbit::Config.email_at_notices).include?(err.notices.count) && err.app.watchers.any? 86 + app.notify_on_errs? && (Errbit::Config.per_app_email_at_notices && app.email_at_notices || Errbit::Config.email_at_notices).include?(problem.notices_count) && app.watchers.any?
97 end 87 end
98 88
99 -  
100 def increase_counter_cache 89 def increase_counter_cache
101 - err.inc(:notices_count,1) 90 + problem.inc(:notices_count, 1)
102 end 91 end
103 92
104 def decrease_counter_cache 93 def decrease_counter_cache
105 - err.inc(:notices_count,-1) 94 + problem.inc(:notices_count, -1)
106 end 95 end
107 96
108 - def cache_message  
109 - err.update_attribute(:message, message) if err.notices_count == 1 97 + def unresolve_problem
  98 + problem.update_attribute(:resolved, false) if problem.resolved?
  99 + end
  100 +
  101 +
  102 + def cache_attributes_on_problem
  103 + problem.cache_notice_attributes(self) if problem.notices_count == 1
110 end 104 end
111 105
112 def sanitize 106 def sanitize
@@ -129,5 +123,6 @@ class Notice @@ -129,5 +123,6 @@ class Notice
129 end 123 end
130 end 124 end
131 end 125 end
  126 +
132 end 127 end
133 128
app/models/problem.rb 0 → 100644
@@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
  1 +# Represents a single Problem. The problem may have been
  2 +# reported as various Errs, but the user has grouped the
  3 +# Errs together as belonging to the same problem.
  4 +
  5 +class Problem
  6 + include Mongoid::Document
  7 + include Mongoid::Timestamps
  8 +
  9 + field :last_notice_at, :type => DateTime
  10 + field :resolved, :type => Boolean, :default => false
  11 + field :issue_link, :type => String
  12 +
  13 + # Cached fields
  14 + field :notices_count, :type => Integer, :default => 0
  15 + field :message
  16 + field :environment
  17 + field :klass
  18 + field :where
  19 +
  20 + index :last_notice_at
  21 + index :app_id
  22 +
  23 + belongs_to :app
  24 + has_many :errs, :inverse_of => :problem, :dependent => :destroy
  25 + has_many :comments, :inverse_of => :err, :dependent => :destroy
  26 +
  27 + scope :resolved, where(:resolved => true)
  28 + scope :unresolved, where(:resolved => false)
  29 + scope :ordered, order_by(:last_notice_at.desc)
  30 + scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
  31 +
  32 + def self.in_env(env)
  33 + env.present? ? where(:environment => env) : scoped
  34 + end
  35 +
  36 +
  37 +
  38 + def notices
  39 + Notice.for_errs(errs).ordered
  40 + end
  41 +
  42 +
  43 +
  44 + def resolve!
  45 + self.update_attributes!(:resolved => true)
  46 + end
  47 +
  48 + def unresolve!
  49 + self.update_attributes!(:resolved => false)
  50 + end
  51 +
  52 + def unresolved?
  53 + !resolved?
  54 + end
  55 +
  56 +
  57 +
  58 + def self.merge!(*problems)
  59 + problems = problems.flatten.uniq
  60 + merged_problem = problems.shift
  61 + problems.each do |problem|
  62 + merged_problem.errs.concat Err.where(:problem_id => problem.id)
  63 + problem.errs(true) # reload problem.errs (should be empty) before problem.destroy
  64 + problem.destroy
  65 + end
  66 + merged_problem.reset_cached_attributes
  67 + merged_problem
  68 + end
  69 +
  70 + def merged?
  71 + errs.length > 1
  72 + end
  73 +
  74 + def unmerge!
  75 + [self] + errs[1..-1].map(&:id).map do |err_id|
  76 + err = Err.find(err_id)
  77 + app.problems.create.tap do |new_problem|
  78 + err.update_attribute(:problem_id, new_problem.id)
  79 + new_problem.reset_cached_attributes
  80 + end
  81 + end
  82 + end
  83 +
  84 +
  85 +
  86 + def reset_cached_attributes
  87 + update_attribute(:notices_count, notices.count)
  88 + cache_notice_attributes
  89 + end
  90 +
  91 + def cache_notice_attributes(notice=nil)
  92 + notice ||= notices.first
  93 + attrs = {:last_notice_at => notices.max(:created_at)}
  94 + attrs.merge!(
  95 + :message => notice.message,
  96 + :environment => notice.environment_name,
  97 + :klass => notice.klass,
  98 + :where => notice.where) if notice
  99 + update_attributes!(attrs)
  100 + end
  101 +
  102 +
  103 +end
app/models/watcher.rb
@@ -41,3 +41,4 @@ class Watcher @@ -41,3 +41,4 @@ class Watcher
41 end 41 end
42 42
43 end 43 end
  44 +
app/views/apps/index.html.haml
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 - else 19 - else
20 n/a 20 n/a
21 %td.count 21 %td.count
22 - - if app.errs.count > 0 22 + - if app.problems.count > 0
23 - unresolved = @unresolved_counts[app.id] 23 - unresolved = @unresolved_counts[app.id]
24 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil) 24 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
25 - else 25 - else
app/views/apps/show.html.haml
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 = javascript_include_tag 'apps.show' 4 = javascript_include_tag 'apps.show'
5 - content_for :meta do 5 - content_for :meta do
6 %strong Errs Caught: 6 %strong Errs Caught:
7 - = @app.errs.count 7 + = @app.problems.count
8 %strong Deploy Count: 8 %strong Deploy Count:
9 = @app.deploys.count 9 = @app.deploys.count
10 %strong API Key: 10 %strong API Key:
@@ -82,9 +82,9 @@ @@ -82,9 +82,9 @@
82 - else 82 - else
83 %h3 No deploys 83 %h3 No deploys
84 84
85 -- if @app.errs.count > 0 85 +- if @app.problems.any?
86 %h3.clear Errs 86 %h3.clear Errs
87 - = render 'errs/table', :errs => @errs 87 + = render 'errs/table', :errs => @problems
88 - else 88 - else
89 %h3.clear No errs have been caught yet, make sure you setup your app 89 %h3.clear No errs have been caught yet, make sure you setup your app
90 = render 'configuration_instructions', :app => @app 90 = render 'configuration_instructions', :app => @app
app/views/errs/_list.atom.builder
1 -feed.updated(@errs.first.created_at) 1 +feed.updated(@problems.first.created_at)
2 2
3 -for err in @errs  
4 - notice = err.notices.first 3 +for problem in @problems
  4 + notice = problem.notices.first
5 5
6 - feed.entry(err, :url => app_err_url(err.app, err)) do |entry|  
7 - entry.title "[#{ err.where }] #{err.message.to_s.truncate(27)}" 6 + feed.entry(problem, :url => app_err_url(problem.app, problem)) do |entry|
  7 + entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}"
8 entry.author do |author| 8 entry.author do |author|
9 - author.name "#{ err.app.name } [#{ err.environment }]" 9 + author.name "#{ problem.app.name } [#{ problem.environment }]"
10 end 10 end
11 if notice 11 if notice
12 entry.summary(notice_atom_summary(notice), :type => "html") 12 entry.summary(notice_atom_summary(notice), :type => "html")
app/views/errs/_table.html.haml
1 -- any_issue_links = errs.any?{|e| e.issue_link.present? }  
2 -%table.errs  
3 - %thead  
4 - %tr  
5 - %th App  
6 - %th What &amp; Where  
7 - %th Latest  
8 - %th Deploy  
9 - %th Count  
10 - - if any_issue_links  
11 - %th Issue  
12 - %th Resolve  
13 - %tbody  
14 - - errs.each do |err|  
15 - %tr{:class => err.resolved? ? 'resolved' : 'unresolved'}  
16 - %td.app  
17 - = link_to err.app.name, app_path(err.app)  
18 - - if current_page?(:controller => 'errs')  
19 - %span.environment= link_to err.environment, errs_path(:environment => err.environment)  
20 - - else  
21 - %span.environment= link_to err.environment, app_path(err.app, :environment => err.environment)  
22 - %td.message  
23 - = link_to truncate(err.message, :length => 230), app_err_path(err.app, err)  
24 - %em= err.where  
25 - - if err.comments.any?  
26 - - comment = err.comments.last  
27 - %br  
28 - .inline_comment  
29 - %em.commenter= (Errbit::Config.user_has_username ? comment.user.username : comment.user.email).to_s << ":"  
30 - %em= truncate(comment.body, :length => 100, :separator => ' ')  
31 - %td.latest #{time_ago_in_words(last_notice_at err)} ago  
32 - %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a'  
33 - %td.count= link_to err.notices_count, app_err_path(err.app, err)  
34 - - if any_issue_links  
35 - %td.issue_link  
36 - - if err.issue_link.present?  
37 - = link_to image_tag("#{err.app.issue_tracker.class::Label}_goto.png"), err.issue_link, :target => "_blank"  
38 - %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(err.app, err), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if err.unresolved?  
39 - - if errs.none? 1 +=form_tag do
  2 + %table.errs.selectable
  3 + %thead
40 %tr 4 %tr
41 - %td{:colspan => 6}  
42 - %em No errs here  
43 -= will_paginate @errs, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'  
44 - 5 + %th
  6 + %th App
  7 + %th What &amp; Where
  8 + %th Latest
  9 + %th Deploy
  10 + %th Count
  11 + %th Resolve
  12 + %tbody
  13 + - errs.each do |problem|
  14 + %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'}
  15 + %td.select
  16 + = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s)
  17 + %td.app
  18 + = link_to problem.app.name, app_path(problem.app)
  19 + - if current_page?(:controller => 'errs')
  20 + %span.environment= link_to problem.environment, errs_path(environment: problem.environment)
  21 + - else
  22 + %span.environment= link_to problem.environment, app_path(problem.app, environment: problem.environment)
  23 + %td.message
  24 + = link_to problem.message, app_err_path(problem.app, problem)
  25 + %em= problem.where
  26 + %td.latest #{time_ago_in_words(last_notice_at problem)} ago
  27 + %td.deploy= problem.app.last_deploy_at ? problem.app.last_deploy_at.to_s(:micro) : 'n/a'
  28 + %td.count= link_to problem.notices.count, app_err_path(problem.app, problem)
  29 + %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(problem.app, problem), :title => "Resolve", :method => :put, :confirm => err_confirm, :class => 'resolve' if problem.unresolved?
  30 + - if errs.none?
  31 + %tr
  32 + %td{:colspan => (@app ? 5 : 6)}
  33 + %em No errs here
  34 + = will_paginate @problems, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'
  35 + .tab-bar
  36 + %ul
  37 + %li= submit_tag 'Merge', :id => 'merge_errs', :class => 'button', 'data-action' => merge_several_errs_path
  38 + %li= submit_tag 'Unmerge', :id => 'unmerge_errs', :class => 'button', 'data-action' => unmerge_several_errs_path
  39 + %li= submit_tag 'Resolve', :id => 'resolve_errs', :class => 'button', 'data-action' => resolve_several_errs_path
  40 + %li= submit_tag 'Unresolve', :id => 'unresolve_errs', :class => 'button', 'data-action' => unresolve_several_errs_path
  41 + %li= submit_tag 'Delete', :id => 'delete_errs', :class => 'button', 'data-action' => destroy_several_errs_path
app/views/errs/all.html.haml
1 - content_for :title, 'All Errs' 1 - content_for :title, 'All Errs'
2 - content_for :action_bar do 2 - content_for :action_bar do
3 = link_to 'hide resolved', errs_path, :class => 'button' 3 = link_to 'hide resolved', errs_path, :class => 'button'
4 -= render 'table', :errs => @errs  
5 \ No newline at end of file 4 \ No newline at end of file
  5 += render 'table', :errs => @problems
6 \ No newline at end of file 6 \ No newline at end of file
app/views/errs/index.html.haml
@@ -3,4 +3,4 @@ @@ -3,4 +3,4 @@
3 = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}" 3 = auto_discovery_link_tag :atom, errs_url(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{root_url}"
4 - content_for :action_bar do 4 - content_for :action_bar do
5 = link_to 'show resolved', all_errs_path, :class => 'button' 5 = link_to 'show resolved', all_errs_path, :class => 'button'
6 -= render 'table', :errs => @errs  
7 \ No newline at end of file 6 \ No newline at end of file
  7 += render 'table', :errs => @problems
8 \ No newline at end of file 8 \ No newline at end of file
app/views/errs/show.html.haml
1 -- content_for :page_title, @err.message  
2 -- content_for :title, @err.klass 1 +- content_for :page_title, @problem.message
  2 +- content_for :title, @problem.klass
3 - content_for :meta do 3 - content_for :meta do
4 %strong App: 4 %strong App:
5 = link_to @app.name, app_path(@app) 5 = link_to @app.name, app_path(@app)
6 %strong Where: 6 %strong Where:
7 - = @err.where 7 + = @problem.where
8 %br 8 %br
9 %strong Environment: 9 %strong Environment:
10 - = @err.environment 10 + = @problem.environment
11 %strong Last Notice: 11 %strong Last Notice:
12 - = last_notice_at(@err).to_s(:micro) 12 + = last_notice_at(@problem).to_s(:micro)
13 - content_for :action_bar do 13 - content_for :action_bar do
14 - - if @err.app.issue_tracker_configured?  
15 - - if @err.issue_link.blank?  
16 - %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => "#{@app.issue_tracker.class::Label}_create create-issue" 14 + - if @problem.app.issue_tracker_configured?
  15 + - if @problem.issue_link.blank?
  16 + %span= link_to 'create issue', create_issue_app_err_path(@app, @problem), :method => :post, :class => "#{@app.issue_tracker.class::Label}_create create-issue"
17 - else 17 - else
18 - %span= link_to 'go to issue', @err.issue_link, :target => "_blank", :class => "#{@app.issue_tracker.class::Label}_goto goto-issue"  
19 - = link_to 'unlink issue', unlink_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Unlink err issues?", :class => "unlink-issue"  
20 - - if @err.unresolved?  
21 - %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' 18 + %span= link_to 'go to issue', @problem.issue_link, :class => "#{@app.issue_tracker.class::Label}_goto goto-issue"
  19 + = link_to 'unlink issue', unlink_issue_app_err_path(@app, @problem), :method => :delete, :confirm => "Unlink err issues?", :class => "unlink-issue"
  20 + - if @problem.unresolved?
  21 + %span= link_to 'resolve', resolve_app_err_path(@app, @problem), :method => :put, :confirm => err_confirm, :class => 'resolve'
22 22
23 -- if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @err.comments.any? 23 +- if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @problem.comments.any?
24 - content_for :comments do 24 - content_for :comments do
25 %h3 Comments on this Err 25 %h3 Comments on this Err
26 - - @err.comments.each do |comment| 26 + - @problem.comments.each do |comment|
27 .window 27 .window
28 %table.comment 28 %table.comment
29 %tr 29 %tr
30 %th 30 %th
31 - %span= link_to '&#10008;'.html_safe, destroy_comment_app_err_path(@app, @err) << "?comment_id=#{comment.id}", :method => :delete, :confirm => "Are sure you don't need this comment?", :class => "destroy-comment" 31 + %span= link_to '&#10008;'.html_safe, destroy_comment_app_err_path(@app, @problem) << "?comment_id=#{comment.id}", :method => :delete, :confirm => "Are sure you don't need this comment?", :class => "destroy-comment"
32 = time_ago_in_words(comment.created_at, true) << " ago by " 32 = time_ago_in_words(comment.created_at, true) << " ago by "
33 = link_to comment.user.email, user_path(comment.user) 33 = link_to comment.user.email, user_path(comment.user)
34 %tr 34 %tr
35 %td= comment.body.gsub("\n", "<br>").html_safe 35 %td= comment.body.gsub("\n", "<br>").html_safe
36 - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? 36 - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured?
37 - = form_for @comment, :url => create_comment_app_err_path(@app, @err) do |comment_form| 37 + = form_for @comment, :url => create_comment_app_err_path(@app, @problem) do |comment_form|
38 %p Add a comment 38 %p Add a comment
39 = comment_form.text_area :body, :style => "width: 420px; height: 80px;" 39 = comment_form.text_area :body, :style => "width: 420px; height: 80px;"
40 = comment_form.submit "Save Comment" 40 = comment_form.submit "Save Comment"
app/views/issue_trackers/fogbugz_body.txt.erb
1 -"See this exception on Errbit": <%= app_err_url(err.app, err) %>  
2 -<% if notice = err.notices.first %> 1 +"See this exception on Errbit": <%= app_err_url(problem.app, problem) %>
  2 +<% if notice = problem.notices.first %>
3 <%= notice.message %> 3 <%= notice.message %>
4 4
5 Summary 5 Summary
6 - Where 6 - Where
7 - <%= notice.err.where %> 7 + <%= notice.where %>
8 8
9 - Occured 9 - Occured
10 <%= notice.created_at.to_s(:micro) %> 10 <%= notice.created_at.to_s(:micro) %>
11 11
12 - Similar 12 - Similar
13 - <%= (notice.err.notices_count - 1).to_s %> 13 + <%= (notice.problem.notices_count - 1).to_s %>
14 14
15 Params 15 Params
16 <%= pretty_hash(notice.params) %> 16 <%= pretty_hash(notice.params) %>
app/views/issue_trackers/github_issues_body.txt.erb
@@ -7,13 +7,13 @@ @@ -7,13 +7,13 @@
7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)" 7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8 <% end %> 8 <% end %>
9 ### Where ### 9 ### Where ###
10 -<%= notice.err.where %> 10 +<%= notice.where %>
11 11
12 ### Occured ### 12 ### Occured ###
13 <%= notice.created_at.to_s(:micro) %> 13 <%= notice.created_at.to_s(:micro) %>
14 14
15 ### Similar ### 15 ### Similar ###
16 -<%= (notice.err.notices_count - 1).to_s %> 16 +<%= (notice.problem.notices_count - 1).to_s %>
17 17
18 ## Params ## 18 ## Params ##
19 ``` 19 ```
app/views/issue_trackers/lighthouseapp_body.txt.erb
1 -[See this exception on Errbit](<%= app_err_url err.app, err %> "See this exception on Errbit")  
2 -<% if notice = err.notices.first %> 1 +[See this exception on Errbit](<%= app_err_url problem.app, problem %> "See this exception on Errbit")
  2 +<% if notice = problem.notices.first %>
3 # <%= notice.message %> # 3 # <%= notice.message %> #
4 ## Summary ## 4 ## Summary ##
5 <% if notice.request['url'].present? %> 5 <% if notice.request['url'].present? %>
@@ -7,13 +7,13 @@ @@ -7,13 +7,13 @@
7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)" 7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8 <% end %> 8 <% end %>
9 ### Where ### 9 ### Where ###
10 - <%= notice.err.where %> 10 + <%= notice.where %>
11 11
12 ### Occured ### 12 ### Occured ###
13 <%= notice.created_at.to_s(:micro) %> 13 <%= notice.created_at.to_s(:micro) %>
14 14
15 ### Similar ### 15 ### Similar ###
16 - <%= (notice.err.notices_count - 1).to_s %> 16 + <%= (notice.problem.notices_count - 1).to_s %>
17 17
18 ## Params ## 18 ## Params ##
19 <code><%= pretty_hash(notice.params) %></code> 19 <code><%= pretty_hash(notice.params) %></code>
app/views/issue_trackers/pivotal_body.txt.erb
1 -See this exception on Errbit: <%= app_err_url err.app, err %>  
2 -<% if notice = err.notices.first %> 1 +See this exception on Errbit: <%= app_err_url problem.app, problem %>
  2 +<% if notice = problem.notices.first %>
3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %> 3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
4 - Where: <%= notice.err.where %> 4 + Where: <%= notice.where %>
5 Occurred: <%= notice.created_at.to_s :micro %> 5 Occurred: <%= notice.created_at.to_s :micro %>
6 - Similar: <%= (notice.err.notices.count - 1).to_s %> 6 + Similar: <%= (notice.problem.notices.count - 1).to_s %>
7 7
8 Params: 8 Params:
9 <%= pretty_hash notice.params %> 9 <%= pretty_hash notice.params %>
app/views/issue_trackers/textile_body.txt.erb
1 -<% if notice = err.notices.first %> 1 +<% if notice = problem.notices.first %>
2 h1. <%= notice.message %> 2 h1. <%= notice.message %>
3 3
4 -h3. "See this exception on Errbit":<%= app_err_url err.app, err %> 4 +h3. "See this exception on Errbit":<%= app_err_url problem.app, problem %>
5 5
6 h2. Summary 6 h2. Summary
7 <% if notice.request['url'].present? %> 7 <% if notice.request['url'].present? %>
@@ -11,7 +11,7 @@ h3. URL @@ -11,7 +11,7 @@ h3. URL
11 <% end %> 11 <% end %>
12 h3. Where 12 h3. Where
13 13
14 -<%= notice.err.where %> 14 +<%= notice.where %>
15 15
16 h3. Occurred 16 h3. Occurred
17 17
@@ -19,7 +19,7 @@ h3. Occurred @@ -19,7 +19,7 @@ h3. Occurred
19 19
20 h3. Similar 20 h3. Similar
21 21
22 -<%= (notice.err.notices_count - 1).to_s %> 22 +<%= (notice.problem.notices_count - 1).to_s %>
23 23
24 h2. Params 24 h2. Params
25 25
app/views/mailer/err_notification.html.haml
@@ -9,12 +9,12 @@ @@ -9,12 +9,12 @@
9 An err has just occurred in 9 An err has just occurred in
10 = link_to(@app.name, app_url(@app), :class => "bold") << "," 10 = link_to(@app.name, app_url(@app), :class => "bold") << ","
11 on the 11 on the
12 - %span.bold= @notice.err.environment 12 + %span.bold= @notice.environment_name
13 environment. 13 environment.
14 %br 14 %br
15 - This err has occurred #{pluralize @notice.err.notices_count, 'time'}. 15 + This err has occurred #{pluralize @notice.problem.notices_count, 'time'}.
16 %p 16 %p
17 - = link_to("Click here to view the error on Errbit", app_err_url(@app, @notice.err), :class => "bold") << "." 17 + = link_to("Click here to view the error on Errbit", app_err_url(@app, @notice.problem), :class => "bold") << "."
18 %tr 18 %tr
19 %td.section 19 %td.section
20 %table(cellpadding="0" cellspacing="0" border="0" align="left") 20 %table(cellpadding="0" cellspacing="0" border="0" align="left")
@@ -23,10 +23,10 @@ @@ -23,10 +23,10 @@
23 %td.content(valign="top") 23 %td.content(valign="top")
24 %div 24 %div
25 %p.heading ERROR MESSAGE: 25 %p.heading ERROR MESSAGE:
26 - %p= @notice.err.message 26 + %p= @notice.message
27 %p.heading WHERE: 27 %p.heading WHERE:
28 %p.monospace 28 %p.monospace
29 - = @notice.err.where 29 + = @notice.where
30 - if (app_backtrace = @notice.app_backtrace).any? 30 - if (app_backtrace = @notice.app_backtrace).any?
31 - app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| 31 - app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line|
32 %p.backtrace= line 32 %p.backtrace= line
app/views/mailer/err_notification.text.erb
1 -An err has just occurred in <%= @app.name %>, on the <%= @notice.err.environment %> environment. 1 +An err has just occurred in <%= @notice.environment_name %>: <%= raw(@notice.message) %>
2 2
3 -This err has occurred <%= pluralize @notice.err.notices_count, 'time' %>. 3 +This err has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here:
4 4
5 -You can view it on Errbit here: <%= app_err_url(@app, @notice.err) %> 5 + <%= app_err_url(@app, @notice.problem) %>
6 6
7 7
8 ERROR MESSAGE: 8 ERROR MESSAGE:
9 9
10 -<%= raw(@notice.err.message) %> 10 +<%= raw(@notice.message) %>
11 11
12 12
13 WHERE: 13 WHERE:
14 14
15 -<%= @notice.err.where %> 15 +<%= @notice.where %>
16 16
17 <% @notice.app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| %> 17 <% @notice.app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| %>
18 <%= line %> 18 <%= line %>
app/views/notices/_atom_entry.html.haml
@@ -6,13 +6,13 @@ @@ -6,13 +6,13 @@
6 = link_to(notice.request['url'], notice.request['url']) 6 = link_to(notice.request['url'], notice.request['url'])
7 %p 7 %p
8 %strong Where: 8 %strong Where:
9 - = notice.err.where 9 + = notice.where
10 %p 10 %p
11 %strong Occured: 11 %strong Occured:
12 = notice.created_at.to_s(:micro) 12 = notice.created_at.to_s(:micro)
13 %p 13 %p
14 %strong Similar: 14 %strong Similar:
15 - = notice.err.notices_count - 1 15 + = notice.problem.notices_count - 1
16 16
17 %h3 Params 17 %h3 Params
18 %p= pretty_hash(notice.params) 18 %p= pretty_hash(notice.params)
app/views/notices/_summary.html.haml
@@ -9,19 +9,20 @@ @@ -9,19 +9,20 @@
9 %td.nowrap= link_to notice.request['url'], notice.request['url'] 9 %td.nowrap= link_to notice.request['url'], notice.request['url']
10 %tr 10 %tr
11 %th Where 11 %th Where
12 - %td= notice.err.where 12 + %td= notice.where
13 %tr 13 %tr
14 %th Occurred 14 %th Occurred
15 %td= notice.created_at.to_s(:micro) 15 %td= notice.created_at.to_s(:micro)
16 %tr 16 %tr
17 %th Similar 17 %th Similar
18 - %td= notice.err.notices.count - 1 18 + %td= notice.problem.notices.count - 1
19 %tr 19 %tr
20 %th Browser 20 %th Browser
21 - %td= user_agent_graph(notice.err) 21 + %td= user_agent_graph(notice.problem)
22 %tr 22 %tr
23 %th App Server 23 %th App Server
24 %td= notice.server_environment && notice.server_environment["hostname"] 24 %td= notice.server_environment && notice.server_environment["hostname"]
25 %tr 25 %tr
26 %th Rel. Directory 26 %th Rel. Directory
27 %td= notice.server_environment && notice.server_environment["project-root"] 27 %td= notice.server_environment && notice.server_environment["project-root"]
  28 +
autotest/discover.rb
1 Autotest.add_discovery { "rails" } 1 Autotest.add_discovery { "rails" }
2 Autotest.add_discovery { "rspec2" } 2 Autotest.add_discovery { "rspec2" }
  3 +
config/application.rb
@@ -39,7 +39,7 @@ module Errbit @@ -39,7 +39,7 @@ module Errbit
39 # config.i18n.default_locale = :de 39 # config.i18n.default_locale = :de
40 40
41 # JavaScript files you want as :defaults (application.js is always included). 41 # JavaScript files you want as :defaults (application.js is always included).
42 - config.action_view.javascript_expansions[:defaults] = %w(jquery rails form) 42 + config.action_view.javascript_expansions[:defaults] = %w(jquery underscore-1.1.6 rails form)
43 43
44 # > rails generate - config 44 # > rails generate - config
45 config.generators do |g| 45 config.generators do |g|
config/boot.rb
@@ -11,3 +11,4 @@ rescue Bundler::GemNotFound =&gt; e @@ -11,3 +11,4 @@ rescue Bundler::GemNotFound =&gt; e
11 STDERR.puts "Try running `bundle install`." 11 STDERR.puts "Try running `bundle install`."
12 exit! 12 exit!
13 end if File.exist?(gemfile) 13 end if File.exist?(gemfile)
  14 +
config/environment.rb
@@ -3,3 +3,4 @@ require File.expand_path(&#39;../application&#39;, __FILE__) @@ -3,3 +3,4 @@ require File.expand_path(&#39;../application&#39;, __FILE__)
3 3
4 # Initialize the rails application 4 # Initialize the rails application
5 Errbit::Application.initialize! 5 Errbit::Application.initialize!
  6 +
config/environments/production.rb
@@ -50,3 +50,4 @@ Errbit::Application.configure do @@ -50,3 +50,4 @@ Errbit::Application.configure do
50 # Send deprecation notices to registered listeners 50 # Send deprecation notices to registered listeners
51 config.active_support.deprecation = :notify 51 config.active_support.deprecation = :notify
52 end 52 end
  53 +
config/environments/test.rb
@@ -34,3 +34,4 @@ Errbit::Application.configure do @@ -34,3 +34,4 @@ Errbit::Application.configure do
34 # Print deprecation notices to the stderr 34 # Print deprecation notices to the stderr
35 config.active_support.deprecation = :stderr 35 config.active_support.deprecation = :stderr
36 end 36 end
  37 +
config/initializers/backtrace_silencers.rb
@@ -5,3 +5,4 @@ @@ -5,3 +5,4 @@
5 5
6 # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 6 # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 # Rails.backtrace_cleaner.remove_silencers! 7 # Rails.backtrace_cleaner.remove_silencers!
  8 +
config/initializers/inflections.rb
@@ -8,3 +8,4 @@ @@ -8,3 +8,4 @@
8 # inflect.irregular 'person', 'people' 8 # inflect.irregular 'person', 'people'
9 # inflect.uncountable %w( fish sheep ) 9 # inflect.uncountable %w( fish sheep )
10 # end 10 # end
  11 +
config/initializers/inherited_resources.rb
1 InheritedResources.flash_keys = [:success, :error] 1 InheritedResources.flash_keys = [:success, :error]
  2 +
config/initializers/mime_types.rb
@@ -3,3 +3,4 @@ @@ -3,3 +3,4 @@
3 # Add new mime types for use in respond_to blocks: 3 # Add new mime types for use in respond_to blocks:
4 # Mime::Type.register "text/richtext", :rtf 4 # Mime::Type.register "text/richtext", :rtf
5 # Mime::Type.register_alias "text/html", :iphone 5 # Mime::Type.register_alias "text/html", :iphone
  6 +
config/initializers/mongo.rb
@@ -8,3 +8,4 @@ if mongo = ENV[&#39;MONGOHQ_URL&#39;] || ENV[&#39;MONGOLAB_URI&#39;] @@ -8,3 +8,4 @@ if mongo = ENV[&#39;MONGOHQ_URL&#39;] || ENV[&#39;MONGOLAB_URI&#39;]
8 config.allow_dynamic_fields = false 8 config.allow_dynamic_fields = false
9 end 9 end
10 end 10 end
  11 +
config/initializers/requirements.rb 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +require 'core_ext/hash'
  2 +
config/initializers/session_store.rb
@@ -6,3 +6,4 @@ Errbit::Application.config.session_store :cookie_store, :key =&gt; &#39;_errbit_session @@ -6,3 +6,4 @@ Errbit::Application.config.session_store :cookie_store, :key =&gt; &#39;_errbit_session
6 # which shouldn't be used to store highly confidential information 6 # which shouldn't be used to store highly confidential information
7 # (create the session table with "rake db:sessions:create") 7 # (create the session table with "rake db:sessions:create")
8 # Errbit::Application.config.session_store :active_record_store 8 # Errbit::Application.config.session_store :active_record_store
  9 +
config/initializers/time_formats.rb
1 -Time::DATE_FORMATS[:micro] = '%b %d %l:%M%P'  
2 \ No newline at end of file 1 \ No newline at end of file
  2 +Time::DATE_FORMATS[:micro] = '%b %d %l:%M%P'
config/initializers/xml_backend.rb
1 -ActiveSupport::XmlMini.backend = 'Nokogiri'  
2 \ No newline at end of file 1 \ No newline at end of file
  2 +ActiveSupport::XmlMini.backend = 'Nokogiri'
config/routes.rb
@@ -6,11 +6,16 @@ Errbit::Application.routes.draw do @@ -6,11 +6,16 @@ Errbit::Application.routes.draw do
6 match '/notifier_api/v2/notices' => 'notices#create' 6 match '/notifier_api/v2/notices' => 'notices#create'
7 match '/deploys.txt' => 'deploys#create' 7 match '/deploys.txt' => 'deploys#create'
8 8
9 - resources :notices, :only => [:show]  
10 - resources :deploys, :only => [:show] 9 + resources :notices, :only => [:show]
  10 + resources :deploys, :only => [:show]
11 resources :users 11 resources :users
12 - resources :errs, :only => [:index] do 12 + resources :errs, :only => [:index] do
13 collection do 13 collection do
  14 + post :destroy_several
  15 + post :resolve_several
  16 + post :unresolve_several
  17 + post :merge_several
  18 + post :unmerge_several
14 get :all 19 get :all
15 end 20 end
16 end 21 end
@@ -20,6 +25,7 @@ Errbit::Application.routes.draw do @@ -20,6 +25,7 @@ Errbit::Application.routes.draw do
20 resources :notices 25 resources :notices
21 member do 26 member do
22 put :resolve 27 put :resolve
  28 + put :unresolve
23 post :create_issue 29 post :create_issue
24 delete :unlink_issue 30 delete :unlink_issue
25 post :create_comment 31 post :create_comment
db/migrate/20110422152027_move_notices_to_separate_collection.rb
@@ -17,10 +17,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration @@ -17,10 +17,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration
17 mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}}) 17 mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}})
18 end 18 end
19 Rake::Task["errbit:db:update_notices_count"].invoke 19 Rake::Task["errbit:db:update_notices_count"].invoke
20 - Rake::Task["errbit:db:update_err_message"].invoke 20 + Rake::Task["errbit:db:update_problem_attrs"].invoke
21 end 21 end
22 22
23 def self.down 23 def self.down
24 end 24 end
25 -  
26 end 25 end
  26 +
db/migrate/20110905134638_link_errs_to_problems.rb 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +class LinkErrsToProblems < Mongoid::Migration
  2 + def self.up
  3 +
  4 + # Copy err.klass to notice.klass
  5 + Notice.all.each do |notice|
  6 + if notice.err && (klass = notice.err['klass'])
  7 + notice.update_attribute(:klass, klass)
  8 + end
  9 + end
  10 +
  11 + # Create a Problem for each Err
  12 + Err.all.each do |err|
  13 + app_id = err['app_id']
  14 + app = app_id && App.where(:_id => app_id).first
  15 + if app
  16 + err.problem = app.problems.create
  17 + err.save
  18 + end
  19 + end
  20 +
  21 + Rake::Task["errbit:db:update_notices_count"].invoke
  22 + Rake::Task["errbit:db:update_problem_attrs"].invoke
  23 + end
  24 +
  25 + def self.down
  26 + end
  27 +end
lib/core_ext/hash.rb 0 → 100644
@@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
  1 +module CoreExt
  2 + module Hash
  3 +
  4 +
  5 +
  6 + def pick(*picks)
  7 + picks = picks.flatten
  8 + picks.inject({}) {|result, key| self.key?(key) ? result.merge(key => self[key]) : result}
  9 + end
  10 +
  11 +
  12 +
  13 + def pick!(*picks)
  14 + picks = picks.flatten
  15 + keys.each {|key| self.delete(key) unless picks.member?(key) }
  16 + end
  17 +
  18 +
  19 +
  20 + def except(*picks)
  21 + result = self.dup
  22 + result.except!(*picks)
  23 + result
  24 + end
  25 +
  26 +
  27 +
  28 + def except!(*picks)
  29 + picks = picks.flatten
  30 + keys.each {|key| self.delete(key) if picks.member?(key) }
  31 + end
  32 +
  33 +
  34 +
  35 + def inspect!(depth=0)
  36 + s = ""
  37 + self.each do |k,v|
  38 + s << (" " * depth)
  39 + s << k
  40 + s << ": "
  41 + if v.is_a?(Hash)
  42 + s << "{\n"
  43 + s << v.inspect!(depth + 2)
  44 + s << (" " * depth)
  45 + s << "}"
  46 + elsif v.is_a?(Array)
  47 + s << v.inspect
  48 + else
  49 + s << v.to_s
  50 + end
  51 + s << "\n"
  52 + end
  53 + s
  54 + end
  55 +
  56 +
  57 +
  58 + end
  59 +end
  60 +
  61 +Hash.send :include, CoreExt::Hash
lib/hoptoad.rb
1 -module Hoptoad  
2 - module V2  
3 - require 'digest/md5' 1 +require 'hoptoad/v2'
4 2
5 - class ApiVersionError < StandardError  
6 - def initialize(version)  
7 - super "Wrong API Version: Expecting v2.0, got version: #{version}"  
8 - end  
9 - end  
10 -  
11 - def self.parse_xml(xml)  
12 - xml = xml.unpack('C*').pack('U*') # Repack string into Unicode to fix invalid UTF-8 chars  
13 - parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice']  
14 - raise ApiVersionError.new(parsed['version']) unless parsed && parsed['version'].to_s == '2.0'  
15 - rekeyed = rekey(parsed)  
16 - rekeyed['fingerprint'] = Digest::MD5.hexdigest(rekeyed['error']['backtrace'].to_s)  
17 - rekeyed 3 +module Hoptoad
  4 + class ApiVersionError < StandardError
  5 + def initialize
  6 + super "Wrong API Version: Expecting v2.0"
18 end 7 end
  8 + end
19 9
20 - private 10 + def self.parse_xml!(xml)
  11 + xml = xml.unpack('C*').pack('U*') # Repack string into Unicode to fix invalid UTF-8 chars
  12 + parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice'] || raise(ApiVersionError)
  13 + processor = get_version_processor(parsed['version'])
  14 + processor.process_notice(parsed)
  15 + end
21 16
22 - def self.rekey(node)  
23 - if node.is_a?(Hash) && node.has_key?('var') && node.has_key?('key')  
24 - {node['key'] => rekey(node['var'])}  
25 - elsif node.is_a?(Hash) && node.has_key?('var')  
26 - rekey(node['var'])  
27 - elsif node.is_a?(Hash) && node.has_key?('__content__') && node.has_key?('key')  
28 - {node['key'] => node['__content__']}  
29 - elsif node.is_a?(Hash) && node.has_key?('__content__')  
30 - node['__content__']  
31 - elsif node.is_a?(Hash)  
32 - node.inject({}) {|rekeyed, (key,val)|  
33 - rekeyed.merge(key => rekey(val))  
34 - }  
35 - elsif node.is_a?(Array) && node.first.has_key?('key')  
36 - node.inject({}) {|rekeyed,keypair|  
37 - rekeyed.merge(rekey(keypair))  
38 - }  
39 - elsif node.is_a?(Array)  
40 - node.map {|n| rekey(n)}  
41 - else  
42 - node  
43 - end 17 + private
  18 + def self.get_version_processor(version)
  19 + case version
  20 + when '2.0'; Hoptoad::V2
  21 + else; raise ApiVersionError
44 end 22 end
45 - end 23 + end
46 end 24 end
47 25
lib/hoptoad/v2.rb 0 → 100644
@@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
  1 +module Hoptoad
  2 + module V2
  3 + def self.process_notice(parsed)
  4 + for_errbit_api(
  5 + normalize(
  6 + rekey(parsed)))
  7 + end
  8 +
  9 + private
  10 + def self.rekey(node)
  11 + case node
  12 + when Hash
  13 + if node.has_key?('var') && node.has_key?('key')
  14 + {normalize_key(node['key']) => rekey(node['var'])}
  15 + elsif node.has_key?('var')
  16 + rekey(node['var'])
  17 + elsif node.has_key?('__content__') && node.has_key?('key')
  18 + {normalize_key(node['key']) => rekey(node['__content__'])}
  19 + elsif node.has_key?('__content__')
  20 + rekey(node['__content__'])
  21 + else
  22 + node.inject({}) {|rekeyed, (key, val)| rekeyed.merge(normalize_key(key) => rekey(val))}
  23 + end
  24 + when Array
  25 + if node.first.has_key?('key')
  26 + node.inject({}) {|rekeyed, keypair| rekeyed.merge(rekey(keypair))}
  27 + else
  28 + node.map {|n| rekey(n)}
  29 + end
  30 + else
  31 + node
  32 + end
  33 + end
  34 +
  35 + def self.normalize_key(key)
  36 + key.gsub('.', '_')
  37 + end
  38 +
  39 + def self.normalize(notice)
  40 + error = notice['error']
  41 + backtrace = error['backtrace']
  42 + backtrace['line'] = [backtrace['line']] unless backtrace['line'].is_a?(Array)
  43 +
  44 + notice['request'] ||= {}
  45 + notice['request']['component'] = 'unknown' if notice['request']['component'].blank?
  46 + notice['request']['action'] = nil if notice['request']['action'].blank?
  47 +
  48 + notice
  49 + end
  50 +
  51 + def self.for_errbit_api(notice)
  52 + {
  53 + :klass => notice['error']['class'],
  54 + :message => notice['error']['message'],
  55 + :backtrace => notice['error']['backtrace']['line'],
  56 +
  57 + :request => notice['request'],
  58 + :server_environment => notice['server-environment'],
  59 +
  60 + :api_key => notice['api-key'],
  61 + :notifier => notice['notifier']
  62 + }
  63 + end
  64 + end
  65 +end
  66 +
lib/overrides/mongoid/relations/builder.rb
@@ -13,3 +13,4 @@ module Mongoid @@ -13,3 +13,4 @@ module Mongoid
13 end 13 end
14 end 14 end
15 end 15 end
  16 +
lib/recurse.rb
@@ -22,3 +22,4 @@ class Hash @@ -22,3 +22,4 @@ class Hash
22 end 22 end
23 23
24 end 24 end
  25 +
lib/tasks/errbit/database.rake
1 namespace :errbit do 1 namespace :errbit do
2 namespace :db do 2 namespace :db do
3 - desc "Updates Err#notices_count"  
4 - task :update_err_message => :environment do  
5 - puts "Updating err.message"  
6 - Err.all.each do |e|  
7 - e.update_attributes(:message => e.notices.first.message) if e.notices.first  
8 - end 3 +
  4 + desc "Updates cached attributes on Problem"
  5 + task :update_problem_attrs => :environment do
  6 + puts "Updating problems"
  7 + Problem.all.each(&:cache_notice_attributes)
9 end 8 end
10 -  
11 - desc "Updates Err#notices_count" 9 +
  10 + desc "Updates Problem#notices_count"
12 task :update_notices_count => :environment do 11 task :update_notices_count => :environment do
13 - puts "Updating err.notices_count"  
14 - Err.all.each do |e|  
15 - e.update_attributes(:notices_count => e.notices.count) 12 + puts "Updating problem.notices_count"
  13 + Problem.all.each do |p|
  14 + p.update_attributes(:notices_count => p.notices.count)
16 end 15 end
17 end 16 end
18 - 17 +
19 desc "Delete resolved errors from the database. (Useful for limited heroku databases)" 18 desc "Delete resolved errors from the database. (Useful for limited heroku databases)"
20 task :clear_resolved => :environment do 19 task :clear_resolved => :environment do
21 - count = Err.resolved.count  
22 - Err.resolved.each {|err| err.destroy } 20 + count = Problem.resolved.count
  21 + Problem.resolved.each {|problem| problem.destroy }
23 puts "=== Cleared #{count} resolved errors from the database." if count > 0 22 puts "=== Cleared #{count} resolved errors from the database." if count > 0
24 end 23 end
25 end 24 end
26 end 25 end
27 -  
lib/tasks/errbit/demo.rake
1 namespace :errbit do 1 namespace :errbit do
2 - desc "Add a demo app & error to your database (for testing)" 2 +
  3 + desc "Add a demo app & errors to your database (for testing)"
3 task :demo => :environment do 4 task :demo => :environment do
4 require 'factory_girl_rails' 5 require 'factory_girl_rails'
5 - Dir.glob(File.join(Rails.root,'spec/factories/*.rb')).each {|f| require f } 6 +
  7 + Dir.glob(File.join(Rails.root, 'spec/factories/*.rb')).each {|f| require f }
6 app = Factory(:app, :name => "Demo App #{Time.now.strftime("%N")}") 8 app = Factory(:app, :name => "Demo App #{Time.now.strftime("%N")}")
7 - Factory(:notice, :err => Factory(:err, :app => app))  
8 - puts "=== Created demo app: '#{app.name}', with an example error." 9 +
  10 + # Report a number of errors for the application
  11 + app.problems.delete_all
  12 +
  13 + errors = [{
  14 + :klass => "ArgumentError",
  15 + :message => "wrong number of arguments (3 for 0)"
  16 + }, {
  17 + :klass => "RuntimeError",
  18 + :message => "Could not find Red October"
  19 + }, {
  20 + :klass => "TypeError",
  21 + :message => "can't convert Symbol into Integer"
  22 + }, {
  23 + :klass => "ActiveRecord::RecordNotFound",
  24 + :message => "could not find a record with the id 5"
  25 + }, {
  26 + :klass => "NameError",
  27 + :message => "uninitialized constant Tag"
  28 + }, {
  29 + :klass => "SyntaxError",
  30 + :message => "unexpected tSTRING_BEG, expecting keyword_do or '{' or '('"
  31 + }]
  32 +
  33 + RANDOM_METHODS = ActiveSupport.methods.shuffle[1..8]
  34 +
  35 + def random_backtrace
  36 + backtrace = []
  37 + 99.times {|t| backtrace << {
  38 + 'number' => t.hash % 1000,
  39 + 'file' => "/path/to/file.rb",
  40 + 'method' => RANDOM_METHODS.shuffle.first
  41 + }}
  42 + backtrace
  43 + end
  44 +
  45 + errors.each do |error_template|
  46 + rand(34).times do
  47 +
  48 + error_report = error_template.reverse_merge({
  49 + :klass => "StandardError",
  50 + :message => "Oops. Something went wrong!",
  51 + :backtrace => random_backtrace,
  52 + :request => {
  53 + 'component' => 'main',
  54 + 'action' => 'error'
  55 + },
  56 + :server_environment => {'environment-name' => Rails.env.to_s},
  57 + :notifier => {:name => "seeds.rb"}
  58 + })
  59 +
  60 + app.report_error!(error_report)
  61 + end
  62 + end
  63 +
  64 +
  65 + Factory(:notice, :err => Factory(:err, :problem => Factory(:problem, :app => app)))
  66 + puts "=== Created demo app: '#{app.name}', with example errors."
9 end 67 end
  68 +
10 end 69 end
11 -  
public/javascripts/application.js
1 // App JS 1 // App JS
2 2
3 -$(function(){  
4 - activateTabbedPanels();  
5 -  
6 - $('#watcher_name').live("click", function() {  
7 - $(this).closest('form').find('.show').removeClass('show');  
8 - $('#app_watchers_attributes_0_user_id').addClass('show');  
9 - });  
10 -  
11 - $('#watcher_email').live("click", function() {  
12 - $(this).closest('form').find('.show').removeClass('show');  
13 - $('#app_watchers_attributes_0_email').addClass('show');  
14 - });  
15 -  
16 - $('a.copy_config').live("click", function() {  
17 - $('select.choose_other_app').show().focus();  
18 - });  
19 - $('select.choose_other_app').live("change", function() {  
20 - var loc = window.location;  
21 - window.location.href = loc.protocol + "//" + loc.host + loc.pathname +  
22 - "?copy_attributes_from=" + $(this).val();  
23 - });  
24 -});  
25 -  
26 -function activateTabbedPanels() {  
27 - $('.tab-bar a').each(function(){  
28 - var tab = $(this); 3 +$(function() {
  4 +
  5 + function init() {
  6 +
  7 + activateTabbedPanels();
  8 +
  9 + activateSelectableRows();
  10 +
  11 + $('#watcher_name').live("click", function() {
  12 + $(this).closest('form').find('.show').removeClass('show');
  13 + $('#app_watchers_attributes_0_user_id').addClass('show');
  14 + });
  15 +
  16 + $('#watcher_email').live("click", function() {
  17 + $(this).closest('form').find('.show').removeClass('show');
  18 + $('#app_watchers_attributes_0_email').addClass('show');
  19 + });
  20 +
  21 + $('a.copy_config').live("click", function() {
  22 + $('select.choose_other_app').show().focus();
  23 + });
  24 +
  25 + $('select.choose_other_app').live("change", function() {
  26 + var loc = window.location;
  27 + window.location.href = loc.protocol + "//" + loc.host + loc.pathname +
  28 + "?copy_attributes_from=" + $(this).val();
  29 + });
  30 +
  31 + $('input[type=submit][data-action]').click(function() {
  32 + $(this).closest('form').attr('action', $(this).attr('data-action'));
  33 + });
  34 + }
  35 +
  36 + function activateTabbedPanels() {
  37 + $('.tab-bar a').each(function(){
  38 + var tab = $(this);
  39 + var panel = $('#'+tab.attr('rel'));
  40 + panel.addClass('panel');
  41 + panel.find('h3').hide();
  42 + })
  43 +
  44 + $('.tab-bar a').click(function(){
  45 + activateTab($(this));
  46 + return(false);
  47 + });
  48 + activateTab($('.tab-bar a').first());
  49 + }
  50 +
  51 + function activateTab(tab) {
  52 + tab = $(tab);
29 var panel = $('#'+tab.attr('rel')); 53 var panel = $('#'+tab.attr('rel'));
30 - panel.addClass('panel');  
31 - panel.find('h3').hide();  
32 - })  
33 -  
34 - $('.tab-bar a').click(function(){  
35 - activateTab($(this));  
36 - return(false);  
37 - });  
38 - activateTab($('.tab-bar a').first());  
39 -}  
40 -  
41 -function activateTab(tab) {  
42 - tab = $(tab);  
43 - var panel = $('#'+tab.attr('rel'));  
44 -  
45 - tab.closest('.tab-bar').find('a.active').removeClass('active');  
46 - tab.addClass('active');  
47 -  
48 - $('.panel').hide();  
49 - panel.show();  
50 -}  
51 - 54 +
  55 + tab.closest('.tab-bar').find('a.active').removeClass('active');
  56 + tab.addClass('active');
  57 +
  58 + $('.panel').hide();
  59 + panel.show();
  60 + }
  61 +
  62 + function activateSelectableRows() {
  63 + $('.selectable tr').click(function(event) {
  64 + if(!_.include(['A', 'INPUT', 'BUTTON', 'TEXTAREA'], event.target.nodeName)) {
  65 + var checkbox = $(this).find('input[name="problems[]"]');
  66 + checkbox.attr('checked', !checkbox.is(':checked'));
  67 + }
  68 + })
  69 + }
  70 +
  71 + init();
  72 +});
public/javascripts/underscore-1.1.6.js 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +// Underscore.js 1.1.6
  2 +// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
  3 +// Underscore is freely distributable under the MIT license.
  4 +// Portions of Underscore are inspired or borrowed from Prototype,
  5 +// Oliver Steele's Functional, and John Resig's Micro-Templating.
  6 +// For all details and documentation:
  7 +// http://documentcloud.github.com/underscore
  8 +(function(){var p=this,C=p._,m={},i=Array.prototype,n=Object.prototype,f=i.slice,D=i.unshift,E=n.toString,l=n.hasOwnProperty,s=i.forEach,t=i.map,u=i.reduce,v=i.reduceRight,w=i.filter,x=i.every,y=i.some,o=i.indexOf,z=i.lastIndexOf;n=Array.isArray;var F=Object.keys,q=Function.prototype.bind,b=function(a){return new j(a)};typeof module!=="undefined"&&module.exports?(module.exports=b,b._=b):p._=b;b.VERSION="1.1.6";var h=b.each=b.forEach=function(a,c,d){if(a!=null)if(s&&a.forEach===s)a.forEach(c,d);else if(b.isNumber(a.length))for(var e=
  9 +0,k=a.length;e<k;e++){if(c.call(d,a[e],e,a)===m)break}else for(e in a)if(l.call(a,e)&&c.call(d,a[e],e,a)===m)break};b.map=function(a,c,b){var e=[];if(a==null)return e;if(t&&a.map===t)return a.map(c,b);h(a,function(a,g,G){e[e.length]=c.call(b,a,g,G)});return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var k=d!==void 0;a==null&&(a=[]);if(u&&a.reduce===u)return e&&(c=b.bind(c,e)),k?a.reduce(c,d):a.reduce(c);h(a,function(a,b,f){!k&&b===0?(d=a,k=!0):d=c.call(e,d,a,b,f)});if(!k)throw new TypeError("Reduce of empty array with no initial value");
  10 +return d};b.reduceRight=b.foldr=function(a,c,d,e){a==null&&(a=[]);if(v&&a.reduceRight===v)return e&&(c=b.bind(c,e)),d!==void 0?a.reduceRight(c,d):a.reduceRight(c);a=(b.isArray(a)?a.slice():b.toArray(a)).reverse();return b.reduce(a,c,d,e)};b.find=b.detect=function(a,c,b){var e;A(a,function(a,g,f){if(c.call(b,a,g,f))return e=a,!0});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(w&&a.filter===w)return a.filter(c,b);h(a,function(a,g,f){c.call(b,a,g,f)&&(e[e.length]=a)});return e};
  11 +b.reject=function(a,c,b){var e=[];if(a==null)return e;h(a,function(a,g,f){c.call(b,a,g,f)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=!0;if(a==null)return e;if(x&&a.every===x)return a.every(c,b);h(a,function(a,g,f){if(!(e=e&&c.call(b,a,g,f)))return m});return e};var A=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=!1;if(a==null)return e;if(y&&a.some===y)return a.some(c,d);h(a,function(a,b,f){if(e=c.call(d,a,b,f))return m});return e};b.include=b.contains=function(a,c){var b=
  12 +!1;if(a==null)return b;if(o&&a.indexOf===o)return a.indexOf(c)!=-1;A(a,function(a){if(b=a===c)return!0});return b};b.invoke=function(a,c){var d=f.call(arguments,2);return b.map(a,function(a){return(c.call?c||a:a[c]).apply(a,d)})};b.pluck=function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);var e={computed:-Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,
  13 +c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);var e={computed:Infinity};h(a,function(a,b,f){b=c?c.call(d,a,b,f):a;b<e.computed&&(e={value:a,computed:b})});return e.value};b.sortBy=function(a,c,d){return b.pluck(b.map(a,function(a,b,f){return{value:a,criteria:c.call(d,a,b,f)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c<d?-1:c>d?1:0}),"value")};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=
  14 +function(a){if(!a)return[];if(a.toArray)return a.toArray();if(b.isArray(a))return a;if(b.isArguments(a))return f.call(a);return b.values(a)};b.size=function(a){return b.toArray(a).length};b.first=b.head=function(a,b,d){return b!=null&&!d?f.call(a,0,b):a[0]};b.rest=b.tail=function(a,b,d){return f.call(a,b==null||d?1:b)};b.last=function(a){return a[a.length-1]};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a){return b.reduce(a,function(a,d){if(b.isArray(d))return a.concat(b.flatten(d));
  15 +a[a.length]=d;return a},[])};b.without=function(a){var c=f.call(arguments,1);return b.filter(a,function(a){return!b.include(c,a)})};b.uniq=b.unique=function(a,c){return b.reduce(a,function(a,e,f){if(0==f||(c===!0?b.last(a)!=e:!b.include(a,e)))a[a.length]=e;return a},[])};b.intersect=function(a){var c=f.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.zip=function(){for(var a=f.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),
  16 +e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d)return d=b.sortedIndex(a,c),a[d]===c?d:-1;if(o&&a.indexOf===o)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(z&&a.lastIndexOf===z)return a.lastIndexOf(b);for(var d=a.length;d--;)if(a[d]===b)return d;return-1};b.range=function(a,b,d){arguments.length<=1&&(b=a||0,a=0);d=arguments[2]||1;for(var e=Math.max(Math.ceil((b-a)/
  17 +d),0),f=0,g=Array(e);f<e;)g[f++]=a,a+=d;return g};b.bind=function(a,b){if(a.bind===q&&q)return q.apply(a,f.call(arguments,1));var d=f.call(arguments,2);return function(){return a.apply(b,d.concat(f.call(arguments)))}};b.bindAll=function(a){var c=f.call(arguments,1);c.length==0&&(c=b.functions(a));h(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var b=c.apply(this,arguments);return l.call(d,b)?d[b]:d[b]=a.apply(this,arguments)}};b.delay=
  18 +function(a,b){var d=f.call(arguments,2);return setTimeout(function(){return a.apply(a,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(f.call(arguments,1)))};var B=function(a,b,d){var e;return function(){var f=this,g=arguments,h=function(){e=null;a.apply(f,g)};d&&clearTimeout(e);if(d||!e)e=setTimeout(h,b)}};b.throttle=function(a,b){return B(a,b,!1)};b.debounce=function(a,b){return B(a,b,!0)};b.once=function(a){var b=!1,d;return function(){if(b)return d;b=!0;return d=a.apply(this,arguments)}};
  19 +b.wrap=function(a,b){return function(){var d=[a].concat(f.call(arguments));return b.apply(this,d)}};b.compose=function(){var a=f.call(arguments);return function(){for(var b=f.call(arguments),d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=F||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var b=[],d;for(d in a)l.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,
  20 +b.identity)};b.functions=b.methods=function(a){return b.filter(b.keys(a),function(c){return b.isFunction(a[c])}).sort()};b.extend=function(a){h(f.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){h(f.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,c){if(a===c)return!0;var d=typeof a;if(d!=
  21 +typeof c)return!1;if(a==c)return!0;if(!a&&c||a&&!c)return!1;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual)return a.isEqual(c);if(b.isDate(a)&&b.isDate(c))return a.getTime()===c.getTime();if(b.isNaN(a)&&b.isNaN(c))return!1;if(b.isRegExp(a)&&b.isRegExp(c))return a.source===c.source&&a.global===c.global&&a.ignoreCase===c.ignoreCase&&a.multiline===c.multiline;if(d!=="object")return!1;if(a.length&&a.length!==c.length)return!1;d=b.keys(a);var e=b.keys(c);if(d.length!=e.length)return!1;
  22 +for(var f in a)if(!(f in c)||!b.isEqual(a[f],c[f]))return!1;return!0};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(l.call(a,c))return!1;return!0};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=n||function(a){return E.call(a)==="[object Array]"};b.isArguments=function(a){return!(!a||!l.call(a,"callee"))};b.isFunction=function(a){return!(!a||!a.constructor||!a.call||!a.apply)};b.isString=function(a){return!!(a===""||a&&a.charCodeAt&&a.substr)};
  23 +b.isNumber=function(a){return!!(a===0||a&&a.toExponential&&a.toFixed)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===!0||a===!1};b.isDate=function(a){return!(!a||!a.getTimezoneOffset||!a.setUTCFullYear)};b.isRegExp=function(a){return!(!a||!a.test||!a.exec||!(a.ignoreCase||a.ignoreCase===!1))};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.noConflict=function(){p._=C;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=
  24 +0;e<a;e++)b.call(d,e)};b.mixin=function(a){h(b.functions(a),function(c){H(c,b[c]=a[c])})};var I=0;b.uniqueId=function(a){var b=I++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings;d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||
  25 +null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');";d=new Function("obj",d);return c?d(c):d};var j=function(a){this._wrapped=a};b.prototype=j.prototype;var r=function(a,c){return c?b(a).chain():a},H=function(a,c){j.prototype[a]=function(){var a=f.call(arguments);D.call(a,this._wrapped);return r(c.apply(b,a),this._chain)}};b.mixin(b);h(["pop","push","reverse","shift","sort",
  26 +"splice","unshift"],function(a){var b=i[a];j.prototype[a]=function(){b.apply(this._wrapped,arguments);return r(this._wrapped,this._chain)}});h(["concat","join","slice"],function(a){var b=i[a];j.prototype[a]=function(){return r(b.apply(this._wrapped,arguments),this._chain)}});j.prototype.chain=function(){this._chain=!0;return this};j.prototype.value=function(){return this._wrapped}})();
0 \ No newline at end of file 27 \ No newline at end of file
public/stylesheets/application.css
@@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;} @@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;}
244 } 244 }
245 245
246 /* Forms */ 246 /* Forms */
247 -form { 247 +form#new_user,
  248 +form.edit_user,
  249 +form#new_app,
  250 +form.edit_app {
248 width: 620px; 251 width: 620px;
249 } 252 }
250 form > div, form fieldset > div { margin: 1em 0;} 253 form > div, form fieldset > div { margin: 1em 0;}
@@ -289,7 +292,11 @@ form input[type=submit] { @@ -289,7 +292,11 @@ form input[type=submit] {
289 font-size: 1.2em; line-height: 1em; text-transform: uppercase; 292 font-size: 1.2em; line-height: 1em; text-transform: uppercase;
290 border: none; color: #FFF; background-color: #387fc1; 293 border: none; color: #FFF; background-color: #387fc1;
291 } 294 }
292 -form div.buttons { 295 +form input[type=submit].button {
  296 + font-size: 1em;
  297 + text-transform: none;
  298 +}
  299 +form div.buttons {
293 color: #666; 300 color: #666;
294 background: #FFF url(images/button-bg.png) 0 bottom repeat-x; 301 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
295 border-radius: 50px; 302 border-radius: 50px;
@@ -477,6 +484,7 @@ pre { @@ -477,6 +484,7 @@ pre {
477 } 484 }
478 485
479 /* Buttons */ 486 /* Buttons */
  487 +input[type="submit"].button,
480 a.button { 488 a.button {
481 display: inline-block; 489 display: inline-block;
482 padding: 0 0.8em; 490 padding: 0 0.8em;
@@ -492,6 +500,7 @@ a.button { @@ -492,6 +500,7 @@ a.button {
492 -webkit-box-shadow: inset 0px 0px 4px #FFF; 500 -webkit-box-shadow: inset 0px 0px 4px #FFF;
493 line-height: 30px; 501 line-height: 30px;
494 } 502 }
  503 +input[type="submit"]:hover.button,
495 a:hover.button { 504 a:hover.button {
496 box-shadow: 0px 0px 4px #ccc; 505 box-shadow: 0px 0px 4px #ccc;
497 -moz-box-shadow: 0px 0px 4px #ccc; 506 -moz-box-shadow: 0px 0px 4px #ccc;
@@ -508,6 +517,7 @@ a.button.active { @@ -508,6 +517,7 @@ a.button.active {
508 -webkit-box-shadow: inset 0 0 5px #999; 517 -webkit-box-shadow: inset 0 0 5px #999;
509 } 518 }
510 519
  520 +
511 /* Tab Bar */ 521 /* Tab Bar */
512 .tab-bar { 522 .tab-bar {
513 margin-bottom: 24px; 523 margin-bottom: 24px;
spec/controllers/apps_controller_spec.rb
@@ -6,6 +6,7 @@ describe AppsController do @@ -6,6 +6,7 @@ describe AppsController do
6 it_requires_authentication 6 it_requires_authentication
7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} 7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete}
8 8
  9 +
9 describe "GET /apps" do 10 describe "GET /apps" do
10 context 'when logged in as an admin' do 11 context 'when logged in as an admin' do
11 it 'finds all apps' do 12 it 'finds all apps' do
@@ -38,8 +39,7 @@ describe AppsController do @@ -38,8 +39,7 @@ describe AppsController do
38 @user = Factory(:admin) 39 @user = Factory(:admin)
39 sign_in @user 40 sign_in @user
40 @app = Factory(:app) 41 @app = Factory(:app)
41 - @err = Factory :err, :app => @app  
42 - @notice = Factory :notice, :err => @err 42 + @problem = Factory(:notice, :err => Factory(:err, :problem => Factory(:problem, :app => @app))).problem
43 end 43 end
44 44
45 it 'finds the app' do 45 it 'finds the app' do
@@ -48,50 +48,51 @@ describe AppsController do @@ -48,50 +48,51 @@ describe AppsController do
48 end 48 end
49 49
50 it "should not raise errors for app with err without notices" do 50 it "should not raise errors for app with err without notices" do
51 - Factory :err, :app => @app 51 + Factory(:err, :problem => Factory(:problem, :app => @app))
52 lambda { get :show, :id => @app.id }.should_not raise_error 52 lambda { get :show, :id => @app.id }.should_not raise_error
53 end 53 end
54 54
55 it "should list atom feed successfully" do 55 it "should list atom feed successfully" do
56 get :show, :id => @app.id, :format => "atom" 56 get :show, :id => @app.id, :format => "atom"
57 response.should be_success 57 response.should be_success
58 - response.body.should match(@err.message) 58 + response.body.should match(@problem.message)
59 end 59 end
60 60
61 context "pagination" do 61 context "pagination" do
62 before(:each) do 62 before(:each) do
63 - 35.times { Factory :err, :app => @app } 63 + 35.times { Factory(:err, :problem => Factory(:problem, :app => @app)) }
64 end 64 end
65 65
66 it "should have default per_page value for user" do 66 it "should have default per_page value for user" do
67 get :show, :id => @app.id 67 get :show, :id => @app.id
68 - assigns(:errs).size.should == User::PER_PAGE 68 + assigns(:problems).size.should == User::PER_PAGE
69 end 69 end
70 70
71 it "should be able to override default per_page value" do 71 it "should be able to override default per_page value" do
72 @user.update_attribute :per_page, 10 72 @user.update_attribute :per_page, 10
73 get :show, :id => @app.id 73 get :show, :id => @app.id
74 - assigns(:errs).size.should == 10 74 + assigns(:problems).size.should == 10
75 end 75 end
76 end 76 end
77 77
78 context 'with resolved errors' do 78 context 'with resolved errors' do
79 before(:each) do 79 before(:each) do
80 - resolved_err = Factory.create(:err, :app => @app, :resolved => true)  
81 - Factory.create(:notice, :err => resolved_err) 80 + resolved_problem = Factory(:problem, :app => @app)
  81 + Factory(:notice, :err => Factory(:err, :problem => resolved_problem))
  82 + resolved_problem.resolve!
82 end 83 end
83 84
84 context 'and no params' do 85 context 'and no params' do
85 - it 'shows only unresolved errs' do 86 + it 'shows only unresolved problems' do
86 get :show, :id => @app.id 87 get :show, :id => @app.id
87 - assigns(:errs).size.should == 1 88 + assigns(:problems).size.should == 1
88 end 89 end
89 end 90 end
90 91
91 - context 'and all_errs=true params' do 92 + context 'and all_problems=true params' do
92 it 'shows all errors' do 93 it 'shows all errors' do
93 get :show, :id => @app.id, :all_errs => true 94 get :show, :id => @app.id, :all_errs => true
94 - assigns(:errs).size.should == 2 95 + assigns(:problems).size.should == 2
95 end 96 end
96 end 97 end
97 end 98 end
@@ -100,42 +101,42 @@ describe AppsController do @@ -100,42 +101,42 @@ describe AppsController do
100 before(:each) do 101 before(:each) do
101 environments = ['production', 'test', 'development', 'staging'] 102 environments = ['production', 'test', 'development', 'staging']
102 20.times do |i| 103 20.times do |i|
103 - Factory.create(:err, :app => @app, :environment => environments[i % environments.length]) 104 + Factory.create(:problem, :app => @app, :environment => environments[i % environments.length])
104 end 105 end
105 end 106 end
106 107
107 context 'no params' do 108 context 'no params' do
108 it 'shows errs for all environments' do 109 it 'shows errs for all environments' do
109 get :show, :id => @app.id 110 get :show, :id => @app.id
110 - assigns(:errs).size.should == 21 111 + assigns(:problems).size.should == 21
111 end 112 end
112 end 113 end
113 114
114 context 'environment production' do 115 context 'environment production' do
115 it 'shows errs for just production' do 116 it 'shows errs for just production' do
116 - get :show, :id => @app.id, :environment => :production  
117 - assigns(:errs).size.should == 6 117 + get :show, :id => @app.id, :environment => 'production'
  118 + assigns(:problems).size.should == 6
118 end 119 end
119 end 120 end
120 121
121 context 'environment staging' do 122 context 'environment staging' do
122 it 'shows errs for just staging' do 123 it 'shows errs for just staging' do
123 - get :show, :id => @app.id, :environment => :staging  
124 - assigns(:errs).size.should == 5 124 + get :show, :id => @app.id, :environment => 'staging'
  125 + assigns(:problems).size.should == 5
125 end 126 end
126 end 127 end
127 128
128 context 'environment development' do 129 context 'environment development' do
129 it 'shows errs for just development' do 130 it 'shows errs for just development' do
130 - get :show, :id => @app.id, :environment => :development  
131 - assigns(:errs).size.should == 5 131 + get :show, :id => @app.id, :environment => 'development'
  132 + assigns(:problems).size.should == 5
132 end 133 end
133 end 134 end
134 135
135 context 'environment test' do 136 context 'environment test' do
136 it 'shows errs for just test' do 137 it 'shows errs for just test' do
137 - get :show, :id => @app.id, :environment => :test  
138 - assigns(:errs).size.should == 5 138 + get :show, :id => @app.id, :environment => 'test'
  139 + assigns(:problems).size.should == 5
139 end 140 end
140 end 141 end
141 end 142 end
@@ -345,5 +346,6 @@ describe AppsController do @@ -345,5 +346,6 @@ describe AppsController do
345 end 346 end
346 end 347 end
347 348
  349 +
348 end 350 end
349 351
spec/controllers/errs_controller_spec.rb
@@ -8,7 +8,8 @@ describe ErrsController do @@ -8,7 +8,8 @@ describe ErrsController do
8 :params => {:app_id => 'dummyid', :id => 'dummyid'} 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9 9
10 let(:app) { Factory(:app) } 10 let(:app) { Factory(:app) }
11 - let(:err) { Factory(:err, :app => app) } 11 + let(:err) { Factory(:err, :problem => Factory(:problem, :app => app, :environment => "production")) }
  12 +
12 13
13 describe "GET /errs" do 14 describe "GET /errs" do
14 render_views 15 render_views
@@ -16,20 +17,19 @@ describe ErrsController do @@ -16,20 +17,19 @@ describe ErrsController do
16 before(:each) do 17 before(:each) do
17 @user = Factory(:admin) 18 @user = Factory(:admin)
18 sign_in @user 19 sign_in @user
19 - @notice = Factory :notice  
20 - @err = @notice.err 20 + @problem = Factory(:notice, :err => Factory(:err, :problem => Factory(:problem, :app => app, :environment => "production"))).problem
21 end 21 end
22 22
23 it "should successfully list errs" do 23 it "should successfully list errs" do
24 get :index 24 get :index
25 response.should be_success 25 response.should be_success
26 - response.body.should match(@err.message) 26 + response.body.should match(@problem.message)
27 end 27 end
28 28
29 it "should list atom feed successfully" do 29 it "should list atom feed successfully" do
30 get :index, :format => "atom" 30 get :index, :format => "atom"
31 response.should be_success 31 response.should be_success
32 - response.body.should match(@err.message) 32 + response.body.should match(@problem.message)
33 end 33 end
34 34
35 context "pagination" do 35 context "pagination" do
@@ -39,13 +39,13 @@ describe ErrsController do @@ -39,13 +39,13 @@ describe ErrsController do
39 39
40 it "should have default per_page value for user" do 40 it "should have default per_page value for user" do
41 get :index 41 get :index
42 - assigns(:errs).size.should == User::PER_PAGE 42 + assigns(:problems).size.should == User::PER_PAGE
43 end 43 end
44 44
45 it "should be able to override default per_page value" do 45 it "should be able to override default per_page value" do
46 @user.update_attribute :per_page, 10 46 @user.update_attribute :per_page, 10
47 get :index 47 get :index
48 - assigns(:errs).size.should == 10 48 + assigns(:problems).size.should == 10
49 end 49 end
50 end 50 end
51 51
@@ -53,42 +53,42 @@ describe ErrsController do @@ -53,42 +53,42 @@ describe ErrsController do
53 before(:each) do 53 before(:each) do
54 environments = ['production', 'test', 'development', 'staging'] 54 environments = ['production', 'test', 'development', 'staging']
55 20.times do |i| 55 20.times do |i|
56 - Factory.create(:err, :environment => environments[i % environments.length]) 56 + Factory(:problem, :environment => environments[i % environments.length])
57 end 57 end
58 end 58 end
59 59
60 context 'no params' do 60 context 'no params' do
61 it 'shows errs for all environments' do 61 it 'shows errs for all environments' do
62 get :index 62 get :index
63 - assigns(:errs).size.should == 21 63 + assigns(:problems).size.should == 21
64 end 64 end
65 end 65 end
66 66
67 context 'environment production' do 67 context 'environment production' do
68 it 'shows errs for just production' do 68 it 'shows errs for just production' do
69 - get :index, :environment => :production  
70 - assigns(:errs).size.should == 6 69 + get :index, :environment => 'production'
  70 + assigns(:problems).size.should == 6
71 end 71 end
72 end 72 end
73 73
74 context 'environment staging' do 74 context 'environment staging' do
75 it 'shows errs for just staging' do 75 it 'shows errs for just staging' do
76 - get :index, :environment => :staging  
77 - assigns(:errs).size.should == 5 76 + get :index, :environment => 'staging'
  77 + assigns(:problems).size.should == 5
78 end 78 end
79 end 79 end
80 80
81 context 'environment development' do 81 context 'environment development' do
82 it 'shows errs for just development' do 82 it 'shows errs for just development' do
83 - get :index, :environment => :development  
84 - assigns(:errs).size.should == 5 83 + get :index, :environment => 'development'
  84 + assigns(:problems).size.should == 5
85 end 85 end
86 end 86 end
87 87
88 context 'environment test' do 88 context 'environment test' do
89 it 'shows errs for just test' do 89 it 'shows errs for just test' do
90 - get :index, :environment => :test  
91 - assigns(:errs).size.should == 5 90 + get :index, :environment => 'test'
  91 + assigns(:problems).size.should == 5
92 end 92 end
93 end 93 end
94 end 94 end
@@ -98,11 +98,11 @@ describe ErrsController do @@ -98,11 +98,11 @@ describe ErrsController do
98 it 'gets a paginated list of unresolved errs for the users apps' do 98 it 'gets a paginated list of unresolved errs for the users apps' do
99 sign_in(user = Factory(:user)) 99 sign_in(user = Factory(:user))
100 unwatched_err = Factory(:err) 100 unwatched_err = Factory(:err)
101 - watched_unresolved_err = Factory(:err, :app => Factory(:user_watcher, :user => user).app, :resolved => false)  
102 - watched_resolved_err = Factory(:err, :app => Factory(:user_watcher, :user => user).app, :resolved => true) 101 + watched_unresolved_err = Factory(:err, :problem => Factory(:problem, :app => Factory(:user_watcher, :user => user).app, :resolved => false))
  102 + watched_resolved_err = Factory(:err, :problem => Factory(:problem, :app => Factory(:user_watcher, :user => user).app, :resolved => true))
103 get :index 103 get :index
104 - assigns(:errs).should include(watched_unresolved_err)  
105 - assigns(:errs).should_not include(unwatched_err, watched_resolved_err) 104 + assigns(:problems).should include(watched_unresolved_err.problem)
  105 + assigns(:problems).should_not include(unwatched_err.problem, watched_resolved_err.problem)
106 end 106 end
107 end 107 end
108 end 108 end
@@ -112,25 +112,25 @@ describe ErrsController do @@ -112,25 +112,25 @@ describe ErrsController do
112 it "gets a paginated list of all errs" do 112 it "gets a paginated list of all errs" do
113 sign_in Factory(:admin) 113 sign_in Factory(:admin)
114 errs = WillPaginate::Collection.new(1,30) 114 errs = WillPaginate::Collection.new(1,30)
115 - 3.times { errs << Factory(:err) }  
116 - 3.times { errs << Factory(:err, :resolved => true)}  
117 - Err.should_receive(:ordered).and_return( 115 + 3.times { errs << Factory(:err).problem }
  116 + 3.times { errs << Factory(:err, :problem => Factory(:problem, :resolved => true)).problem }
  117 + Problem.should_receive(:ordered).and_return(
118 mock('proxy', :paginate => errs) 118 mock('proxy', :paginate => errs)
119 ) 119 )
120 get :all 120 get :all
121 - assigns(:errs).should == errs 121 + assigns(:problems).should == errs
122 end 122 end
123 end 123 end
124 124
125 context 'when logged in as a user' do 125 context 'when logged in as a user' do
126 it 'gets a paginated list of all errs for the users apps' do 126 it 'gets a paginated list of all errs for the users apps' do
127 sign_in(user = Factory(:user)) 127 sign_in(user = Factory(:user))
128 - unwatched_err = Factory(:err)  
129 - watched_unresolved_err = Factory(:err, :app => Factory(:user_watcher, :user => user).app, :resolved => false)  
130 - watched_resolved_err = Factory(:err, :app => Factory(:user_watcher, :user => user).app, :resolved => true) 128 + unwatched_err = Factory(:problem)
  129 + watched_unresolved_err = Factory(:problem, :app => Factory(:user_watcher, :user => user).app, :resolved => false)
  130 + watched_resolved_err = Factory(:problem, :app => Factory(:user_watcher, :user => user).app, :resolved => true)
131 get :all 131 get :all
132 - assigns(:errs).should include(watched_resolved_err, watched_unresolved_err)  
133 - assigns(:errs).should_not include(unwatched_err) 132 + assigns(:problems).should include(watched_resolved_err, watched_unresolved_err)
  133 + assigns(:problems).should_not include(unwatched_err)
134 end 134 end
135 end 135 end
136 end 136 end
@@ -148,17 +148,17 @@ describe ErrsController do @@ -148,17 +148,17 @@ describe ErrsController do
148 end 148 end
149 149
150 it "finds the app" do 150 it "finds the app" do
151 - get :show, :app_id => app.id, :id => err.id 151 + get :show, :app_id => app.id, :id => err.problem.id
152 assigns(:app).should == app 152 assigns(:app).should == app
153 end 153 end
154 154
155 it "finds the err" do 155 it "finds the err" do
156 - get :show, :app_id => app.id, :id => err.id  
157 - assigns(:err).should == err 156 + get :show, :app_id => app.id, :id => err.problem.id
  157 + assigns(:problem).should == err.problem
158 end 158 end
159 159
160 it "successfully render page" do 160 it "successfully render page" do
161 - get :show, :app_id => app.id, :id => err.id 161 + get :show, :app_id => app.id, :id => err.problem.id
162 response.should be_success 162 response.should be_success
163 end 163 end
164 164
@@ -167,23 +167,23 @@ describe ErrsController do @@ -167,23 +167,23 @@ describe ErrsController do
167 167
168 it "should not exist for err's app without issue tracker" do 168 it "should not exist for err's app without issue tracker" do
169 err = Factory :err 169 err = Factory :err
170 - get :show, :app_id => err.app.id, :id => err.id 170 + get :show, :app_id => err.app.id, :id => err.problem.id
171 171
172 response.body.should_not button_matcher 172 response.body.should_not button_matcher
173 end 173 end
174 174
175 it "should exist for err's app with issue tracker" do 175 it "should exist for err's app with issue tracker" do
176 tracker = Factory(:lighthouse_tracker) 176 tracker = Factory(:lighthouse_tracker)
177 - err = Factory(:err, :app => tracker.app)  
178 - get :show, :app_id => err.app.id, :id => err.id 177 + err = Factory(:err, :problem => Factory(:problem, :app => tracker.app))
  178 + get :show, :app_id => err.app.id, :id => err.problem.id
179 179
180 response.body.should button_matcher 180 response.body.should button_matcher
181 end 181 end
182 182
183 it "should not exist for err with issue_link" do 183 it "should not exist for err with issue_link" do
184 tracker = Factory(:lighthouse_tracker) 184 tracker = Factory(:lighthouse_tracker)
185 - err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host")  
186 - get :show, :app_id => err.app.id, :id => err.id 185 + err = Factory(:err, :problem => Factory(:problem, :app => tracker.app, :issue_link => "http://some.host"))
  186 + get :show, :app_id => err.app.id, :id => err.problem.id
187 187
188 response.body.should_not button_matcher 188 response.body.should_not button_matcher
189 end 189 end
@@ -196,17 +196,17 @@ describe ErrsController do @@ -196,17 +196,17 @@ describe ErrsController do
196 @unwatched_err = Factory(:err) 196 @unwatched_err = Factory(:err)
197 @watched_app = Factory(:app) 197 @watched_app = Factory(:app)
198 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app) 198 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app)
199 - @watched_err = Factory(:err, :app => @watched_app) 199 + @watched_err = Factory(:err, :problem => Factory(:problem, :app => @watched_app))
200 end 200 end
201 201
202 it 'finds the err if the user is watching the app' do 202 it 'finds the err if the user is watching the app' do
203 - get :show, :app_id => @watched_app.to_param, :id => @watched_err.id  
204 - assigns(:err).should == @watched_err 203 + get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id
  204 + assigns(:problem).should == @watched_err.problem
205 end 205 end
206 206
207 it 'raises a DocumentNotFound error if the user is not watching the app' do 207 it 'raises a DocumentNotFound error if the user is not watching the app' do
208 lambda { 208 lambda {
209 - get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id 209 + get :show, :app_id => @unwatched_err.problem.app_id, :id => @unwatched_err.problem.id
210 }.should raise_error(Mongoid::Errors::DocumentNotFound) 210 }.should raise_error(Mongoid::Errors::DocumentNotFound)
211 end 211 end
212 end 212 end
@@ -216,38 +216,38 @@ describe ErrsController do @@ -216,38 +216,38 @@ describe ErrsController do
216 before do 216 before do
217 sign_in Factory(:admin) 217 sign_in Factory(:admin)
218 218
219 - @err = Factory(:err)  
220 - App.stub(:find).with(@err.app.id).and_return(@err.app)  
221 - @err.app.errs.stub(:find).and_return(@err)  
222 - @err.stub(:resolve!) 219 + @problem = Factory(:err)
  220 + App.stub(:find).with(@problem.app.id).and_return(@problem.app)
  221 + @problem.app.problems.stub(:find).and_return(@problem.problem)
  222 + @problem.problem.stub(:resolve!)
223 end 223 end
224 224
225 it 'finds the app and the err' do 225 it 'finds the app and the err' do
226 - App.should_receive(:find).with(@err.app.id).and_return(@err.app)  
227 - @err.app.errs.should_receive(:find).and_return(@err)  
228 - put :resolve, :app_id => @err.app.id, :id => @err.id  
229 - assigns(:app).should == @err.app  
230 - assigns(:err).should == @err 226 + App.should_receive(:find).with(@problem.app.id).and_return(@problem.app)
  227 + @problem.app.problems.should_receive(:find).and_return(@problem.problem)
  228 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  229 + assigns(:app).should == @problem.app
  230 + assigns(:problem).should == @problem.problem
231 end 231 end
232 232
233 it "should resolve the issue" do 233 it "should resolve the issue" do
234 - @err.should_receive(:resolve!).and_return(true)  
235 - put :resolve, :app_id => @err.app.id, :id => @err.id 234 + @problem.problem.should_receive(:resolve!).and_return(true)
  235 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
236 end 236 end
237 237
238 it "should display a message" do 238 it "should display a message" do
239 - put :resolve, :app_id => @err.app.id, :id => @err.id 239 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
240 request.flash[:success].should match(/Great news/) 240 request.flash[:success].should match(/Great news/)
241 end 241 end
242 242
243 it "should redirect to the app page" do 243 it "should redirect to the app page" do
244 - put :resolve, :app_id => @err.app.id, :id => @err.id  
245 - response.should redirect_to(app_path(@err.app)) 244 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  245 + response.should redirect_to(app_path(@problem.app))
246 end 246 end
247 247
248 it "should redirect back to errs page" do 248 it "should redirect back to errs page" do
249 request.env["Referer"] = errs_path 249 request.env["Referer"] = errs_path
250 - put :resolve, :app_id => @err.app.id, :id => @err.id 250 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
251 response.should redirect_to(errs_path) 251 response.should redirect_to(errs_path)
252 end 252 end
253 end 253 end
@@ -262,8 +262,8 @@ describe ErrsController do @@ -262,8 +262,8 @@ describe ErrsController do
262 context "successful issue creation" do 262 context "successful issue creation" do
263 context "lighthouseapp tracker" do 263 context "lighthouseapp tracker" do
264 let(:notice) { Factory :notice } 264 let(:notice) { Factory :notice }
265 - let(:tracker) { Factory :lighthouse_tracker, :app => notice.err.app }  
266 - let(:err) { notice.err } 265 + let(:tracker) { Factory :lighthouse_tracker, :app => notice.app }
  266 + let(:problem) { notice.problem }
267 267
268 before(:each) do 268 before(:each) do
269 number = 5 269 number = 5
@@ -272,25 +272,25 @@ describe ErrsController do @@ -272,25 +272,25 @@ describe ErrsController do
272 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml"). 272 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
273 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) 273 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
274 274
275 - post :create_issue, :app_id => err.app.id, :id => err.id  
276 - err.reload 275 + post :create_issue, :app_id => problem.app.id, :id => problem.id
  276 + problem.reload
277 end 277 end
278 278
279 - it "should redirect to err page" do  
280 - response.should redirect_to( app_err_path(err.app, err) ) 279 + it "should redirect to problem page" do
  280 + response.should redirect_to( app_err_path(problem.app, problem) )
281 end 281 end
282 end 282 end
283 end 283 end
284 284
285 context "absent issue tracker" do 285 context "absent issue tracker" do
286 - let(:err) { Factory :err } 286 + let(:problem) { Factory :problem }
287 287
288 before(:each) do 288 before(:each) do
289 - post :create_issue, :app_id => err.app.id, :id => err.id 289 + post :create_issue, :app_id => problem.app.id, :id => problem.id
290 end 290 end
291 291
292 - it "should redirect to err page" do  
293 - response.should redirect_to( app_err_path(err.app, err) ) 292 + it "should redirect to problem page" do
  293 + response.should redirect_to( app_err_path(problem.app, problem) )
294 end 294 end
295 295
296 it "should set flash error message telling issue tracker of the app doesn't exist" do 296 it "should set flash error message telling issue tracker of the app doesn't exist" do
@@ -301,16 +301,16 @@ describe ErrsController do @@ -301,16 +301,16 @@ describe ErrsController do
301 context "error during request to a tracker" do 301 context "error during request to a tracker" do
302 context "lighthouseapp tracker" do 302 context "lighthouseapp tracker" do
303 let(:tracker) { Factory :lighthouse_tracker } 303 let(:tracker) { Factory :lighthouse_tracker }
304 - let(:err) { Factory :err, :app => tracker.app } 304 + let(:err) { Factory(:err, :problem => Factory(:problem, :app => tracker.app)) }
305 305
306 before(:each) do 306 before(:each) do
307 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 500) 307 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 500)
308 308
309 - post :create_issue, :app_id => err.app.id, :id => err.id 309 + post :create_issue, :app_id => err.app.id, :id => err.problem.id
310 end 310 end
311 311
312 it "should redirect to err page" do 312 it "should redirect to err page" do
313 - response.should redirect_to( app_err_path(err.app, err) ) 313 + response.should redirect_to( app_err_path(err.app, err.problem) )
314 end 314 end
315 315
316 it "should notify of connection error" do 316 it "should notify of connection error" do
@@ -326,19 +326,19 @@ describe ErrsController do @@ -326,19 +326,19 @@ describe ErrsController do
326 end 326 end
327 327
328 context "err with issue" do 328 context "err with issue" do
329 - let(:err) { Factory :err, :issue_link => "http://some.host" } 329 + let(:err) { Factory(:err, :problem => Factory(:problem, :issue_link => "http://some.host")) }
330 330
331 before(:each) do 331 before(:each) do
332 - delete :unlink_issue, :app_id => err.app.id, :id => err.id  
333 - err.reload 332 + delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
  333 + err.problem.reload
334 end 334 end
335 335
336 it "should redirect to err page" do 336 it "should redirect to err page" do
337 - response.should redirect_to( app_err_path(err.app, err) ) 337 + response.should redirect_to( app_err_path(err.app, err.problem) )
338 end 338 end
339 339
340 it "should clear issue link" do 340 it "should clear issue link" do
341 - err.issue_link.should be_nil 341 + err.problem.issue_link.should be_nil
342 end 342 end
343 end 343 end
344 344
@@ -346,12 +346,12 @@ describe ErrsController do @@ -346,12 +346,12 @@ describe ErrsController do
346 let(:err) { Factory :err } 346 let(:err) { Factory :err }
347 347
348 before(:each) do 348 before(:each) do
349 - delete :unlink_issue, :app_id => err.app.id, :id => err.id  
350 - err.reload 349 + delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
  350 + err.problem.reload
351 end 351 end
352 352
353 it "should redirect to err page" do 353 it "should redirect to err page" do
354 - response.should redirect_to( app_err_path(err.app, err) ) 354 + response.should redirect_to( app_err_path(err.app, err.problem) )
355 end 355 end
356 end 356 end
357 end 357 end
@@ -365,21 +365,21 @@ describe ErrsController do @@ -365,21 +365,21 @@ describe ErrsController do
365 end 365 end
366 366
367 context "successful comment creation" do 367 context "successful comment creation" do
368 - let(:err) { Factory(:err) } 368 + let(:problem) { Factory(:problem) }
369 let(:user) { Factory(:user) } 369 let(:user) { Factory(:user) }
370 370
371 before(:each) do 371 before(:each) do
372 - post :create_comment, :app_id => err.app.id, :id => err.id, 372 + post :create_comment, :app_id => problem.app.id, :id => problem.id,
373 :comment => { :body => "One test comment", :user_id => user.id } 373 :comment => { :body => "One test comment", :user_id => user.id }
374 - err.reload 374 + problem.reload
375 end 375 end
376 376
377 it "should create the comment" do 377 it "should create the comment" do
378 - err.comments.size.should == 1 378 + problem.comments.size.should == 1
379 end 379 end
380 380
381 - it "should redirect to err page" do  
382 - response.should redirect_to( app_err_path(err.app, err) ) 381 + it "should redirect to problem page" do
  382 + response.should redirect_to( app_err_path(problem.app, problem) )
383 end 383 end
384 end 384 end
385 end 385 end
@@ -392,20 +392,85 @@ describe ErrsController do @@ -392,20 +392,85 @@ describe ErrsController do
392 end 392 end
393 393
394 context "successful comment deletion" do 394 context "successful comment deletion" do
395 - let(:err) { Factory :err_with_comments }  
396 - let(:comment) { err.comments.first } 395 + let(:problem) { Factory(:problem_with_comments) }
  396 + let(:comment) { problem.comments.first }
397 397
398 before(:each) do 398 before(:each) do
399 - delete :destroy_comment, :app_id => err.app.id, :id => err.id, :comment_id => comment.id  
400 - err.reload 399 + delete :destroy_comment, :app_id => problem.app.id, :id => problem.id, :comment_id => comment.id
  400 + problem.reload
401 end 401 end
402 402
403 it "should delete the comment" do 403 it "should delete the comment" do
404 - err.comments.detect{|c| c.id.to_s == comment.id }.should == nil 404 + problem.comments.detect{|c| c.id.to_s == comment.id }.should == nil
405 end 405 end
406 406
407 - it "should redirect to err page" do  
408 - response.should redirect_to( app_err_path(err.app, err) ) 407 + it "should redirect to problem page" do
  408 + response.should redirect_to( app_err_path(problem.app, problem) )
  409 + end
  410 + end
  411 + end
  412 +
  413 + describe "Bulk Actions" do
  414 + before(:each) do
  415 + sign_in Factory(:admin)
  416 + @problem1 = Factory(:err, :problem => Factory(:problem, :resolved => true)).problem
  417 + @problem2 = Factory(:err, :problem => Factory(:problem, :resolved => false)).problem
  418 + end
  419 +
  420 + it "should apply to multiple problems" do
  421 + post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  422 + assigns(:selected_problems).should == [@problem1, @problem2]
  423 + end
  424 +
  425 + it "should require at least one problem" do
  426 + post :resolve_several, :problems => []
  427 + request.flash[:notice].should match(/You have not selected any/)
  428 + end
  429 +
  430 + context "POST /errs/merge_several" do
  431 + it "should require at least two problems" do
  432 + post :merge_several, :problems => [@problem1.id.to_s]
  433 + request.flash[:notice].should match(/You must select at least two/)
  434 + end
  435 +
  436 + it "should merge the problems" do
  437 + lambda {
  438 + post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  439 + assigns(:merged_problem).reload.errs.length.should == 2
  440 + }.should change(Problem, :count).by(-1)
  441 + end
  442 + end
  443 +
  444 + context "POST /errs/unmerge_several" do
  445 + it "should unmerge a merged problem" do
  446 + merged_problem = Problem.merge!(@problem1, @problem2)
  447 + merged_problem.errs.length.should == 2
  448 + lambda {
  449 + post :unmerge_several, :problems => [merged_problem.id.to_s]
  450 + merged_problem.reload.errs.length.should == 1
  451 + }.should change(Problem, :count).by(1)
  452 + end
  453 + end
  454 +
  455 + context "POST /errs/resolve_several" do
  456 + it "should resolve the issue" do
  457 + post :resolve_several, :problems => [@problem2.id.to_s]
  458 + @problem2.reload.resolved?.should == true
  459 + end
  460 + end
  461 +
  462 + context "POST /errs/unresolve_several" do
  463 + it "should unresolve the issue" do
  464 + post :unresolve_several, :problems => [@problem1.id.to_s]
  465 + @problem1.reload.resolved?.should == false
  466 + end
  467 + end
  468 +
  469 + context "POST /errs/destroy_several" do
  470 + it "should delete the errs" do
  471 + lambda {
  472 + post :destroy_several, :problems => [@problem1.id.to_s]
  473 + }.should change(Problem, :count).by(-1)
409 end 474 end
410 end 475 end
411 end 476 end
spec/controllers/notices_controller_spec.rb
@@ -7,20 +7,20 @@ describe NoticesController do @@ -7,20 +7,20 @@ describe NoticesController do
7 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read 7 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
8 @app = Factory(:app_with_watcher) 8 @app = Factory(:app_with_watcher)
9 App.stub(:find_by_api_key!).and_return(@app) 9 App.stub(:find_by_api_key!).and_return(@app)
10 - @notice = Notice.from_xml(@xml) 10 + @notice = App.report_error!(@xml)
11 11
12 request.env['Content-type'] = 'text/xml' 12 request.env['Content-type'] = 'text/xml'
13 request.env['Accept'] = 'text/xml, application/xml' 13 request.env['Accept'] = 'text/xml, application/xml'
14 end 14 end
15 15
16 it "generates a notice from xml [POST]" do 16 it "generates a notice from xml [POST]" do
17 - Notice.should_receive(:from_xml).with(@xml).and_return(@notice) 17 + App.should_receive(:report_error!).with(@xml).and_return(@notice)
18 request.should_receive(:raw_post).and_return(@xml) 18 request.should_receive(:raw_post).and_return(@xml)
19 post :create 19 post :create
20 end 20 end
21 21
22 it "generates a notice from xml [GET]" do 22 it "generates a notice from xml [GET]" do
23 - Notice.should_receive(:from_xml).with(@xml).and_return(@notice) 23 + App.should_receive(:report_error!).with(@xml).and_return(@notice)
24 get :create, {:data => @xml} 24 get :create, {:data => @xml}
25 end 25 end
26 26
@@ -29,10 +29,11 @@ describe NoticesController do @@ -29,10 +29,11 @@ describe NoticesController do
29 post :create 29 post :create
30 email = ActionMailer::Base.deliveries.last 30 email = ActionMailer::Base.deliveries.last
31 email.to.should include(@app.watchers.first.email) 31 email.to.should include(@app.watchers.first.email)
32 - email.subject.should include(@notice.err.message) 32 + email.subject.should include(@notice.message)
33 email.subject.should include("[#{@app.name}]") 33 email.subject.should include("[#{@app.name}]")
34 - email.subject.should include("[#{@notice.err.environment}]") 34 + email.subject.should include("[#{@notice.environment_name}]")
35 end 35 end
36 end 36 end
37 37
38 end 38 end
  39 +
spec/controllers/users_controller_spec.rb
@@ -215,3 +215,4 @@ describe UsersController do @@ -215,3 +215,4 @@ describe UsersController do
215 215
216 end 216 end
217 end 217 end
  218 +
spec/factories.rb
@@ -3,3 +3,4 @@ Factory.sequence(:word) {|n| &quot;word#{n}&quot;} @@ -3,3 +3,4 @@ Factory.sequence(:word) {|n| &quot;word#{n}&quot;}
3 Factory.sequence(:app_name) {|n| "App ##{n}"} 3 Factory.sequence(:app_name) {|n| "App ##{n}"}
4 Factory.sequence(:email) {|n| "email#{n}@example.com"} 4 Factory.sequence(:email) {|n| "email#{n}@example.com"}
5 Factory.sequence(:user_email) {|n| "user.#{n}@example.com"} 5 Factory.sequence(:user_email) {|n| "user.#{n}@example.com"}
  6 +
spec/factories/app_factories.rb
@@ -20,7 +20,7 @@ Factory.define(:user_watcher, :parent =&gt; :watcher) do |w| @@ -20,7 +20,7 @@ Factory.define(:user_watcher, :parent =&gt; :watcher) do |w|
20 end 20 end
21 21
22 Factory.define(:deploy) do |d| 22 Factory.define(:deploy) do |d|
23 - d.app {|p| p.association :app} 23 + d.app {|p| p.association :app}
24 d.username 'clyde.frog' 24 d.username 'clyde.frog'
25 d.repository 'git@github.com/errbit/errbit.git' 25 d.repository 'git@github.com/errbit/errbit.git'
26 d.environment 'production' 26 d.environment 'production'
spec/factories/err_factories.rb
  1 +Factory.define :problem do |p|
  2 + p.app {|a| a.association :app}
  3 + p.comments []
  4 +end
  5 +
  6 +Factory.define(:problem_with_comments, :parent => :problem) do |ec|
  7 + ec.comments { (1..3).map { Factory(:comment) } }
  8 +end
  9 +
  10 +
  11 +
1 Factory.define :err do |e| 12 Factory.define :err do |e|
2 - e.app {|p| p.association :app } 13 + e.problem {|p| p.association :problem}
3 e.klass 'FooError' 14 e.klass 'FooError'
4 e.component 'foo' 15 e.component 'foo'
5 e.action 'bar' 16 e.action 'bar'
6 e.environment 'production' 17 e.environment 'production'
7 - e.comments []  
8 end 18 end
9 19
10 -Factory.define(:err_with_comments, :parent => :err) do |ec|  
11 - ec.comments { (1..3).map{Factory(:comment)} }  
12 -end 20 +
13 21
14 Factory.define :notice do |n| 22 Factory.define :notice do |n|
15 n.err {|e| e.association :err} 23 n.err {|e| e.association :err}
16 n.message 'FooError: Too Much Bar' 24 n.message 'FooError: Too Much Bar'
17 n.backtrace { random_backtrace } 25 n.backtrace { random_backtrace }
18 - n.server_environment 'server-environment' => 'production' 26 + n.server_environment 'environment-name' => 'production'
  27 + n.request {{ 'component' => 'foo', 'action' => 'bar' }}
19 n.notifier 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' 28 n.notifier 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com'
20 end 29 end
21 30
spec/factories/user_factories.rb
@@ -7,4 +7,4 @@ end @@ -7,4 +7,4 @@ end
7 7
8 Factory.define :admin, :parent => :user do |a| 8 Factory.define :admin, :parent => :user do |a|
9 a.admin true 9 a.admin true
10 -end  
11 \ No newline at end of file 10 \ No newline at end of file
  11 +end
spec/fixtures/hoptoad_test_notice_with_one_line_of_backtrace.xml 0 → 100644
@@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<notice version="2.0">
  3 + <api-key>APIKEY</api-key>
  4 + <notifier>
  5 + <name>Hoptoad Notifier</name>
  6 + <version>2.3.2</version>
  7 + <url>http://hoptoadapp.com</url>
  8 + </notifier>
  9 + <error>
  10 + <class>HoptoadTestingException</class>
  11 + <message>HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.</message>
  12 + <backtrace>
  13 + <line number="425" file="[GEM_ROOT]/gems/activesupport-3.0.0.rc/lib/active_support/callbacks.rb" method="_run__2115867319__process_action__262109504__callbacks"/>
  14 + </backtrace>
  15 + </error>
  16 + <request>
  17 + <url>http://example.org/verify</url>
  18 + <component>application</component>
  19 + <action>verify</action>
  20 + <params>
  21 + <var key="action">verify</var>
  22 + <var key="controller">application</var>
  23 + </params>
  24 + <cgi-data>
  25 + <var key="rack.session"/>
  26 + <var key="action_dispatch.request.formats">text/html</var>
  27 + <var key="action_dispatch.request.parameters">
  28 + <var key="action">verify</var>
  29 + <var key="controller">application</var>
  30 + </var>
  31 + <var key="SERVER_NAME">example.org</var>
  32 + <var key="rack.url_scheme">http</var>
  33 + <var key="action_dispatch.remote_ip"/>
  34 + <var key="CONTENT_LENGTH">0</var>
  35 + <var key="rack.errors">#&lt;StringIO:0x103d9dec0&gt;</var>
  36 + <var key="action_dispatch.request.unsigned_session_cookie"/>
  37 + <var key="action_dispatch.request.query_parameters"/>
  38 + <var key="HTTPS">off</var>
  39 + <var key="rack.run_once">false</var>
  40 + <var key="PATH_INFO">/verify</var>
  41 + <var key="action_dispatch.secret_token">994f235e3372684bc736dd11842b754d2ddcffc8c2958d33a29527c3217becd6655fa4653a318bc7c34131f9baf2acc0c424ed07e48e0e5e87c6cd34d711e985</var>
  42 + <var key="rack.version">11</var>
  43 + <var key="SCRIPT_NAME"/>
  44 + <var key="action_dispatch.request.path_parameters">
  45 + <var key="action">verify</var>
  46 + <var key="controller">application</var>
  47 + </var>
  48 + <var key="rack.multithread">false</var>
  49 + <var key="action_dispatch.parameter_filter">password</var>
  50 + <var key="action_dispatch.cookies"/>
  51 + <var key="action_dispatch.request.request_parameters"/>
  52 + <var key="rack.multiprocess">true</var>
  53 + <var key="rack.request.query_hash"/>
  54 + <var key="SERVER_PORT">80</var>
  55 + <var key="REQUEST_METHOD">GET</var>
  56 + <var key="action_controller.instance">#&lt;ApplicationController:0x103d2f560&gt;</var>
  57 + <var key="rack.session.options">
  58 + <var key="secure">false</var>
  59 + <var key="httponly">true</var>
  60 + <var key="path">/</var>
  61 + <var key="expire_after"/>
  62 + <var key="domain"/>
  63 + <var key="id"/>
  64 + </var>
  65 + <var key="rack.input">#&lt;StringIO:0x103d9dc90&gt;</var>
  66 + <var key="action_dispatch.request.content_type"/>
  67 + <var key="rack.request.query_string"/>
  68 + <var key="QUERY_STRING"/>
  69 + </cgi-data>
  70 + </request>
  71 + <server-environment>
  72 + <project-root>/path/to/sample/project</project-root>
  73 + <environment-name>development</environment-name>
  74 + </server-environment>
  75 +</notice>
0 \ No newline at end of file 76 \ No newline at end of file
spec/models/app_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe App do 3 describe App do
4 -  
5 context 'validations' do 4 context 'validations' do
6 it 'requires a name' do 5 it 'requires a name' do
7 app = Factory.build(:app, :name => nil) 6 app = Factory.build(:app, :name => nil)
@@ -24,6 +23,7 @@ describe App do @@ -24,6 +23,7 @@ describe App do
24 end 23 end
25 end 24 end
26 25
  26 +
27 context 'being created' do 27 context 'being created' do
28 it 'generates a new api-key' do 28 it 'generates a new api-key' do
29 app = Factory.build(:app) 29 app = Factory.build(:app)
@@ -99,6 +99,7 @@ describe App do @@ -99,6 +99,7 @@ describe App do
99 end 99 end
100 end 100 end
101 101
  102 +
102 context "copying attributes from existing app" do 103 context "copying attributes from existing app" do
103 it "should only copy the necessary fields" do 104 it "should only copy the necessary fields" do
104 @app, @copy_app = Factory(:app, :name => "app", :github_url => "url"), 105 @app, @copy_app = Factory(:app, :name => "app", :github_url => "url"),
@@ -110,5 +111,127 @@ describe App do @@ -110,5 +111,127 @@ describe App do
110 @app.watchers.first.email.should == "copywatcher@example.com" 111 @app.watchers.first.email.should == "copywatcher@example.com"
111 end 112 end
112 end 113 end
  114 +
  115 +
  116 + context '#find_or_create_err!' do
  117 + before do
  118 + @app = Factory(:app)
  119 + @conditions = {
  120 + :klass => 'Whoops',
  121 + :component => 'Foo',
  122 + :action => 'bar',
  123 + :environment => 'production'
  124 + }
  125 + end
  126 +
  127 + it 'returns the correct err if one already exists' do
  128 + existing = Factory(:err, @conditions.merge(:problem => Factory(:problem, :app => @app)))
  129 + Err.where(@conditions).first.should == existing
  130 + @app.find_or_create_err!(@conditions).should == existing
  131 + end
  132 +
  133 + it 'assigns the returned err to the given app' do
  134 + @app.find_or_create_err!(@conditions).app.should == @app
  135 + end
  136 +
  137 + it 'creates a new problem if a matching one does not already exist' do
  138 + Err.where(@conditions).first.should be_nil
  139 + lambda {
  140 + @app.find_or_create_err!(@conditions)
  141 + }.should change(Problem,:count).by(1)
  142 + end
  143 + end
  144 +
  145 +
  146 + context '#report_error!' do
  147 + before do
  148 + @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
  149 + @app = Factory(:app, :api_key => 'APIKEY')
  150 + ErrorReport.any_instance.stub(:fingerprint).and_return('fingerprintdigest')
  151 + end
  152 +
  153 + it 'finds the correct app' do
  154 + @notice = App.report_error!(@xml)
  155 + @notice.err.app.should == @app
  156 + end
  157 +
  158 + it 'finds the correct err for the notice' do
  159 + App.should_receive(:find_by_api_key!).and_return(@app)
  160 + @app.should_receive(:find_or_create_err!).with({
  161 + :klass => 'HoptoadTestingException',
  162 + :component => 'application',
  163 + :action => 'verify',
  164 + :environment => 'development',
  165 + :fingerprint => 'fingerprintdigest'
  166 + }).and_return(err = Factory(:err))
  167 + err.notices.stub(:create!)
  168 + @notice = App.report_error!(@xml)
  169 + end
  170 +
  171 + it 'marks the err as unresolved if it was previously resolved' do
  172 + App.should_receive(:find_by_api_key!).and_return(@app)
  173 + @app.should_receive(:find_or_create_err!).with({
  174 + :klass => 'HoptoadTestingException',
  175 + :component => 'application',
  176 + :action => 'verify',
  177 + :environment => 'development',
  178 + :fingerprint => 'fingerprintdigest'
  179 + }).and_return(err = Factory(:err, :problem => Factory(:problem, :resolved => true)))
  180 + err.should be_resolved
  181 + @notice = App.report_error!(@xml)
  182 + @notice.err.should == err
  183 + @notice.err.should_not be_resolved
  184 + end
  185 +
  186 + it 'should create a new notice' do
  187 + @notice = App.report_error!(@xml)
  188 + @notice.should be_persisted
  189 + end
  190 +
  191 + it 'assigns an err to the notice' do
  192 + @notice = App.report_error!(@xml)
  193 + @notice.err.should be_a(Err)
  194 + end
  195 +
  196 + it 'captures the err message' do
  197 + @notice = App.report_error!(@xml)
  198 + @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
  199 + end
  200 +
  201 + it 'captures the backtrace' do
  202 + @notice = App.report_error!(@xml)
  203 + @notice.backtrace.size.should == 73
  204 + @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake'
  205 + end
  206 +
  207 + it 'captures the server_environment' do
  208 + @notice = App.report_error!(@xml)
  209 + @notice.server_environment['environment-name'].should == 'development'
  210 + end
  211 +
  212 + it 'captures the request' do
  213 + @notice = App.report_error!(@xml)
  214 + @notice.request['url'].should == 'http://example.org/verify'
  215 + @notice.request['params']['controller'].should == 'application'
  216 + end
  217 +
  218 + it 'captures the notifier' do
  219 + @notice = App.report_error!(@xml)
  220 + @notice.notifier['name'].should == 'Hoptoad Notifier'
  221 + end
  222 +
  223 + it "should handle params without 'request' section" do
  224 + xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read
  225 + lambda { App.report_error!(xml) }.should_not raise_error
  226 + end
  227 +
  228 + it "should handle params with only a single line of backtrace" do
  229 + xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read
  230 + lambda { @notice = App.report_error!(xml) }.should_not raise_error
  231 + @notice.backtrace.length.should == 1
  232 + end
  233 + end
  234 +
  235 +
113 end 236 end
114 237
spec/models/deploy_spec.rb
@@ -26,20 +26,20 @@ describe Deploy do @@ -26,20 +26,20 @@ describe Deploy do
26 context 'when the app has resolve_errs_on_deploy set to false' do 26 context 'when the app has resolve_errs_on_deploy set to false' do
27 it 'should not resolve the apps errs' do 27 it 'should not resolve the apps errs' do
28 app = Factory(:app, :resolve_errs_on_deploy => false) 28 app = Factory(:app, :resolve_errs_on_deploy => false)
29 - @errs = 3.times.inject([]) {|errs,_| errs << Factory(:err, :resolved => false, :app => app)} 29 + @problems = 3.times.map{Factory(:err, :problem => Factory(:problem, :resolved => false, :app => app))}
30 Factory(:deploy, :app => app) 30 Factory(:deploy, :app => app)
31 - app.reload.errs.none?{|err| err.resolved?}.should == true 31 + app.reload.problems.none?{|problem| problem.resolved?}.should == true
32 end 32 end
33 end 33 end
34 34
35 context 'when the app has resolve_errs_on_deploy set to true' do 35 context 'when the app has resolve_errs_on_deploy set to true' do
36 it 'should resolve the apps errs that were in the same environment' do 36 it 'should resolve the apps errs that were in the same environment' do
37 app = Factory(:app, :resolve_errs_on_deploy => true) 37 app = Factory(:app, :resolve_errs_on_deploy => true)
38 - @prod_errs = 3.times.inject([]) {|errs,_| errs << Factory(:err, :resolved => false, :app => app, :environment => 'production')}  
39 - @staging_errs = 3.times.inject([]) {|errs,_| errs << Factory(:err, :resolved => false, :app => app, :environment => 'staging')} 38 + @prod_errs = 3.times.map{Factory(:problem, :resolved => false, :app => app, :environment => 'production')}
  39 + @staging_errs = 3.times.map{Factory(:problem, :resolved => false, :app => app, :environment => 'staging')}
40 Factory(:deploy, :app => app, :environment => 'production') 40 Factory(:deploy, :app => app, :environment => 'production')
41 - @prod_errs.all?{|err| err.reload.resolved?}.should == true  
42 - @staging_errs.all?{|err| err.reload.resolved?}.should == false 41 + @prod_errs.all?{|problem| problem.reload.resolved?}.should == true
  42 + @staging_errs.all?{|problem| problem.reload.resolved?}.should == false
43 end 43 end
44 end 44 end
45 45
spec/models/err_spec.rb
@@ -16,157 +16,5 @@ describe Err do @@ -16,157 +16,5 @@ describe Err do
16 end 16 end
17 end 17 end
18 18
19 - context '#for' do  
20 - before do  
21 - @app = Factory(:app)  
22 - @conditions = {  
23 - :app => @app,  
24 - :klass => 'Whoops',  
25 - :component => 'Foo',  
26 - :action => 'bar',  
27 - :environment => 'production'  
28 - }  
29 - end  
30 -  
31 - it 'returns the correct err if one already exists' do  
32 - existing = Err.create(@conditions)  
33 - Err.for(@conditions).should == existing  
34 - end  
35 -  
36 - it 'assigns the returned err to the given app' do  
37 - Err.for(@conditions).app.should == @app  
38 - end  
39 -  
40 - it 'creates a new err if a matching one does not already exist' do  
41 - Err.where(@conditions.except(:app)).exists?.should == false  
42 - lambda {  
43 - Err.for(@conditions)  
44 - }.should change(Err,:count).by(1)  
45 - end  
46 - end  
47 -  
48 - context '#last_notice_at' do  
49 - it "returns the created_at timestamp of the latest notice" do  
50 - err = Factory(:err)  
51 - err.last_notice_at.should be_nil  
52 -  
53 - notice1 = Factory(:notice, :err => err)  
54 - err.last_notice_at.should == notice1.created_at  
55 -  
56 - notice2 = Factory(:notice, :err => err)  
57 - err.last_notice_at.should == notice2.created_at  
58 - end  
59 - end  
60 -  
61 - context '#message' do  
62 - it "returns klass by default" do  
63 - err = Factory(:err)  
64 - err.message.should == err.klass  
65 - end  
66 -  
67 - it 'returns the message from the first notice' do  
68 - err = Factory(:err)  
69 - notice1 = Factory(:notice, :err => err, :message => 'ERR 1')  
70 - notice2 = Factory(:notice, :err => err, :message => 'ERR 2')  
71 - err.message.should == notice1.message  
72 - end  
73 -  
74 - it "adding a notice caches its message" do  
75 - err = Factory(:err)  
76 - lambda {  
77 - notice1 = Factory(:notice, :err => err, :message => 'ERR 1')}.should change(err, :message).from(err.klass).to('ERR 1')  
78 - end  
79 - end  
80 -  
81 - context "#resolved?" do  
82 - it "should start out as unresolved" do  
83 - err = Err.new  
84 - err.should_not be_resolved  
85 - err.should be_unresolved  
86 - end  
87 -  
88 - it "should be able to be resolved" do  
89 - err = Factory(:err)  
90 - err.should_not be_resolved  
91 - err.resolve!  
92 - err.reload.should be_resolved  
93 - end  
94 - end  
95 -  
96 - context "resolve!" do  
97 - it "marks the err as resolved" do  
98 - err = Factory(:err)  
99 - err.should_not be_resolved  
100 - err.resolve!  
101 - err.should be_resolved  
102 - end  
103 -  
104 - it "should throw an err if it's not successful" do  
105 - err = Factory(:err)  
106 - err.should_not be_resolved  
107 - err.klass = nil  
108 - err.should_not be_valid  
109 - lambda {  
110 - err.resolve!  
111 - }.should raise_error(Mongoid::Errors::Validations)  
112 - end  
113 - end  
114 -  
115 - context "Scopes" do  
116 - context "resolved" do  
117 - it 'only finds resolved Errs' do  
118 - resolved = Factory(:err, :resolved => true)  
119 - unresolved = Factory(:err, :resolved => false)  
120 - Err.resolved.all.should include(resolved)  
121 - Err.resolved.all.should_not include(unresolved)  
122 - end  
123 - end  
124 -  
125 - context "unresolved" do  
126 - it 'only finds unresolved Errs' do  
127 - resolved = Factory(:err, :resolved => true)  
128 - unresolved = Factory(:err, :resolved => false)  
129 - Err.unresolved.all.should_not include(resolved)  
130 - Err.unresolved.all.should include(unresolved)  
131 - end  
132 - end  
133 - end  
134 -  
135 - context 'being created' do  
136 - context 'when the app has err notifications set to false' do  
137 - it 'should not send an email notification' do  
138 - app = Factory(:app_with_watcher, :notify_on_errs => false)  
139 - Mailer.should_not_receive(:err_notification)  
140 - Factory(:err, :app => app)  
141 - end  
142 - end  
143 - end  
144 -  
145 - context "notice counter cache" do  
146 -  
147 - before do  
148 - @app = Factory(:app)  
149 - @err = Factory(:err, :app => @app)  
150 - end  
151 -  
152 - it "#notices_count returns 0 by default" do  
153 - @err.notices_count.should == 0  
154 - end  
155 -  
156 - it "adding a notice increases #notices_count by 1" do  
157 - lambda {  
158 - notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')}.should change(@err, :notices_count).from(0).to(1)  
159 - end  
160 -  
161 - it "removing a notice decreases #notices_count by 1" do  
162 - notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')  
163 - lambda {  
164 - @err.notices.first.destroy  
165 - @err.reload  
166 - }.should change(@err, :notices_count).from(1).to(0)  
167 - end  
168 - end  
169 -  
170 -  
171 end 19 end
172 20
spec/models/issue_trackers/fogbugz_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe FogbugzTracker do 3 describe FogbugzTracker do
4 - it "should create an issue on Fogbugz with err params, and set issue link for err" do 4 + it "should create an issue on Fogbugz with problem params, and set issue link for problem" do
5 notice = Factory :notice 5 notice = Factory :notice
6 - tracker = Factory :fogbugz_tracker, :app => notice.err.app  
7 - err = notice.err 6 + tracker = Factory :fogbugz_tracker, :app => notice.app
  7 + problem = notice.problem
8 8
9 number = 123 9 number = 123
10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}" 10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}"
@@ -14,10 +14,10 @@ describe FogbugzTracker do @@ -14,10 +14,10 @@ describe FogbugzTracker do
14 http_mock.should_receive(:request).twice.and_return(response) 14 http_mock.should_receive(:request).twice.and_return(response)
15 Fogbugz.adapter[:http] = http_mock 15 Fogbugz.adapter[:http] = http_mock
16 16
17 - err.app.issue_tracker.create_issue(err)  
18 - err.reload 17 + problem.app.issue_tracker.create_issue(problem)
  18 + problem.reload
19 19
20 - err.issue_link.should == @issue_link 20 + problem.issue_link.should == @issue_link
21 end 21 end
22 end 22 end
23 23
spec/models/issue_trackers/github_issues_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe GithubIssuesTracker do 3 describe GithubIssuesTracker do
4 - it "should create an issue on Github Issues with err params, and set issue link for err" do 4 + it "should create an issue on Github Issues with problem params, and set issue link for problem" do
5 notice = Factory :notice 5 notice = Factory :notice
6 - tracker = Factory :github_issues_tracker, :app => notice.err.app  
7 - err = notice.err 6 + tracker = Factory :github_issues_tracker, :app => notice.app
  7 + problem = notice.problem
8 8
9 number = 5 9 number = 5
10 @issue_link = "https://github.com/#{tracker.project_id}/issues/#{number}" 10 @issue_link = "https://github.com/#{tracker.project_id}/issues/#{number}"
@@ -27,15 +27,15 @@ EOF @@ -27,15 +27,15 @@ EOF
27 stub_request(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}"). 27 stub_request(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}").
28 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) 28 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
29 29
30 - err.app.issue_tracker.create_issue(err)  
31 - err.reload 30 + problem.app.issue_tracker.create_issue(problem)
  31 + problem.reload
32 32
33 requested = have_requested(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}") 33 requested = have_requested(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}")
34 WebMock.should requested.with(:headers => {'Content-Type' => 'application/x-www-form-urlencoded'}) 34 WebMock.should requested.with(:headers => {'Content-Type' => 'application/x-www-form-urlencoded'})
35 WebMock.should requested.with(:body => /title=%5Bproduction%5D%5Bfoo%23bar%5D%20FooError%3A%20Too%20Much%20Bar/) 35 WebMock.should requested.with(:body => /title=%5Bproduction%5D%5Bfoo%23bar%5D%20FooError%3A%20Too%20Much%20Bar/)
36 WebMock.should requested.with(:body => /See%20this%20exception%20on%20Errbit/) 36 WebMock.should requested.with(:body => /See%20this%20exception%20on%20Errbit/)
37 37
38 - err.issue_link.should == @issue_link 38 + problem.issue_link.should == @issue_link
39 end 39 end
40 end 40 end
41 41
spec/models/issue_trackers/lighthouse_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe LighthouseTracker do 3 describe LighthouseTracker do
4 - it "should create an issue on Lighthouse with err params, and set issue link for err" do 4 + it "should create an issue on Lighthouse with problem params, and set issue link for problem" do
5 notice = Factory :notice 5 notice = Factory :notice
6 - tracker = Factory :lighthouse_tracker, :app => notice.err.app  
7 - err = notice.err 6 + tracker = Factory :lighthouse_tracker, :app => notice.app
  7 + problem = notice.problem
8 8
9 number = 5 9 number = 5
10 @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml" 10 @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
@@ -12,16 +12,16 @@ describe LighthouseTracker do @@ -12,16 +12,16 @@ describe LighthouseTracker do
12 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml"). 12 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
13 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) 13 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
14 14
15 - err.app.issue_tracker.create_issue(err)  
16 - err.reload 15 + problem.app.issue_tracker.create_issue(problem)
  16 + problem.reload
17 17
18 requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml") 18 requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml")
19 WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token}) 19 WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token})
20 WebMock.should requested.with(:body => /<tag>errbit<\/tag>/) 20 WebMock.should requested.with(:body => /<tag>errbit<\/tag>/)
21 - WebMock.should requested.with(:body => /<title>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/title>/) 21 + WebMock.should requested.with(:body => /<title>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/title>/)
22 WebMock.should requested.with(:body => /<body>.+<\/body>/m) 22 WebMock.should requested.with(:body => /<body>.+<\/body>/m)
23 23
24 - err.issue_link.should == @issue_link.sub(/\.xml$/, '') 24 + problem.issue_link.should == @issue_link.sub(/\.xml$/, '')
25 end 25 end
26 end 26 end
27 27
spec/models/issue_trackers/mingle_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe MingleTracker do 3 describe MingleTracker do
4 - it "should create an issue on Mingle with err params, and set issue link for err" do 4 + it "should create an issue on Mingle with problem params, and set issue link for problem" do
5 notice = Factory :notice 5 notice = Factory :notice
6 - tracker = Factory :mingle_tracker, :app => notice.err.app  
7 - err = notice.err 6 + tracker = Factory :mingle_tracker, :app => notice.app
  7 + problem = notice.problem
8 8
9 number = 5 9 number = 5
10 @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/cards/#{number}.xml" 10 @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/cards/#{number}.xml"
@@ -13,8 +13,8 @@ describe MingleTracker do @@ -13,8 +13,8 @@ describe MingleTracker do
13 stub_request(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml"). 13 stub_request(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml").
14 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) 14 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
15 15
16 - err.app.issue_tracker.create_issue(err)  
17 - err.reload 16 + problem.app.issue_tracker.create_issue(problem)
  17 + problem.reload
18 18
19 requested = have_requested(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml") 19 requested = have_requested(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml")
20 WebMock.should requested.with(:headers => {'Content-Type' => 'application/xml'}) 20 WebMock.should requested.with(:headers => {'Content-Type' => 'application/xml'})
@@ -22,7 +22,7 @@ describe MingleTracker do @@ -22,7 +22,7 @@ describe MingleTracker do
22 WebMock.should requested.with(:body => /See this exception on Errbit/) 22 WebMock.should requested.with(:body => /See this exception on Errbit/)
23 WebMock.should requested.with(:body => /<card-type-name>Defect<\/card-type-name>/) 23 WebMock.should requested.with(:body => /<card-type-name>Defect<\/card-type-name>/)
24 24
25 - err.issue_link.should == @issue_link.sub(/\.xml$/, '') 25 + problem.issue_link.should == @issue_link.sub(/\.xml$/, '')
26 end 26 end
27 end 27 end
28 28
spec/models/issue_trackers/pivotal_labs_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe PivotalLabsTracker do 3 describe PivotalLabsTracker do
4 - it "should create an issue on Pivotal Tracker with err params, and set issue link for err" do 4 + it "should create an issue on Pivotal Tracker with problem params, and set issue link for problem" do
5 notice = Factory :notice 5 notice = Factory :notice
6 - tracker = Factory :pivotal_labs_tracker, :app => notice.err.app, :project_id => 10  
7 - err = notice.err 6 + tracker = Factory :pivotal_labs_tracker, :app => notice.app, :project_id => 10
  7 + problem = notice.problem
8 8
9 story_id = 5 9 story_id = 5
10 @issue_link = "https://www.pivotaltracker.com/story/show/#{story_id}" 10 @issue_link = "https://www.pivotaltracker.com/story/show/#{story_id}"
@@ -15,16 +15,16 @@ describe PivotalLabsTracker do @@ -15,16 +15,16 @@ describe PivotalLabsTracker do
15 stub_request(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories"). 15 stub_request(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories").
16 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => story_body ) 16 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => story_body )
17 17
18 - err.app.issue_tracker.create_issue(err)  
19 - err.reload 18 + problem.app.issue_tracker.create_issue(problem)
  19 + problem.reload
20 20
21 requested = have_requested(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories") 21 requested = have_requested(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories")
22 WebMock.should requested.with(:headers => {'X-Trackertoken' => tracker.api_token}) 22 WebMock.should requested.with(:headers => {'X-Trackertoken' => tracker.api_token})
23 WebMock.should requested.with(:body => /See this exception on Errbit/) 23 WebMock.should requested.with(:body => /See this exception on Errbit/)
24 - WebMock.should requested.with(:body => /<name>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/name>/) 24 + WebMock.should requested.with(:body => /<name>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/name>/)
25 WebMock.should requested.with(:body => /<description>.+<\/description>/m) 25 WebMock.should requested.with(:body => /<description>.+<\/description>/m)
26 26
27 - err.issue_link.should == @issue_link 27 + problem.issue_link.should == @issue_link
28 end 28 end
29 end 29 end
30 30
spec/models/issue_trackers/redmine_tracker_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe RedmineTracker do 3 describe RedmineTracker do
4 - it "should create an issue on Redmine with err params, and set issue link for err" do 4 + it "should create an issue on Redmine with problem params, and set issue link for problem" do
5 notice = Factory(:notice) 5 notice = Factory(:notice)
6 - tracker = Factory(:redmine_tracker, :app => notice.err.app, :project_id => 10)  
7 - err = notice.err 6 + tracker = Factory(:redmine_tracker, :app => notice.app, :project_id => 10)
  7 + problem = notice.problem
8 number = 5 8 number = 5
9 @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" 9 @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}"
10 body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" 10 body = "<issue><subject>my subject</subject><id>#{number}</id></issue>"
11 stub_request(:post, "#{tracker.account}/issues.xml"). 11 stub_request(:post, "#{tracker.account}/issues.xml").
12 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) 12 to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
13 13
14 - err.app.issue_tracker.create_issue(err)  
15 - err.reload 14 + problem.app.issue_tracker.create_issue(problem)
  15 + problem.reload
16 16
17 requested = have_requested(:post, "#{tracker.account}/issues.xml") 17 requested = have_requested(:post, "#{tracker.account}/issues.xml")
18 WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) 18 WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token})
19 WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) 19 WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/)
20 - WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/) 20 + WebMock.should requested.with(:body => /<subject>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/subject>/)
21 WebMock.should requested.with(:body => /<description>.+<\/description>/m) 21 WebMock.should requested.with(:body => /<description>.+<\/description>/m)
22 22
23 - err.issue_link.should == @issue_link.sub(/\.xml/, '') 23 + problem.issue_link.should == @issue_link.sub(/\.xml/, '')
24 end 24 end
25 25
26 it "should generate a url where a file with line number can be viewed" do 26 it "should generate a url where a file with line number can be viewed" do
spec/models/notice_spec.rb
@@ -2,6 +2,7 @@ require &#39;spec_helper&#39; @@ -2,6 +2,7 @@ require &#39;spec_helper&#39;
2 2
3 describe Notice do 3 describe Notice do
4 4
  5 +
5 context 'validations' do 6 context 'validations' do
6 it 'requires a backtrace' do 7 it 'requires a backtrace' do
7 notice = Factory.build(:notice, :backtrace => nil) 8 notice = Factory.build(:notice, :backtrace => nil)
@@ -22,6 +23,7 @@ describe Notice do @@ -22,6 +23,7 @@ describe Notice do
22 end 23 end
23 end 24 end
24 25
  26 +
25 context '.in_app_backtrace_line?' do 27 context '.in_app_backtrace_line?' do
26 let(:backtrace) do [{ 28 let(:backtrace) do [{
27 'number' => rand(999), 29 'number' => rand(999),
@@ -51,93 +53,6 @@ describe Notice do @@ -51,93 +53,6 @@ describe Notice do
51 end 53 end
52 end 54 end
53 55
54 - context '#from_xml' do  
55 - before do  
56 - @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read  
57 - @app = Factory(:app, :api_key => 'APIKEY')  
58 - Digest::MD5.stub(:hexdigest).and_return('fingerprintdigest')  
59 - end  
60 -  
61 - it 'finds the correct app' do  
62 - @notice = Notice.from_xml(@xml)  
63 - @notice.err.app.should == @app  
64 - end  
65 -  
66 - it 'finds the correct err for the notice' do  
67 - Err.should_receive(:for).with({  
68 - :app => @app,  
69 - :klass => 'HoptoadTestingException',  
70 - :component => 'application',  
71 - :action => 'verify',  
72 - :environment => 'development',  
73 - :fingerprint => 'fingerprintdigest'  
74 - }).and_return(err = Factory(:err))  
75 - err.notices.stub(:create!)  
76 - @notice = Notice.from_xml(@xml)  
77 - end  
78 -  
79 - it 'marks the err as unresolve if it was previously resolved' do  
80 - Err.should_receive(:for).with({  
81 - :app => @app,  
82 - :klass => 'HoptoadTestingException',  
83 - :component => 'application',  
84 - :action => 'verify',  
85 - :environment => 'development',  
86 - :fingerprint => 'fingerprintdigest'  
87 - }).and_return(err = Factory(:err, :resolved => true))  
88 - err.should be_resolved  
89 - @notice = Notice.from_xml(@xml)  
90 - @notice.err.should == err  
91 - @notice.err.should_not be_resolved  
92 - end  
93 -  
94 - it 'should create a new notice' do  
95 - @notice = Notice.from_xml(@xml)  
96 - @notice.should be_persisted  
97 - end  
98 -  
99 - it 'assigns an err to the notice' do  
100 - @notice = Notice.from_xml(@xml)  
101 - @notice.err.should be_a(Err)  
102 - end  
103 -  
104 - it 'captures the err message' do  
105 - @notice = Notice.from_xml(@xml)  
106 - @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'  
107 - end  
108 -  
109 - it 'captures the backtrace' do  
110 - @notice = Notice.from_xml(@xml)  
111 - @notice.backtrace.size.should == 73  
112 - @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake'  
113 - end  
114 -  
115 - it 'captures the server_environment' do  
116 - @notice = Notice.from_xml(@xml)  
117 - @notice.server_environment['environment-name'].should == 'development'  
118 - end  
119 -  
120 - it 'captures the request' do  
121 - @notice = Notice.from_xml(@xml)  
122 - @notice.request['url'].should == 'http://example.org/verify'  
123 - @notice.request['params']['controller'].should == 'application'  
124 - end  
125 -  
126 - it 'captures the notifier' do  
127 - @notice = Notice.from_xml(@xml)  
128 - @notice.notifier['name'].should == 'Hoptoad Notifier'  
129 - end  
130 -  
131 - it "should handle params without 'request' section" do  
132 - @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read  
133 - lambda { Notice.from_xml(@xml) }.should_not raise_error  
134 - end  
135 -  
136 - it "should raise ApiVersionError" do  
137 - @xml = Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice_with_wrong_version.xml').read  
138 - expect { Notice.from_xml(@xml) }.to raise_error(Hoptoad::V2::ApiVersionError)  
139 - end  
140 - end  
141 56
142 describe "key sanitization" do 57 describe "key sanitization" do
143 before do 58 before do
@@ -153,6 +68,7 @@ describe Notice do @@ -153,6 +68,7 @@ describe Notice do
153 end 68 end
154 end 69 end
155 70
  71 +
156 describe "user agent" do 72 describe "user agent" do
157 it "should be parsed and human-readable" do 73 it "should be parsed and human-readable" do
158 notice = Factory.build(:notice, :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'}}) 74 notice = Factory.build(:notice, :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'}})
@@ -166,13 +82,14 @@ describe Notice do @@ -166,13 +82,14 @@ describe Notice do
166 end 82 end
167 end 83 end
168 84
  85 +
169 describe "email notifications (configured individually for each app)" do 86 describe "email notifications (configured individually for each app)" do
170 custom_thresholds = [2, 4, 8, 16, 32, 64] 87 custom_thresholds = [2, 4, 8, 16, 32, 64]
171 88
172 before do 89 before do
173 Errbit::Config.per_app_email_at_notices = true 90 Errbit::Config.per_app_email_at_notices = true
174 @app = Factory(:app_with_watcher, :email_at_notices => custom_thresholds) 91 @app = Factory(:app_with_watcher, :email_at_notices => custom_thresholds)
175 - @err = Factory(:err, :app => @app) 92 + @err = Factory(:err, :problem => Factory(:problem, :app => @app))
176 end 93 end
177 94
178 after do 95 after do
@@ -181,7 +98,7 @@ describe Notice do @@ -181,7 +98,7 @@ describe Notice do
181 98
182 custom_thresholds.each do |threshold| 99 custom_thresholds.each do |threshold|
183 it "sends an email notification after #{threshold} notice(s)" do 100 it "sends an email notification after #{threshold} notice(s)" do
184 - @err.notices.stub(:count).and_return(threshold) 101 + @err.problem.stub(:notices_count).and_return(threshold)
185 Mailer.should_receive(:err_notification). 102 Mailer.should_receive(:err_notification).
186 and_return(mock('email', :deliver => true)) 103 and_return(mock('email', :deliver => true))
187 Factory(:notice, :err => @err) 104 Factory(:notice, :err => @err)
@@ -189,5 +106,6 @@ describe Notice do @@ -189,5 +106,6 @@ describe Notice do
189 end 106 end
190 end 107 end
191 108
  109 +
192 end 110 end
193 111
spec/models/problem_spec.rb 0 → 100644
@@ -0,0 +1,162 @@ @@ -0,0 +1,162 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Problem do
  4 +
  5 +
  6 + context '#last_notice_at' do
  7 + it "returns the created_at timestamp of the latest notice" do
  8 + err = Factory(:err)
  9 + problem = err.problem
  10 + problem.should_not be_nil
  11 +
  12 + problem.last_notice_at.should be_nil
  13 +
  14 + notice1 = Factory(:notice, :err => err)
  15 + problem.last_notice_at.should == notice1.created_at
  16 +
  17 + notice2 = Factory(:notice, :err => err)
  18 + problem.last_notice_at.should == notice2.created_at
  19 + end
  20 + end
  21 +
  22 +
  23 + context '#message' do
  24 + it "adding a notice caches its message" do
  25 + err = Factory(:err)
  26 + problem = err.problem
  27 + lambda {
  28 + Factory(:notice, :err => err, :message => 'ERR 1')
  29 + }.should change(problem, :message).from(nil).to('ERR 1')
  30 + end
  31 + end
  32 +
  33 +
  34 + context 'being created' do
  35 + context 'when the app has err notifications set to false' do
  36 + it 'should not send an email notification' do
  37 + app = Factory(:app_with_watcher, :notify_on_errs => false)
  38 + Mailer.should_not_receive(:err_notification)
  39 + Factory(:problem, :app => app)
  40 + end
  41 + end
  42 + end
  43 +
  44 +
  45 + context "#resolved?" do
  46 + it "should start out as unresolved" do
  47 + problem = Problem.new
  48 + problem.should_not be_resolved
  49 + problem.should be_unresolved
  50 + end
  51 +
  52 + it "should be able to be resolved" do
  53 + problem = Factory(:problem)
  54 + problem.should_not be_resolved
  55 + problem.resolve!
  56 + problem.reload.should be_resolved
  57 + end
  58 + end
  59 +
  60 +
  61 + context "resolve!" do
  62 + it "marks the problem as resolved" do
  63 + problem = Factory(:problem)
  64 + problem.should_not be_resolved
  65 + problem.resolve!
  66 + problem.should be_resolved
  67 + end
  68 +
  69 + it "should throw an err if it's not successful" do
  70 + problem = Factory(:problem)
  71 + problem.should_not be_resolved
  72 + problem.stub!(:valid?).and_return(false)
  73 + problem.should_not be_valid
  74 + lambda {
  75 + problem.resolve!
  76 + }.should raise_error(Mongoid::Errors::Validations)
  77 + end
  78 + end
  79 +
  80 +
  81 + context ".merge!" do
  82 + it "collects the Errs from several problems into one and deletes the other problems" do
  83 + problem1 = Factory(:err).problem
  84 + problem2 = Factory(:err).problem
  85 + problem1.errs.length.should == 1
  86 + problem2.errs.length.should == 1
  87 +
  88 + lambda {
  89 + merged_problem = Problem.merge!(problem1, problem2)
  90 + merged_problem.reload.errs.length.should == 2
  91 + }.should change(Problem, :count).by(-1)
  92 + end
  93 + end
  94 +
  95 +
  96 + context "#unmerge!" do
  97 + it "creates a separate problem for each err" do
  98 + problem1 = Factory(:notice).problem
  99 + problem2 = Factory(:notice).problem
  100 + merged_problem = Problem.merge!(problem1, problem2)
  101 + merged_problem.errs.length.should == 2
  102 +
  103 + lambda {
  104 + problems = merged_problem.unmerge!
  105 + problems.length.should == 2
  106 + merged_problem.errs(true).length.should == 1
  107 + }.should change(Problem, :count).by(1)
  108 + end
  109 + end
  110 +
  111 +
  112 + context "Scopes" do
  113 + context "resolved" do
  114 + it 'only finds resolved Problems' do
  115 + resolved = Factory(:problem, :resolved => true)
  116 + unresolved = Factory(:problem, :resolved => false)
  117 + Problem.resolved.all.should include(resolved)
  118 + Problem.resolved.all.should_not include(unresolved)
  119 + end
  120 + end
  121 +
  122 + context "unresolved" do
  123 + it 'only finds unresolved Problems' do
  124 + resolved = Factory(:problem, :resolved => true)
  125 + unresolved = Factory(:problem, :resolved => false)
  126 + Problem.unresolved.all.should_not include(resolved)
  127 + Problem.unresolved.all.should include(unresolved)
  128 + end
  129 + end
  130 + end
  131 +
  132 +
  133 + context "notice counter cache" do
  134 +
  135 + before do
  136 + @app = Factory(:app)
  137 + @problem = Factory(:problem, :app => @app)
  138 + @err = Factory(:err, :problem => @problem)
  139 + end
  140 +
  141 + it "#notices_count returns 0 by default" do
  142 + @problem.notices_count.should == 0
  143 + end
  144 +
  145 + it "adding a notice increases #notices_count by 1" do
  146 + lambda {
  147 + Factory(:notice, :err => @err, :message => 'ERR 1')
  148 + }.should change(@problem, :notices_count).from(0).to(1)
  149 + end
  150 +
  151 + it "removing a notice decreases #notices_count by 1" do
  152 + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')
  153 + lambda {
  154 + @err.notices.first.destroy
  155 + @problem.reload
  156 + }.should change(@problem, :notices_count).from(1).to(0)
  157 + end
  158 + end
  159 +
  160 +
  161 +end
  162 +
spec/models/user_spec.rb
@@ -49,3 +49,4 @@ describe User do @@ -49,3 +49,4 @@ describe User do
49 end 49 end
50 50
51 end 51 end
  52 +
spec/models/watcher_spec.rb
@@ -34,3 +34,4 @@ describe Watcher do @@ -34,3 +34,4 @@ describe Watcher do
34 end 34 end
35 35
36 end 36 end
  37 +
spec/spec_helper.rb
@@ -23,4 +23,4 @@ RSpec.configure do |config| @@ -23,4 +23,4 @@ RSpec.configure do |config|
23 DatabaseCleaner.clean 23 DatabaseCleaner.clean
24 end 24 end
25 config.include WebMock::API 25 config.include WebMock::API
26 -end  
27 \ No newline at end of file 26 \ No newline at end of file
  27 +end
spec/support/macros.rb
@@ -55,4 +55,4 @@ def it_requires_admin_privileges(options = {}) @@ -55,4 +55,4 @@ def it_requires_admin_privileges(options = {})
55 end 55 end
56 end 56 end
57 end 57 end
58 -end  
59 \ No newline at end of file 58 \ No newline at end of file
  59 +end
spec/views/errs/show.html.haml_spec.rb
@@ -3,11 +3,12 @@ require &#39;spec_helper&#39; @@ -3,11 +3,12 @@ require &#39;spec_helper&#39;
3 describe "errs/show.html.haml" do 3 describe "errs/show.html.haml" do
4 before do 4 before do
5 err = Factory(:err) 5 err = Factory(:err)
  6 + problem = err.problem
6 comment = Factory(:comment) 7 comment = Factory(:comment)
7 - assign :err, err 8 + assign :problem, problem
8 assign :comment, comment 9 assign :comment, comment
9 - assign :app, err.app  
10 - assign :notices, err.notices.ordered.paginate(:page => 1, :per_page => 1) 10 + assign :app, problem.app
  11 + assign :notices, err.notices.paginate(:page => 1, :per_page => 1)
11 assign :notice, err.notices.first 12 assign :notice, err.notices.first
12 end 13 end
13 14
@@ -44,9 +45,9 @@ describe &quot;errs/show.html.haml&quot; do @@ -44,9 +45,9 @@ describe &quot;errs/show.html.haml&quot; do
44 end 45 end
45 46
46 it 'should display comments and new comment form when no issue tracker' do 47 it 'should display comments and new comment form when no issue tracker' do
47 - err = Factory(:err_with_comments)  
48 - assign :err, err  
49 - assign :app, err.app 48 + problem = Factory(:problem_with_comments)
  49 + assign :problem, problem
  50 + assign :app, problem.app
50 render 51 render
51 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments]) 52 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
52 comments_section.should =~ /Test comment/ 53 comments_section.should =~ /Test comment/
@@ -54,20 +55,22 @@ describe &quot;errs/show.html.haml&quot; do @@ -54,20 +55,22 @@ describe &quot;errs/show.html.haml&quot; do
54 end 55 end
55 56
56 context "with issue tracker" do 57 context "with issue tracker" do
57 - def with_issue_tracker(err)  
58 - err.app.issue_tracker = PivotalLabsTracker.new :api_token => "token token token", :project_id => "1234"  
59 - assign :err, err  
60 - assign :app, err.app 58 + def with_issue_tracker(problem)
  59 + problem.app.issue_tracker = PivotalLabsTracker.new :api_token => "token token token", :project_id => "1234"
  60 + assign :problem, problem
  61 + assign :app, problem.app
61 end 62 end
62 63
63 it 'should not display the comments section' do 64 it 'should not display the comments section' do
64 - with_issue_tracker(Factory(:err)) 65 + problem = Factory(:problem)
  66 + with_issue_tracker(problem)
65 render 67 render
66 view.instance_variable_get(:@_content_for)[:comments].should be_blank 68 view.instance_variable_get(:@_content_for)[:comments].should be_blank
67 end 69 end
68 70
69 it 'should display existing comments' do 71 it 'should display existing comments' do
70 - with_issue_tracker(Factory(:err_with_comments)) 72 + problem = Factory(:problem_with_comments)
  73 + with_issue_tracker(problem)
71 render 74 render
72 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments]) 75 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
73 comments_section.should =~ /Test comment/ 76 comments_section.should =~ /Test comment/