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
.gitignore
... ... @@ -4,6 +4,7 @@ log/*.log
4 4 tmp/**/*
5 5 config/config.yml
6 6 config/deploy.rb
  7 +config/deploy
7 8 config/mongoid.yml
8 9 .rvmrc
9 10 *~
... ...
Gemfile
... ... @@ -24,7 +24,7 @@ platform :ruby do
24 24 end
25 25  
26 26 group :development, :test do
27   - gem 'rspec-rails', '~> 2.5'
  27 + gem 'rspec-rails', '~> 2.6'
28 28 gem 'webmock', :require => false
29 29 gem 'factory_girl_rails'
30 30 unless ENV['TRAVIS']
... ... @@ -34,7 +34,7 @@ group :development, :test do
34 34 end
35 35  
36 36 group :test do
37   - gem 'rspec', '~> 2.5'
  37 + gem 'rspec', '~> 2.6'
38 38 gem 'database_cleaner', '~> 0.6.0'
39 39 gem 'email_spec'
40 40 end
... ...
Gemfile.lock
... ... @@ -57,7 +57,7 @@ GEM
57 57 bcrypt-ruby (~> 2.1.2)
58 58 orm_adapter (~> 0.0.3)
59 59 warden (~> 1.0.3)
60   - diff-lcs (1.1.2)
  60 + diff-lcs (1.1.3)
61 61 email_spec (1.1.1)
62 62 rspec (~> 2.0)
63 63 erubis (2.6.6)
... ... @@ -158,19 +158,19 @@ GEM
158 158 responders (0.6.4)
159 159 rest-client (1.5.1)
160 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 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 170 actionpack (~> 3.0)
171 171 activesupport (~> 3.0)
172 172 railties (~> 3.0)
173   - rspec (~> 2.5.0)
  173 + rspec (~> 2.6.0)
174 174 ruby-debug (0.10.4)
175 175 columnize (>= 0.1)
176 176 ruby-debug-base (~> 0.10.4.0)
... ... @@ -232,8 +232,8 @@ DEPENDENCIES
232 232 pivotal-tracker
233 233 rails (= 3.0.10)
234 234 redmine_client!
235   - rspec (~> 2.5)
236   - rspec-rails (~> 2.5)
  235 + rspec (~> 2.6)
  236 + rspec-rails (~> 2.6)
237 237 ruby-debug
238 238 ruby-debug19
239 239 ruby-fogbugz
... ...
app/controllers/application_controller.rb
... ... @@ -10,11 +10,20 @@ class ApplicationController < ActionController::Base
10 10 (location == root_path && App.count == 1) ? app_path(App.first) : location
11 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 28 end
20 29  
... ...
app/controllers/apps_controller.rb
1 1 class AppsController < InheritedResources::Base
2   -
3 2 before_filter :require_admin!, :except => [:index, :show]
4 3 before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
5 4 respond_to :html
6 5  
  6 +
7 7 def show
8 8 respond_to do |format|
9 9 format.html do
10 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 17 @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
17 18 end
18 19 format.atom do
19   - @errs = resource.errs.unresolved.ordered
  20 + @problems = resource.problems.unresolved.ordered
20 21 end
21 22 end
22 23 end
... ... @@ -49,7 +50,7 @@ class AppsController &lt; InheritedResources::Base
49 50 # Caches the unresolved err counts while performing the sort.
50 51 @unresolved_counts = {}
51 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 54 @unresolved_counts[b.id] <=> @unresolved_counts[a.id]
54 55 }
55 56 end
... ...
app/controllers/deploys_controller.rb
... ... @@ -18,9 +18,9 @@ class DeploysController &lt; ApplicationController
18 18  
19 19 @deploys = app.deploys.order_by(:created_at.desc).paginate(:page => params[:page], :per_page => 10)
20 20 end
21   -
  21 +
22 22 private
23   -
  23 +
24 24 def default_deploy
25 25 if params[:deploy]
26 26 {
... ... @@ -32,7 +32,7 @@ class DeploysController &lt; ApplicationController
32 32 }
33 33 end
34 34 end
35   -
  35 +
36 36 # handle Heroku's HTTP post deployhook format
37 37 def heroku_deploy
38 38 {
... ... @@ -42,5 +42,6 @@ class DeploysController &lt; ApplicationController
42 42 :revision => params[:head],
43 43 }
44 44 end
45   -
  45 +
46 46 end
  47 +
... ...
app/controllers/errs_controller.rb
1 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 10 def index
7 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 14 respond_to do |format|
10 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 17 end
13 18 format.atom
14 19 end
15 20 end
16 21  
  22 +
17 23 def all
18 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 27 end
21 28  
  29 +
22 30 def show
23   - page = (params[:notice] || @err.notices_count)
  31 + page = (params[:notice] || @problem.notices_count)
24 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 34 @notice = @notices.first
27 35 @comment = Comment.new
28 36 end
29 37  
  38 +
30 39 def create_issue
31 40 set_tracker_params
32 41  
33 42 if @app.issue_tracker
34   - @app.issue_tracker.create_issue @err
  43 + @app.issue_tracker.create_issue @problem
35 44 else
36 45 flash[:error] = "This app has no issue tracker setup."
37 46 end
38   - redirect_to app_err_path(@app, @err)
  47 + redirect_to app_err_path(@app, @problem)
39 48 rescue ActiveResource::ConnectionError => e
40 49 Rails.logger.error e.to_s
41 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 52 end
44 53  
  54 +
45 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 58 end
49 59  
  60 +
50 61 def resolve
51 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 66 flash[:success] = 'Great news everyone! The err has been resolved.'
57   -
58 67 redirect_to :back
59 68 rescue ActionController::RedirectBackError
60 69 redirect_to app_path(@app)
... ... @@ -64,15 +73,16 @@ class ErrsController &lt; ApplicationController
64 73 def create_comment
65 74 @comment = Comment.new(params[:comment].merge(:user_id => current_user.id))
66 75 if @comment.valid?
67   - @err.comments << @comment
68   - @err.save
  76 + @problem.comments << @comment
  77 + @problem.save
69 78 flash[:success] = "Comment saved!"
70 79 else
71 80 flash[:error] = "I'm sorry, your comment was blank! Try again?"
72 81 end
73   - redirect_to app_err_path(@app, @err)
  82 + redirect_to app_err_path(@app, @problem)
74 83 end
75 84  
  85 +
76 86 def destroy_comment
77 87 @comment = Comment.find(params[:comment_id])
78 88 if @comment.destroy
... ... @@ -80,29 +90,86 @@ class ErrsController &lt; ApplicationController
80 90 else
81 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 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 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 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 170 end
  171 + end
  172 +
106 173  
107 174 end
108 175  
... ...
app/controllers/notices_controller.rb
... ... @@ -5,8 +5,9 @@ class NoticesController &lt; ApplicationController
5 5  
6 6 def create
7 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 9 respond_with @notice
10 10 end
11 11  
12 12 end
  13 +
... ...
app/controllers/users_controller.rb
... ... @@ -71,3 +71,4 @@ class UsersController &lt; ApplicationController
71 71 end
72 72  
73 73 end
  74 +
... ...
app/helpers/errs_helper.rb
1 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 4 end
5 5  
6 6 def err_confirm
... ...
app/helpers/form_helper.rb
... ... @@ -15,4 +15,4 @@ module FormHelper
15 15 (builder.object_name + field).gsub(/[\[\]]/,'_').squeeze('_')
16 16 end
17 17  
18   -end
19 18 \ No newline at end of file
  19 +end
... ...
app/helpers/hash_helper.rb
... ... @@ -17,4 +17,4 @@ module HashHelper
17 17 pretty += "\n#{' '*nesting*tab_size}}"
18 18 end
19 19  
20   -end
21 20 \ No newline at end of file
  21 +end
... ...
app/helpers/navigation_helper.rb
... ... @@ -34,4 +34,4 @@ module NavigationHelper
34 34 active
35 35 end
36 36  
37   -end
38 37 \ No newline at end of file
  38 +end
... ...
app/mailers/mailer.rb
... ... @@ -7,10 +7,10 @@ class Mailer &lt; ActionMailer::Base
7 7  
8 8 def err_notification(notice)
9 9 @notice = notice
10   - @app = notice.err.app
  10 + @app = notice.app
11 11  
12 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 14 end
15 15  
16 16 def deploy_notification(deploy)
... ...
app/models/app.rb
... ... @@ -13,6 +13,7 @@ class App
13 13  
14 14 # Some legacy apps may have string as key instead of BSON::ObjectID
15 15 identity :type => String
  16 +
16 17 # There seems to be a Mongoid bug making it impossible to use String identity with references_many feature:
17 18 # https://github.com/mongoid/mongoid/issues/703
18 19 # Using 32 character string as a workaround.
... ... @@ -23,7 +24,7 @@ class App
23 24 embeds_many :watchers
24 25 embeds_many :deploys
25 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 29 before_validation :generate_api_key, :on => :create
29 30 before_save :normalize_github_url
... ... @@ -39,6 +40,50 @@ class App
39 40 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
40 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 87 def self.find_by_id!(app_id)
43 88 find app_id
44 89 end
... ... @@ -51,6 +96,7 @@ class App
51 96 deploys.last && deploys.last.created_at
52 97 end
53 98  
  99 +
54 100 # Legacy apps don't have notify_on_errs and notify_on_deploys params
55 101 def notify_on_errs
56 102 !(self[:notify_on_errs] == false)
... ... @@ -62,6 +108,7 @@ class App
62 108 end
63 109 alias :notify_on_deploys? :notify_on_deploys
64 110  
  111 +
65 112 def github_url?
66 113 self.github_url.present?
67 114 end
... ...
app/models/deploy.rb
... ... @@ -22,7 +22,7 @@ class Deploy
22 22 end
23 23  
24 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 26 end
27 27  
28 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 5 class Err
2 6 include Mongoid::Document
3 7 include Mongoid::Timestamps
... ... @@ -7,53 +11,18 @@ class Err
7 11 field :action
8 12 field :environment
9 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 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 27 end
59 28  
... ...
app/models/error_report.rb 0 → 100644
... ... @@ -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 20 # Subclasses are responsible for overwriting this method.
21 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 25 end
26 26  
27 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 22 end
23 23 end
24 24  
25   - def create_issue(err)
  25 + def create_issue(problem)
26 26 fogbugz = Fogbugz::Interface.new(:email => username, :password => password, :uri => "https://#{account}.fogbugz.com")
27 27 fogbugz.authenticate
28 28  
29 29 issue = {}
30   - issue['sTitle'] = issue_title err
  30 + issue['sTitle'] = issue_title problem
31 31 issue['sArea'] = project_id
32 32 issue['sEvent'] = body_template.result(binding)
33 33 issue['sTags'] = ['errbit'].join(',')
34 34 issue['cols'] = ['ixBug'].join(',')
35 35  
36 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 38 end
39 39  
40 40 def body_template
... ...
app/models/issue_trackers/lighthouse_tracker.rb
... ... @@ -18,20 +18,20 @@ class IssueTrackers::LighthouseTracker &lt; IssueTracker
18 18 end
19 19 end
20 20  
21   - def create_issue(err)
  21 + def create_issue(problem)
22 22 Lighthouse.account = account
23 23 Lighthouse.token = api_token
24 24 # updating lighthouse account
25 25 Lighthouse::Ticket.site
26 26  
27 27 ticket = Lighthouse::Ticket.new(:project_id => project_id)
28   - ticket.title = issue_title err
  28 + ticket.title = issue_title problem
29 29  
30 30 ticket.body = body_template.result(binding)
31 31  
32 32 ticket.tags << "errbit"
33 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 35 end
36 36  
37 37 def body_template
... ...
app/models/issue_trackers/mingle_tracker.rb
... ... @@ -27,21 +27,21 @@ class IssueTrackers::MingleTracker &lt; IssueTracker
27 27 end
28 28 end
29 29  
30   - def create_issue(err)
  30 + def create_issue(problem)
31 31 properties = ticket_properties_hash
32 32 basic_auth = account.gsub(/https?:\/\//, "https://#{username}:#{password}@")
33 33 Mingle.set_site "#{basic_auth}/api/v1/projects/#{project_id}/"
34 34  
35 35 card = Mingle::Card.new
36 36 card.card_type_name = properties.delete("card_type")
37   - card.name = issue_title(err)
  37 + card.name = issue_title(problem)
38 38 card.description = body_template.result(binding)
39 39 properties.each do |property, value|
40 40 card.send("cp_#{property}=", value)
41 41 end
42 42  
43 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 45 end
46 46  
47 47 def body_template
... ...
app/models/issue_trackers/pivotal_labs_tracker.rb
... ... @@ -13,12 +13,12 @@ class IssueTrackers::PivotalLabsTracker &lt; IssueTracker
13 13 end
14 14 end
15 15  
16   - def create_issue(err)
  16 + def create_issue(problem)
17 17 PivotalTracker::Client.token = api_token
18 18 PivotalTracker::Client.use_ssl = true
19 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 22 end
23 23  
24 24 def body_template
... ...
app/models/issue_trackers/redmine_tracker.rb
... ... @@ -25,7 +25,7 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
25 25 end
26 26 end
27 27  
28   - def create_issue(err)
  28 + def create_issue(problem)
29 29 token = api_token
30 30 acc = account
31 31 RedmineClient::Base.configure do
... ... @@ -33,10 +33,10 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
33 33 self.site = acc
34 34 end
35 35 issue = RedmineClient::Issue.new(:project_id => project_id)
36   - issue.subject = issue_title err
  36 + issue.subject = issue_title problem
37 37 issue.description = body_template.result(binding)
38 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 40 end
41 41  
42 42 def url_to_file(file_path, line_number = nil)
... ...
app/models/notice.rb
... ... @@ -10,53 +10,47 @@ class Notice
10 10 field :server_environment, :type => Hash
11 11 field :request, :type => Hash
12 12 field :notifier, :type => Hash
  13 + field :klass
13 14  
14 15 belongs_to :err
15 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 20 after_create :deliver_notification, :if => :should_notify?
19   - before_create :increase_counter_cache, :cache_message
20 21 before_save :sanitize
21 22 before_destroy :decrease_counter_cache
22 23  
23 24 validates_presence_of :backtrace, :server_environment, :notifier
24 25  
25 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 31 def user_agent
56 32 agent_string = env_vars['HTTP_USER_AGENT']
57 33 agent_string.blank? ? nil : UserAgent.parse(agent_string)
58 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 54 def self.in_app_backtrace_line?(line)
61 55 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})
62 56 end
... ... @@ -81,10 +75,6 @@ class Notice
81 75 Mailer.err_notification(self).deliver
82 76 end
83 77  
84   - def cache_last_notice_at
85   - err.update_attributes(:last_notice_at => created_at)
86   - end
87   -
88 78 # Backtrace containing only files from the app itself (ignore gems)
89 79 def app_backtrace
90 80 backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") }
... ... @@ -93,20 +83,24 @@ class Notice
93 83 protected
94 84  
95 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 87 end
98 88  
99   -
100 89 def increase_counter_cache
101   - err.inc(:notices_count,1)
  90 + problem.inc(:notices_count, 1)
102 91 end
103 92  
104 93 def decrease_counter_cache
105   - err.inc(:notices_count,-1)
  94 + problem.inc(:notices_count, -1)
106 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 104 end
111 105  
112 106 def sanitize
... ... @@ -129,5 +123,6 @@ class Notice
129 123 end
130 124 end
131 125 end
  126 +
132 127 end
133 128  
... ...
app/models/problem.rb 0 → 100644
... ... @@ -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 41 end
42 42  
43 43 end
  44 +
... ...
app/views/apps/index.html.haml
... ... @@ -19,7 +19,7 @@
19 19 - else
20 20 n/a
21 21 %td.count
22   - - if app.errs.count > 0
  22 + - if app.problems.count > 0
23 23 - unresolved = @unresolved_counts[app.id]
24 24 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
25 25 - else
... ...
app/views/apps/show.html.haml
... ... @@ -4,7 +4,7 @@
4 4 = javascript_include_tag 'apps.show'
5 5 - content_for :meta do
6 6 %strong Errs Caught:
7   - = @app.errs.count
  7 + = @app.problems.count
8 8 %strong Deploy Count:
9 9 = @app.deploys.count
10 10 %strong API Key:
... ... @@ -82,9 +82,9 @@
82 82 - else
83 83 %h3 No deploys
84 84  
85   -- if @app.errs.count > 0
  85 +- if @app.problems.any?
86 86 %h3.clear Errs
87   - = render 'errs/table', :errs => @errs
  87 + = render 'errs/table', :errs => @problems
88 88 - else
89 89 %h3.clear No errs have been caught yet, make sure you setup your app
90 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 8 entry.author do |author|
9   - author.name "#{ err.app.name } [#{ err.environment }]"
  9 + author.name "#{ problem.app.name } [#{ problem.environment }]"
10 10 end
11 11 if notice
12 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 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 1 - content_for :title, 'All Errs'
2 2 - content_for :action_bar do
3 3 = link_to 'hide resolved', errs_path, :class => 'button'
4   -= render 'table', :errs => @errs
5 4 \ No newline at end of file
  5 += render 'table', :errs => @problems
6 6 \ No newline at end of file
... ...
app/views/errs/index.html.haml
... ... @@ -3,4 +3,4 @@
3 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 4 - content_for :action_bar do
5 5 = link_to 'show resolved', all_errs_path, :class => 'button'
6   -= render 'table', :errs => @errs
7 6 \ No newline at end of file
  7 += render 'table', :errs => @problems
8 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 3 - content_for :meta do
4 4 %strong App:
5 5 = link_to @app.name, app_path(@app)
6 6 %strong Where:
7   - = @err.where
  7 + = @problem.where
8 8 %br
9 9 %strong Environment:
10   - = @err.environment
  10 + = @problem.environment
11 11 %strong Last Notice:
12   - = last_notice_at(@err).to_s(:micro)
  12 + = last_notice_at(@problem).to_s(:micro)
13 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 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 24 - content_for :comments do
25 25 %h3 Comments on this Err
26   - - @err.comments.each do |comment|
  26 + - @problem.comments.each do |comment|
27 27 .window
28 28 %table.comment
29 29 %tr
30 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 32 = time_ago_in_words(comment.created_at, true) << " ago by "
33 33 = link_to comment.user.email, user_path(comment.user)
34 34 %tr
35 35 %td= comment.body.gsub("\n", "<br>").html_safe
36 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 38 %p Add a comment
39 39 = comment_form.text_area :body, :style => "width: 420px; height: 80px;"
40 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 3 <%= notice.message %>
4 4  
5 5 Summary
6 6 - Where
7   - <%= notice.err.where %>
  7 + <%= notice.where %>
8 8  
9 9 - Occured
10 10 <%= notice.created_at.to_s(:micro) %>
11 11  
12 12 - Similar
13   - <%= (notice.err.notices_count - 1).to_s %>
  13 + <%= (notice.problem.notices_count - 1).to_s %>
14 14  
15 15 Params
16 16 <%= pretty_hash(notice.params) %>
... ...
app/views/issue_trackers/github_issues_body.txt.erb
... ... @@ -7,13 +7,13 @@
7 7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8 8 <% end %>
9 9 ### Where ###
10   -<%= notice.err.where %>
  10 +<%= notice.where %>
11 11  
12 12 ### Occured ###
13 13 <%= notice.created_at.to_s(:micro) %>
14 14  
15 15 ### Similar ###
16   -<%= (notice.err.notices_count - 1).to_s %>
  16 +<%= (notice.problem.notices_count - 1).to_s %>
17 17  
18 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 3 # <%= notice.message %> #
4 4 ## Summary ##
5 5 <% if notice.request['url'].present? %>
... ... @@ -7,13 +7,13 @@
7 7 [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
8 8 <% end %>
9 9 ### Where ###
10   - <%= notice.err.where %>
  10 + <%= notice.where %>
11 11  
12 12 ### Occured ###
13 13 <%= notice.created_at.to_s(:micro) %>
14 14  
15 15 ### Similar ###
16   - <%= (notice.err.notices_count - 1).to_s %>
  16 + <%= (notice.problem.notices_count - 1).to_s %>
17 17  
18 18 ## Params ##
19 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 3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
4   - Where: <%= notice.err.where %>
  4 + Where: <%= notice.where %>
5 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 8 Params:
9 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 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 6 h2. Summary
7 7 <% if notice.request['url'].present? %>
... ... @@ -11,7 +11,7 @@ h3. URL
11 11 <% end %>
12 12 h3. Where
13 13  
14   -<%= notice.err.where %>
  14 +<%= notice.where %>
15 15  
16 16 h3. Occurred
17 17  
... ... @@ -19,7 +19,7 @@ h3. Occurred
19 19  
20 20 h3. Similar
21 21  
22   -<%= (notice.err.notices_count - 1).to_s %>
  22 +<%= (notice.problem.notices_count - 1).to_s %>
23 23  
24 24 h2. Params
25 25  
... ...
app/views/mailer/err_notification.html.haml
... ... @@ -9,12 +9,12 @@
9 9 An err has just occurred in
10 10 = link_to(@app.name, app_url(@app), :class => "bold") << ","
11 11 on the
12   - %span.bold= @notice.err.environment
  12 + %span.bold= @notice.environment_name
13 13 environment.
14 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 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 18 %tr
19 19 %td.section
20 20 %table(cellpadding="0" cellspacing="0" border="0" align="left")
... ... @@ -23,10 +23,10 @@
23 23 %td.content(valign="top")
24 24 %div
25 25 %p.heading ERROR MESSAGE:
26   - %p= @notice.err.message
  26 + %p= @notice.message
27 27 %p.heading WHERE:
28 28 %p.monospace
29   - = @notice.err.where
  29 + = @notice.where
30 30 - if (app_backtrace = @notice.app_backtrace).any?
31 31 - app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line|
32 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 8 ERROR MESSAGE:
9 9  
10   -<%= raw(@notice.err.message) %>
  10 +<%= raw(@notice.message) %>
11 11  
12 12  
13 13 WHERE:
14 14  
15   -<%= @notice.err.where %>
  15 +<%= @notice.where %>
16 16  
17 17 <% @notice.app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| %>
18 18 <%= line %>
... ...
app/views/notices/_atom_entry.html.haml
... ... @@ -6,13 +6,13 @@
6 6 = link_to(notice.request['url'], notice.request['url'])
7 7 %p
8 8 %strong Where:
9   - = notice.err.where
  9 + = notice.where
10 10 %p
11 11 %strong Occured:
12 12 = notice.created_at.to_s(:micro)
13 13 %p
14 14 %strong Similar:
15   - = notice.err.notices_count - 1
  15 + = notice.problem.notices_count - 1
16 16  
17 17 %h3 Params
18 18 %p= pretty_hash(notice.params)
... ...
app/views/notices/_summary.html.haml
... ... @@ -9,19 +9,20 @@
9 9 %td.nowrap= link_to notice.request['url'], notice.request['url']
10 10 %tr
11 11 %th Where
12   - %td= notice.err.where
  12 + %td= notice.where
13 13 %tr
14 14 %th Occurred
15 15 %td= notice.created_at.to_s(:micro)
16 16 %tr
17 17 %th Similar
18   - %td= notice.err.notices.count - 1
  18 + %td= notice.problem.notices.count - 1
19 19 %tr
20 20 %th Browser
21   - %td= user_agent_graph(notice.err)
  21 + %td= user_agent_graph(notice.problem)
22 22 %tr
23 23 %th App Server
24 24 %td= notice.server_environment && notice.server_environment["hostname"]
25 25 %tr
26 26 %th Rel. Directory
27 27 %td= notice.server_environment && notice.server_environment["project-root"]
  28 +
... ...
autotest/discover.rb
1 1 Autotest.add_discovery { "rails" }
2 2 Autotest.add_discovery { "rspec2" }
  3 +
... ...
config/application.rb
... ... @@ -39,7 +39,7 @@ module Errbit
39 39 # config.i18n.default_locale = :de
40 40  
41 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 44 # > rails generate - config
45 45 config.generators do |g|
... ...
config/boot.rb
... ... @@ -11,3 +11,4 @@ rescue Bundler::GemNotFound =&gt; e
11 11 STDERR.puts "Try running `bundle install`."
12 12 exit!
13 13 end if File.exist?(gemfile)
  14 +
... ...
config/environment.rb
... ... @@ -3,3 +3,4 @@ require File.expand_path(&#39;../application&#39;, __FILE__)
3 3  
4 4 # Initialize the rails application
5 5 Errbit::Application.initialize!
  6 +
... ...
config/environments/production.rb
... ... @@ -50,3 +50,4 @@ Errbit::Application.configure do
50 50 # Send deprecation notices to registered listeners
51 51 config.active_support.deprecation = :notify
52 52 end
  53 +
... ...
config/environments/test.rb
... ... @@ -34,3 +34,4 @@ Errbit::Application.configure do
34 34 # Print deprecation notices to the stderr
35 35 config.active_support.deprecation = :stderr
36 36 end
  37 +
... ...
config/initializers/backtrace_silencers.rb
... ... @@ -5,3 +5,4 @@
5 5  
6 6 # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 7 # Rails.backtrace_cleaner.remove_silencers!
  8 +
... ...
config/initializers/inflections.rb
... ... @@ -8,3 +8,4 @@
8 8 # inflect.irregular 'person', 'people'
9 9 # inflect.uncountable %w( fish sheep )
10 10 # end
  11 +
... ...
config/initializers/inherited_resources.rb
1 1 InheritedResources.flash_keys = [:success, :error]
  2 +
... ...
config/initializers/mime_types.rb
... ... @@ -3,3 +3,4 @@
3 3 # Add new mime types for use in respond_to blocks:
4 4 # Mime::Type.register "text/richtext", :rtf
5 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 8 config.allow_dynamic_fields = false
9 9 end
10 10 end
  11 +
... ...
config/initializers/requirements.rb 0 → 100644
... ... @@ -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 6 # which shouldn't be used to store highly confidential information
7 7 # (create the session table with "rake db:sessions:create")
8 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 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 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 6 match '/notifier_api/v2/notices' => 'notices#create'
7 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 11 resources :users
12   - resources :errs, :only => [:index] do
  12 + resources :errs, :only => [:index] do
13 13 collection do
  14 + post :destroy_several
  15 + post :resolve_several
  16 + post :unresolve_several
  17 + post :merge_several
  18 + post :unmerge_several
14 19 get :all
15 20 end
16 21 end
... ... @@ -20,6 +25,7 @@ Errbit::Application.routes.draw do
20 25 resources :notices
21 26 member do
22 27 put :resolve
  28 + put :unresolve
23 29 post :create_issue
24 30 delete :unlink_issue
25 31 post :create_comment
... ...
db/migrate/20110422152027_move_notices_to_separate_collection.rb
... ... @@ -17,10 +17,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration
17 17 mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}})
18 18 end
19 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 21 end
22 22  
23 23 def self.down
24 24 end
25   -
26 25 end
  26 +
... ...
db/migrate/20110905134638_link_errs_to_problems.rb 0 → 100644
... ... @@ -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 @@
  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 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 22 end
45   - end
  23 + end
46 24 end
47 25  
... ...
lib/hoptoad/v2.rb 0 → 100644
... ... @@ -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 13 end
14 14 end
15 15 end
  16 +
... ...
lib/recurse.rb
... ... @@ -22,3 +22,4 @@ class Hash
22 22 end
23 23  
24 24 end
  25 +
... ...
lib/tasks/errbit/database.rake
1 1 namespace :errbit do
2 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 8 end
10   -
11   - desc "Updates Err#notices_count"
  9 +
  10 + desc "Updates Problem#notices_count"
12 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 15 end
17 16 end
18   -
  17 +
19 18 desc "Delete resolved errors from the database. (Useful for limited heroku databases)"
20 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 22 puts "=== Cleared #{count} resolved errors from the database." if count > 0
24 23 end
25 24 end
26 25 end
27   -
... ...
lib/tasks/errbit/demo.rake
1 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 4 task :demo => :environment do
4 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 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 67 end
  68 +
10 69 end
11   -
... ...
public/javascripts/application.js
1 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 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 @@
  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 27 \ No newline at end of file
... ...
public/stylesheets/application.css
... ... @@ -244,7 +244,10 @@ a.action { float: right; font-size: 0.9em;}
244 244 }
245 245  
246 246 /* Forms */
247   -form {
  247 +form#new_user,
  248 +form.edit_user,
  249 +form#new_app,
  250 +form.edit_app {
248 251 width: 620px;
249 252 }
250 253 form > div, form fieldset > div { margin: 1em 0;}
... ... @@ -289,7 +292,11 @@ form input[type=submit] {
289 292 font-size: 1.2em; line-height: 1em; text-transform: uppercase;
290 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 300 color: #666;
294 301 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
295 302 border-radius: 50px;
... ... @@ -477,6 +484,7 @@ pre {
477 484 }
478 485  
479 486 /* Buttons */
  487 +input[type="submit"].button,
480 488 a.button {
481 489 display: inline-block;
482 490 padding: 0 0.8em;
... ... @@ -492,6 +500,7 @@ a.button {
492 500 -webkit-box-shadow: inset 0px 0px 4px #FFF;
493 501 line-height: 30px;
494 502 }
  503 +input[type="submit"]:hover.button,
495 504 a:hover.button {
496 505 box-shadow: 0px 0px 4px #ccc;
497 506 -moz-box-shadow: 0px 0px 4px #ccc;
... ... @@ -508,6 +517,7 @@ a.button.active {
508 517 -webkit-box-shadow: inset 0 0 5px #999;
509 518 }
510 519  
  520 +
511 521 /* Tab Bar */
512 522 .tab-bar {
513 523 margin-bottom: 24px;
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -6,6 +6,7 @@ describe AppsController do
6 6 it_requires_authentication
7 7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete}
8 8  
  9 +
9 10 describe "GET /apps" do
10 11 context 'when logged in as an admin' do
11 12 it 'finds all apps' do
... ... @@ -38,8 +39,7 @@ describe AppsController do
38 39 @user = Factory(:admin)
39 40 sign_in @user
40 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 43 end
44 44  
45 45 it 'finds the app' do
... ... @@ -48,50 +48,51 @@ describe AppsController do
48 48 end
49 49  
50 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 52 lambda { get :show, :id => @app.id }.should_not raise_error
53 53 end
54 54  
55 55 it "should list atom feed successfully" do
56 56 get :show, :id => @app.id, :format => "atom"
57 57 response.should be_success
58   - response.body.should match(@err.message)
  58 + response.body.should match(@problem.message)
59 59 end
60 60  
61 61 context "pagination" do
62 62 before(:each) do
63   - 35.times { Factory :err, :app => @app }
  63 + 35.times { Factory(:err, :problem => Factory(:problem, :app => @app)) }
64 64 end
65 65  
66 66 it "should have default per_page value for user" do
67 67 get :show, :id => @app.id
68   - assigns(:errs).size.should == User::PER_PAGE
  68 + assigns(:problems).size.should == User::PER_PAGE
69 69 end
70 70  
71 71 it "should be able to override default per_page value" do
72 72 @user.update_attribute :per_page, 10
73 73 get :show, :id => @app.id
74   - assigns(:errs).size.should == 10
  74 + assigns(:problems).size.should == 10
75 75 end
76 76 end
77 77  
78 78 context 'with resolved errors' do
79 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 83 end
83 84  
84 85 context 'and no params' do
85   - it 'shows only unresolved errs' do
  86 + it 'shows only unresolved problems' do
86 87 get :show, :id => @app.id
87   - assigns(:errs).size.should == 1
  88 + assigns(:problems).size.should == 1
88 89 end
89 90 end
90 91  
91   - context 'and all_errs=true params' do
  92 + context 'and all_problems=true params' do
92 93 it 'shows all errors' do
93 94 get :show, :id => @app.id, :all_errs => true
94   - assigns(:errs).size.should == 2
  95 + assigns(:problems).size.should == 2
95 96 end
96 97 end
97 98 end
... ... @@ -100,42 +101,42 @@ describe AppsController do
100 101 before(:each) do
101 102 environments = ['production', 'test', 'development', 'staging']
102 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 105 end
105 106 end
106 107  
107 108 context 'no params' do
108 109 it 'shows errs for all environments' do
109 110 get :show, :id => @app.id
110   - assigns(:errs).size.should == 21
  111 + assigns(:problems).size.should == 21
111 112 end
112 113 end
113 114  
114 115 context 'environment production' do
115 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 119 end
119 120 end
120 121  
121 122 context 'environment staging' do
122 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 126 end
126 127 end
127 128  
128 129 context 'environment development' do
129 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 133 end
133 134 end
134 135  
135 136 context 'environment test' do
136 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 140 end
140 141 end
141 142 end
... ... @@ -345,5 +346,6 @@ describe AppsController do
345 346 end
346 347 end
347 348  
  349 +
348 350 end
349 351  
... ...
spec/controllers/errs_controller_spec.rb
... ... @@ -8,7 +8,8 @@ describe ErrsController do
8 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9 9  
10 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 14 describe "GET /errs" do
14 15 render_views
... ... @@ -16,20 +17,19 @@ describe ErrsController do
16 17 before(:each) do
17 18 @user = Factory(:admin)
18 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 21 end
22 22  
23 23 it "should successfully list errs" do
24 24 get :index
25 25 response.should be_success
26   - response.body.should match(@err.message)
  26 + response.body.should match(@problem.message)
27 27 end
28 28  
29 29 it "should list atom feed successfully" do
30 30 get :index, :format => "atom"
31 31 response.should be_success
32   - response.body.should match(@err.message)
  32 + response.body.should match(@problem.message)
33 33 end
34 34  
35 35 context "pagination" do
... ... @@ -39,13 +39,13 @@ describe ErrsController do
39 39  
40 40 it "should have default per_page value for user" do
41 41 get :index
42   - assigns(:errs).size.should == User::PER_PAGE
  42 + assigns(:problems).size.should == User::PER_PAGE
43 43 end
44 44  
45 45 it "should be able to override default per_page value" do
46 46 @user.update_attribute :per_page, 10
47 47 get :index
48   - assigns(:errs).size.should == 10
  48 + assigns(:problems).size.should == 10
49 49 end
50 50 end
51 51  
... ... @@ -53,42 +53,42 @@ describe ErrsController do
53 53 before(:each) do
54 54 environments = ['production', 'test', 'development', 'staging']
55 55 20.times do |i|
56   - Factory.create(:err, :environment => environments[i % environments.length])
  56 + Factory(:problem, :environment => environments[i % environments.length])
57 57 end
58 58 end
59 59  
60 60 context 'no params' do
61 61 it 'shows errs for all environments' do
62 62 get :index
63   - assigns(:errs).size.should == 21
  63 + assigns(:problems).size.should == 21
64 64 end
65 65 end
66 66  
67 67 context 'environment production' do
68 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 71 end
72 72 end
73 73  
74 74 context 'environment staging' do
75 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 78 end
79 79 end
80 80  
81 81 context 'environment development' do
82 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 85 end
86 86 end
87 87  
88 88 context 'environment test' do
89 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 92 end
93 93 end
94 94 end
... ... @@ -98,11 +98,11 @@ describe ErrsController do
98 98 it 'gets a paginated list of unresolved errs for the users apps' do
99 99 sign_in(user = Factory(:user))
100 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 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 106 end
107 107 end
108 108 end
... ... @@ -112,25 +112,25 @@ describe ErrsController do
112 112 it "gets a paginated list of all errs" do
113 113 sign_in Factory(:admin)
114 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 118 mock('proxy', :paginate => errs)
119 119 )
120 120 get :all
121   - assigns(:errs).should == errs
  121 + assigns(:problems).should == errs
122 122 end
123 123 end
124 124  
125 125 context 'when logged in as a user' do
126 126 it 'gets a paginated list of all errs for the users apps' do
127 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 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 134 end
135 135 end
136 136 end
... ... @@ -148,17 +148,17 @@ describe ErrsController do
148 148 end
149 149  
150 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 152 assigns(:app).should == app
153 153 end
154 154  
155 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 158 end
159 159  
160 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 162 response.should be_success
163 163 end
164 164  
... ... @@ -167,23 +167,23 @@ describe ErrsController do
167 167  
168 168 it "should not exist for err's app without issue tracker" do
169 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 172 response.body.should_not button_matcher
173 173 end
174 174  
175 175 it "should exist for err's app with issue tracker" do
176 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 180 response.body.should button_matcher
181 181 end
182 182  
183 183 it "should not exist for err with issue_link" do
184 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 188 response.body.should_not button_matcher
189 189 end
... ... @@ -196,17 +196,17 @@ describe ErrsController do
196 196 @unwatched_err = Factory(:err)
197 197 @watched_app = Factory(:app)
198 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 200 end
201 201  
202 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 205 end
206 206  
207 207 it 'raises a DocumentNotFound error if the user is not watching the app' do
208 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 210 }.should raise_error(Mongoid::Errors::DocumentNotFound)
211 211 end
212 212 end
... ... @@ -216,38 +216,38 @@ describe ErrsController do
216 216 before do
217 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 223 end
224 224  
225 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 231 end
232 232  
233 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 236 end
237 237  
238 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 240 request.flash[:success].should match(/Great news/)
241 241 end
242 242  
243 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 246 end
247 247  
248 248 it "should redirect back to errs page" do
249 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 251 response.should redirect_to(errs_path)
252 252 end
253 253 end
... ... @@ -262,8 +262,8 @@ describe ErrsController do
262 262 context "successful issue creation" do
263 263 context "lighthouseapp tracker" do
264 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 268 before(:each) do
269 269 number = 5
... ... @@ -272,25 +272,25 @@ describe ErrsController do
272 272 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
273 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 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 281 end
282 282 end
283 283 end
284 284  
285 285 context "absent issue tracker" do
286   - let(:err) { Factory :err }
  286 + let(:problem) { Factory :problem }
287 287  
288 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 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 294 end
295 295  
296 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 301 context "error during request to a tracker" do
302 302 context "lighthouseapp tracker" do
303 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 306 before(:each) do
307 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 310 end
311 311  
312 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 314 end
315 315  
316 316 it "should notify of connection error" do
... ... @@ -326,19 +326,19 @@ describe ErrsController do
326 326 end
327 327  
328 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 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 334 end
335 335  
336 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 338 end
339 339  
340 340 it "should clear issue link" do
341   - err.issue_link.should be_nil
  341 + err.problem.issue_link.should be_nil
342 342 end
343 343 end
344 344  
... ... @@ -346,12 +346,12 @@ describe ErrsController do
346 346 let(:err) { Factory :err }
347 347  
348 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 351 end
352 352  
353 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 355 end
356 356 end
357 357 end
... ... @@ -365,21 +365,21 @@ describe ErrsController do
365 365 end
366 366  
367 367 context "successful comment creation" do
368   - let(:err) { Factory(:err) }
  368 + let(:problem) { Factory(:problem) }
369 369 let(:user) { Factory(:user) }
370 370  
371 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 373 :comment => { :body => "One test comment", :user_id => user.id }
374   - err.reload
  374 + problem.reload
375 375 end
376 376  
377 377 it "should create the comment" do
378   - err.comments.size.should == 1
  378 + problem.comments.size.should == 1
379 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 383 end
384 384 end
385 385 end
... ... @@ -392,20 +392,85 @@ describe ErrsController do
392 392 end
393 393  
394 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 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 401 end
402 402  
403 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 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 474 end
410 475 end
411 476 end
... ...
spec/controllers/notices_controller_spec.rb
... ... @@ -7,20 +7,20 @@ describe NoticesController do
7 7 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
8 8 @app = Factory(:app_with_watcher)
9 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 12 request.env['Content-type'] = 'text/xml'
13 13 request.env['Accept'] = 'text/xml, application/xml'
14 14 end
15 15  
16 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 18 request.should_receive(:raw_post).and_return(@xml)
19 19 post :create
20 20 end
21 21  
22 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 24 get :create, {:data => @xml}
25 25 end
26 26  
... ... @@ -29,10 +29,11 @@ describe NoticesController do
29 29 post :create
30 30 email = ActionMailer::Base.deliveries.last
31 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 33 email.subject.should include("[#{@app.name}]")
34   - email.subject.should include("[#{@notice.err.environment}]")
  34 + email.subject.should include("[#{@notice.environment_name}]")
35 35 end
36 36 end
37 37  
38 38 end
  39 +
... ...
spec/controllers/users_controller_spec.rb
... ... @@ -215,3 +215,4 @@ describe UsersController do
215 215  
216 216 end
217 217 end
  218 +
... ...
spec/factories.rb
... ... @@ -3,3 +3,4 @@ Factory.sequence(:word) {|n| &quot;word#{n}&quot;}
3 3 Factory.sequence(:app_name) {|n| "App ##{n}"}
4 4 Factory.sequence(:email) {|n| "email#{n}@example.com"}
5 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 20 end
21 21  
22 22 Factory.define(:deploy) do |d|
23   - d.app {|p| p.association :app}
  23 + d.app {|p| p.association :app}
24 24 d.username 'clyde.frog'
25 25 d.repository 'git@github.com/errbit/errbit.git'
26 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 12 Factory.define :err do |e|
2   - e.app {|p| p.association :app }
  13 + e.problem {|p| p.association :problem}
3 14 e.klass 'FooError'
4 15 e.component 'foo'
5 16 e.action 'bar'
6 17 e.environment 'production'
7   - e.comments []
8 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 22 Factory.define :notice do |n|
15 23 n.err {|e| e.association :err}
16 24 n.message 'FooError: Too Much Bar'
17 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 28 n.notifier 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com'
20 29 end
21 30  
... ...
spec/factories/user_factories.rb
... ... @@ -7,4 +7,4 @@ end
7 7  
8 8 Factory.define :admin, :parent => :user do |a|
9 9 a.admin true
10   -end
11 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 @@
  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 76 \ No newline at end of file
... ...
spec/models/app_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe App do
4   -
5 4 context 'validations' do
6 5 it 'requires a name' do
7 6 app = Factory.build(:app, :name => nil)
... ... @@ -24,6 +23,7 @@ describe App do
24 23 end
25 24 end
26 25  
  26 +
27 27 context 'being created' do
28 28 it 'generates a new api-key' do
29 29 app = Factory.build(:app)
... ... @@ -99,6 +99,7 @@ describe App do
99 99 end
100 100 end
101 101  
  102 +
102 103 context "copying attributes from existing app" do
103 104 it "should only copy the necessary fields" do
104 105 @app, @copy_app = Factory(:app, :name => "app", :github_url => "url"),
... ... @@ -110,5 +111,127 @@ describe App do
110 111 @app.watchers.first.email.should == "copywatcher@example.com"
111 112 end
112 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 236 end
114 237  
... ...
spec/models/deploy_spec.rb
... ... @@ -26,20 +26,20 @@ describe Deploy do
26 26 context 'when the app has resolve_errs_on_deploy set to false' do
27 27 it 'should not resolve the apps errs' do
28 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 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 32 end
33 33 end
34 34  
35 35 context 'when the app has resolve_errs_on_deploy set to true' do
36 36 it 'should resolve the apps errs that were in the same environment' do
37 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 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 43 end
44 44 end
45 45  
... ...
spec/models/err_spec.rb
... ... @@ -16,157 +16,5 @@ describe Err do
16 16 end
17 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 19 end
172 20  
... ...
spec/models/issue_trackers/fogbugz_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 9 number = 123
10 10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}"
... ... @@ -14,10 +14,10 @@ describe FogbugzTracker do
14 14 http_mock.should_receive(:request).twice.and_return(response)
15 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 21 end
22 22 end
23 23  
... ...
spec/models/issue_trackers/github_issues_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 9 number = 5
10 10 @issue_link = "https://github.com/#{tracker.project_id}/issues/#{number}"
... ... @@ -27,15 +27,15 @@ EOF
27 27 stub_request(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}").
28 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 33 requested = have_requested(:post, "https://#{tracker.username}%2Ftoken:#{tracker.api_token}@github.com/api/v2/json/issues/open/#{tracker.project_id}")
34 34 WebMock.should requested.with(:headers => {'Content-Type' => 'application/x-www-form-urlencoded'})
35 35 WebMock.should requested.with(:body => /title=%5Bproduction%5D%5Bfoo%23bar%5D%20FooError%3A%20Too%20Much%20Bar/)
36 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 39 end
40 40 end
41 41  
... ...
spec/models/issue_trackers/lighthouse_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 9 number = 5
10 10 @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
... ... @@ -12,16 +12,16 @@ describe LighthouseTracker do
12 12 stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
13 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 18 requested = have_requested(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml")
19 19 WebMock.should requested.with(:headers => {'X-Lighthousetoken' => tracker.api_token})
20 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 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 25 end
26 26 end
27 27  
... ...
spec/models/issue_trackers/mingle_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 9 number = 5
10 10 @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/cards/#{number}.xml"
... ... @@ -13,8 +13,8 @@ describe MingleTracker do
13 13 stub_request(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml").
14 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 19 requested = have_requested(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml")
20 20 WebMock.should requested.with(:headers => {'Content-Type' => 'application/xml'})
... ... @@ -22,7 +22,7 @@ describe MingleTracker do
22 22 WebMock.should requested.with(:body => /See this exception on Errbit/)
23 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 26 end
27 27 end
28 28  
... ...
spec/models/issue_trackers/pivotal_labs_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 9 story_id = 5
10 10 @issue_link = "https://www.pivotaltracker.com/story/show/#{story_id}"
... ... @@ -15,16 +15,16 @@ describe PivotalLabsTracker do
15 15 stub_request(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories").
16 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 21 requested = have_requested(:post, "https://www.pivotaltracker.com/services/v3/projects/#{tracker.project_id}/stories")
22 22 WebMock.should requested.with(:headers => {'X-Trackertoken' => tracker.api_token})
23 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 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 28 end
29 29 end
30 30  
... ...
spec/models/issue_trackers/redmine_tracker_spec.rb
1 1 require 'spec_helper'
2 2  
3 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 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 8 number = 5
9 9 @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}"
10 10 body = "<issue><subject>my subject</subject><id>#{number}</id></issue>"
11 11 stub_request(:post, "#{tracker.account}/issues.xml").
12 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 17 requested = have_requested(:post, "#{tracker.account}/issues.xml")
18 18 WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token})
19 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 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 24 end
25 25  
26 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 2  
3 3 describe Notice do
4 4  
  5 +
5 6 context 'validations' do
6 7 it 'requires a backtrace' do
7 8 notice = Factory.build(:notice, :backtrace => nil)
... ... @@ -22,6 +23,7 @@ describe Notice do
22 23 end
23 24 end
24 25  
  26 +
25 27 context '.in_app_backtrace_line?' do
26 28 let(:backtrace) do [{
27 29 'number' => rand(999),
... ... @@ -51,93 +53,6 @@ describe Notice do
51 53 end
52 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 57 describe "key sanitization" do
143 58 before do
... ... @@ -153,6 +68,7 @@ describe Notice do
153 68 end
154 69 end
155 70  
  71 +
156 72 describe "user agent" do
157 73 it "should be parsed and human-readable" do
158 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 82 end
167 83 end
168 84  
  85 +
169 86 describe "email notifications (configured individually for each app)" do
170 87 custom_thresholds = [2, 4, 8, 16, 32, 64]
171 88  
172 89 before do
173 90 Errbit::Config.per_app_email_at_notices = true
174 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 93 end
177 94  
178 95 after do
... ... @@ -181,7 +98,7 @@ describe Notice do
181 98  
182 99 custom_thresholds.each do |threshold|
183 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 102 Mailer.should_receive(:err_notification).
186 103 and_return(mock('email', :deliver => true))
187 104 Factory(:notice, :err => @err)
... ... @@ -189,5 +106,6 @@ describe Notice do
189 106 end
190 107 end
191 108  
  109 +
192 110 end
193 111  
... ...
spec/models/problem_spec.rb 0 → 100644
... ... @@ -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 49 end
50 50  
51 51 end
  52 +
... ...
spec/models/watcher_spec.rb
... ... @@ -34,3 +34,4 @@ describe Watcher do
34 34 end
35 35  
36 36 end
  37 +
... ...
spec/spec_helper.rb
... ... @@ -23,4 +23,4 @@ RSpec.configure do |config|
23 23 DatabaseCleaner.clean
24 24 end
25 25 config.include WebMock::API
26   -end
27 26 \ No newline at end of file
  27 +end
... ...
spec/support/macros.rb
... ... @@ -55,4 +55,4 @@ def it_requires_admin_privileges(options = {})
55 55 end
56 56 end
57 57 end
58   -end
59 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 3 describe "errs/show.html.haml" do
4 4 before do
5 5 err = Factory(:err)
  6 + problem = err.problem
6 7 comment = Factory(:comment)
7   - assign :err, err
  8 + assign :problem, problem
8 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 12 assign :notice, err.notices.first
12 13 end
13 14  
... ... @@ -44,9 +45,9 @@ describe &quot;errs/show.html.haml&quot; do
44 45 end
45 46  
46 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 51 render
51 52 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
52 53 comments_section.should =~ /Test comment/
... ... @@ -54,20 +55,22 @@ describe &quot;errs/show.html.haml&quot; do
54 55 end
55 56  
56 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 62 end
62 63  
63 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 67 render
66 68 view.instance_variable_get(:@_content_for)[:comments].should be_blank
67 69 end
68 70  
69 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 74 render
72 75 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
73 76 comments_section.should =~ /Test comment/
... ...