Commit 4d5a1bf84d42431b434ace49aca1272f6418375f

Authored by Bob Lail
1 parent 00791678
Exists in master and in 1 other branch production

refactored model to embed Errs into Problems so that they can be merged

Showing 53 changed files with 989 additions and 858 deletions   Show diff stats
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_errs = params[:errs] || []
  16 + @selected_problems = params[:problems] || []
17 17 @deploys = @app.deploys.order_by(:created_at.desc).limit(5)
18 18 end
19 19 format.atom do
20   - @errs = resource.errs.unresolved.ordered
  20 + @problems = resource.problems.unresolved.ordered
21 21 end
22 22 end
23 23 end
24   -
  24 +
25 25 def create
26 26 @app = App.new(params[:app])
27 27 initialize_subclassed_issue_tracker
28 28 create!
29 29 end
30   -
  30 +
31 31 def update
32 32 @app = resource
33 33 initialize_subclassed_issue_tracker
34 34 update!
35 35 end
36   -
  36 +
37 37 def new
38 38 plug_params(build_resource)
39 39 new!
40 40 end
41   -
  41 +
42 42 def edit
43 43 plug_params(resource)
44 44 edit!
45 45 end
46   -
  46 +
47 47 protected
48 48 def collection
49 49 # Sort apps by number of unresolved errs, descending.
50 50 # Caches the unresolved err counts while performing the sort.
51 51 @unresolved_counts = {}
52 52 @apps ||= end_of_association_chain.all.sort{|a,b|
53   - [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 }
54 54 @unresolved_counts[b.id] <=> @unresolved_counts[a.id]
55 55 }
56 56 end
... ...
app/controllers/errs_controller.rb
... ... @@ -2,17 +2,18 @@ class ErrsController &lt; ApplicationController
2 2 include ActionView::Helpers::TextHelper
3 3  
4 4 before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several]
5   - before_filter :find_err, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several]
6   - before_filter :find_selected_errs, :only => [:destroy_several, :resolve_several, :unresolve_several]
  5 + before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several]
  6 + before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several]
  7 +
7 8  
8 9  
9 10 def index
10 11 app_scope = current_user.admin? ? App.all : current_user.apps
11   - @errs = Err.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered
12   - @selected_errs = params[:errs] || []
  12 + @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered
  13 + @selected_problems = params[:problems] || []
13 14 respond_to do |format|
14 15 format.html do
15   - @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)
16 17 end
17 18 format.atom
18 19 end
... ... @@ -21,15 +22,15 @@ class ErrsController &lt; ApplicationController
21 22  
22 23 def all
23 24 app_scope = current_user.admin? ? App.all : current_user.apps
24   - @selected_errs = params[:errs] || []
25   - @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] || []
26 27 end
27 28  
28 29  
29 30 def show
30   - page = (params[:notice] || @err.notices_count)
  31 + page = (params[:notice] || @problem.notices_count)
31 32 page = 1 if page.to_i.zero?
32   - @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1)
  33 + @notices = @problem.notices.paginate(:page => page, :per_page => 1)
33 34 @notice = @notices.first
34 35 @comment = Comment.new
35 36 end
... ... @@ -39,32 +40,30 @@ class ErrsController &lt; ApplicationController
39 40 set_tracker_params
40 41  
41 42 if @app.issue_tracker
42   - @app.issue_tracker.create_issue @err
  43 + @app.issue_tracker.create_issue @problem
43 44 else
44 45 flash[:error] = "This app has no issue tracker setup."
45 46 end
46   - redirect_to app_err_path(@app, @err)
  47 + redirect_to app_err_path(@app, @problem)
47 48 rescue ActiveResource::ConnectionError => e
48 49 Rails.logger.error e.to_s
49 50 flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later."
50   - redirect_to app_err_path(@app, @err)
  51 + redirect_to app_err_path(@app, @problem)
51 52 end
52 53  
53 54  
54 55 def unlink_issue
55   - @err.update_attribute :issue_link, nil
56   - redirect_to app_err_path(@app, @err)
  56 + @problem.update_attribute :issue_link, nil
  57 + redirect_to app_err_path(@app, @problem)
57 58 end
58 59  
59 60  
60 61 def resolve
61 62 # Deal with bug in mongoid where find is returning an Enumberable obj
62   - @err = @err.first if @err.respond_to?(:first)
63   -
64   - @err.resolve!
  63 + @problem = @problem.first if @problem.respond_to?(:first)
65 64  
  65 + @problem.resolve!
66 66 flash[:success] = 'Great news everyone! The err has been resolved.'
67   -
68 67 redirect_to :back
69 68 rescue ActionController::RedirectBackError
70 69 redirect_to app_path(@app)
... ... @@ -74,13 +73,13 @@ class ErrsController &lt; ApplicationController
74 73 def create_comment
75 74 @comment = Comment.new(params[:comment].merge(:user_id => current_user.id))
76 75 if @comment.valid?
77   - @err.comments << @comment
78   - @err.save
  76 + @problem.comments << @comment
  77 + @problem.save
79 78 flash[:success] = "Comment saved!"
80 79 else
81 80 flash[:error] = "I'm sorry, your comment was blank! Try again?"
82 81 end
83   - redirect_to app_err_path(@app, @err)
  82 + redirect_to app_err_path(@app, @problem)
84 83 end
85 84  
86 85  
... ... @@ -91,27 +90,27 @@ class ErrsController &lt; ApplicationController
91 90 else
92 91 flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!"
93 92 end
94   - redirect_to app_err_path(@app, @err)
  93 + redirect_to app_err_path(@app, @problem)
95 94 end
96 95  
97 96  
98 97 def resolve_several
99   - @selected_errs.each(&:resolve!)
100   - flash[:success] = "Great news everyone! #{pluralize(@selected_errs.count, 'err has', 'errs have')} been resolved."
  98 + @selected_problems.each(&:resolve!)
  99 + flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved."
101 100 redirect_to :back
102 101 end
103 102  
104 103  
105 104 def unresolve_several
106   - @selected_errs.each(&:unresolve!)
107   - flash[:success] = "#{pluralize(@selected_errs.count, 'err has', 'errs have')} been unresolved."
  105 + @selected_problems.each(&:unresolve!)
  106 + flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved."
108 107 redirect_to :back
109 108 end
110 109  
111 110  
112 111 def destroy_several
113   - @selected_errs.each(&:destroy)
114   - flash[:notice] = "#{pluralize(@selected_errs.count, 'err has', 'errs have')} been deleted."
  112 + @selected_problems.each(&:destroy)
  113 + flash[:notice] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been deleted."
115 114 redirect_to :back
116 115 end
117 116  
... ... @@ -128,8 +127,11 @@ protected
128 127 end
129 128  
130 129  
131   - def find_err
132   - @err = @app.errs.find(params[:id])
  130 + def find_problem
  131 + @problem = @app.problems.find(params[:id])
  132 +
  133 + # Deal with bug in mogoid where find is returning an Enumberable obj
  134 + @problem = @problem.first if @problem.respond_to?(:first)
133 135 end
134 136  
135 137  
... ... @@ -140,13 +142,13 @@ protected
140 142 end
141 143  
142 144  
143   - def find_selected_errs
144   - err_ids = (params[:errs] || []).compact
  145 + def find_selected_problems
  146 + err_ids = (params[:problems] || []).compact
145 147 if err_ids.empty?
146 148 flash[:notice] = "You have not selected any errors"
147 149 redirect_to :back
148 150 else
149   - @selected_errs = Array(Err.find(err_ids))
  151 + @selected_problems = Array(Problem.find(err_ids))
150 152 end
151 153 end
152 154  
... ...
app/helpers/errs_helper.rb
1 1 module ErrsHelper
2   -
3   - def last_notice_at err
4   - err.last_notice_at || err.created_at
  2 +
  3 + def last_notice_at(problem)
  4 + problem.last_notice_at || problem.created_at
5 5 end
6   -
  6 +
7 7 def err_confirm
8 8 Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?'
9 9 end
10   -
11   - def link_to_github app, line, text=nil
  10 +
  11 + def link_to_github(app, line, text=nil)
12 12 file_name = line['file'].split('/').last
13 13 file_path = line['file'].gsub('[PROJECT_ROOT]', '')
14 14 line_number = line['number']
15 15 link_to(text || file_name, "#{app.github_url_to_file(file_path)}#L#{line_number}", :target => '_blank')
16 16 end
17   -
  17 +
18 18 end
19 19 \ No newline at end of file
... ...
app/mailers/mailer.rb
... ... @@ -4,21 +4,21 @@ require Rails.root.join(&#39;config/routes.rb&#39;)
4 4  
5 5 class Mailer < ActionMailer::Base
6 6 default :from => Errbit::Config.email_from
7   -
  7 +
8 8 def err_notification(notice)
9 9 @notice = notice
10   - @app = notice.err.app
11   -
  10 + @app = notice.app
  11 +
12 12 mail({
13 13 :to => @app.notification_recipients,
14   - :subject => "[#{@app.name}][#{@notice.err.environment}] #{@notice.err.message}"
  14 + :subject => "[#{@app.name}][#{@notice.environment_name}] #{@notice.message}"
15 15 })
16 16 end
17   -
  17 +
18 18 def deploy_notification(deploy)
19 19 @deploy = deploy
20   - @app = deploy.app
21   -
  20 + @app = deploy.app
  21 +
22 22 mail({
23 23 :to => @app.notification_recipients,
24 24 :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}"
... ...
app/models/app.rb
1 1 class App
2 2 include Mongoid::Document
3 3 include Mongoid::Timestamps
4   -
  4 +
5 5 field :name, :type => String
6 6 field :api_key
7 7 field :github_url
... ... @@ -10,53 +10,65 @@ class App
10 10 field :notify_on_errs, :type => Boolean, :default => true
11 11 field :notify_on_deploys, :type => Boolean, :default => false
12 12 field :email_at_notices, :type => Array, :default => Errbit::Config.email_at_notices
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.
19 20 before_create do |r|
20 21 r.id = ActiveSupport::SecureRandom.hex
21 22 end
22   -
  23 +
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   -
  27 + has_many :problems, :inverse_of => :app, :dependent => :destroy
  28 +
28 29 before_validation :generate_api_key, :on => :create
29 30 before_save :normalize_github_url
30   -
  31 +
31 32 validates_presence_of :name, :api_key
32 33 validates_uniqueness_of :name, :allow_blank => true
33 34 validates_uniqueness_of :api_key, :allow_blank => true
34 35 validates_associated :watchers
35 36 validate :check_issue_tracker
36   -
  37 +
37 38 accepts_nested_attributes_for :watchers, :allow_destroy => true,
38 39 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
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 +
  45 + def find_or_create_err!(attrs)
  46 + Err.where(attrs).first || problems.create!.errs.create!(attrs)
  47 + end
  48 +
  49 +
  50 +
  51 + # Mongoid Bug: find(id) on association proxies returns an Enumerator
42 52 def self.find_by_id!(app_id)
43 53 find app_id
44 54 end
45   -
  55 +
46 56 def self.find_by_api_key!(key)
47 57 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key))
48 58 end
49   -
  59 +
50 60 def last_deploy_at
51 61 deploys.last && deploys.last.created_at
52 62 end
53   -
  63 +
  64 +
54 65 # Legacy apps don't have notify_on_errs and notify_on_deploys params
55 66 def notify_on_errs
56 67 !(self[:notify_on_errs] == false)
57 68 end
58 69 alias :notify_on_errs? :notify_on_errs
59   -
  70 +
  71 +
60 72 def notify_on_deploys
61 73 !(self[:notify_on_deploys] == false)
62 74 end
... ... @@ -117,4 +129,3 @@ class App
117 129 end
118 130  
119 131 end
120   -
... ...
app/models/deploy.rb
1 1 class Deploy
2 2 include Mongoid::Document
3 3 include Mongoid::Timestamps
4   -
  4 +
5 5 field :username
6 6 field :repository
7 7 field :environment
8 8 field :revision
9 9 field :message
10   -
  10 +
11 11 index :created_at, Mongo::DESCENDING
12   -
  12 +
13 13 embedded_in :app, :inverse_of => :deploys
14   -
  14 +
15 15 after_create :deliver_notification, :if => :should_notify?
16 16 after_create :resolve_app_errs, :if => :should_resolve_app_errs?
17   -
  17 +
18 18 validates_presence_of :username, :environment
19   -
  19 +
20 20 def deliver_notification
21 21 Mailer.deploy_notification(self).deliver
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
29 29 revision.to_s[0,7]
30 30 end
31   -
  31 +
32 32 protected
33   -
  33 +
34 34 def should_notify?
35 35 app.notify_on_deploys? && app.watchers.any?
36 36 end
37   -
  37 +
38 38 def should_resolve_app_errs?
39 39 app.resolve_errs_on_deploy?
40 40 end
41   -
  41 +
42 42 end
43 43  
... ...
app/models/err.rb
  1 +# An Err is a group of notices that can programatically
  2 +# be determined to be equal. (Errbit groups notices into
  3 +# errs by a notice's fingerprint.)
  4 +
1 5 class Err
2 6 include Mongoid::Document
3 7 include Mongoid::Timestamps
4   -
  8 +
5 9 field :klass
6 10 field :component
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   -
16   - index :last_notice_at
17   - index :app_id
18   - index :notices
19   -
20   - belongs_to :app
21   - has_many :notices
22   - has_many :comments, :inverse_of => :err, :dependent => :destroy
23   -
24   - validates_presence_of :klass, :environment
25   -
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 unresolve!
45   - self.update_attributes!(:resolved => false)
46   - end
47   -
48   - def unresolved?
49   - !resolved?
50   - end
51   -
52   - def where
53   - where = component.dup
54   - where << "##{action}" if action.present?
55   - where
56   - end
57   -
58   - def message
59   - super || klass
60   - end
61   -
  14 +
  15 + belongs_to :problem
  16 + has_many :notices, :inverse_of => :err, :dependent => :destroy
  17 +
  18 + validates_presence_of :klass, :environment, :problem
  19 +
  20 + delegate :app,
  21 + :resolved?,
  22 + :to => :problem
  23 +
  24 +
62 25 end
... ...
app/models/issue_tracker.rb
... ... @@ -19,8 +19,8 @@ class IssueTracker
19 19 # Subclasses are responsible for overwriting this method.
20 20 def check_params; true; end
21 21  
22   - def issue_title err
23   - "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}"
  22 + def issue_title(problem)
  23 + "[#{ problem.environment }][#{ problem.where }] #{problem.message.to_s.truncate(100)}"
24 24 end
25 25  
26 26 # 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
... ... @@ -17,7 +17,7 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
17 17 end
18 18 end
19 19  
20   - def create_issue(err)
  20 + def create_issue(problem)
21 21 token = api_token
22 22 acc = account
23 23 RedmineClient::Base.configure do
... ... @@ -25,10 +25,10 @@ class IssueTrackers::RedmineTracker &lt; IssueTracker
25 25 self.site = acc
26 26 end
27 27 issue = RedmineClient::Issue.new(:project_id => project_id)
28   - issue.subject = issue_title err
  28 + issue.subject = issue_title problem
29 29 issue.description = body_template.result(binding)
30 30 issue.save!
31   - 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}")
  31 + 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}")
32 32 end
33 33  
34 34 def body_template
... ...
app/models/notice.rb
... ... @@ -4,46 +4,50 @@ require &#39;recurse&#39;
4 4 class Notice
5 5 include Mongoid::Document
6 6 include Mongoid::Timestamps
7   -
  7 +
8 8 field :message
9 9 field :backtrace, :type => Array
10 10 field :server_environment, :type => Hash
11 11 field :request, :type => Hash
12 12 field :notifier, :type => Hash
13   -
  13 + field :klass
  14 +
14 15 belongs_to :err
15 16 index :err_id
16   -
17   - after_create :cache_last_notice_at
  17 + index :created_at
  18 +
18 19 after_create :deliver_notification, :if => :should_notify?
19   - before_create :increase_counter_cache, :cache_message
  20 + after_create :increase_counter_cache, :cache_attributes_on_problem
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   -
  27 + scope :for_errs, lambda {|errs| where(:err_id.in => errs.all.map(&:id))}
  28 +
  29 + delegate :app, :problem, :to => :err
  30 +
  31 +
  32 +
28 33 def self.from_xml(hoptoad_xml)
29 34 hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml)
30 35 app = App.find_by_api_key!(hoptoad_notice['api-key'])
31   -
  36 +
32 37 hoptoad_notice['request'] ||= {}
33 38 hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank?
34 39 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']
  40 +
  41 + err = app.find_or_create_err!({
  42 + :klass => hoptoad_notice['error']['class'],
  43 + :component => hoptoad_notice['request']['component'],
  44 + :action => hoptoad_notice['request']['action'],
  45 + :environment => hoptoad_notice['server-environment']['environment-name'],
  46 + :fingerprint => hoptoad_notice['fingerprint']
43 47 })
44   - err.update_attributes(:resolved => false) if err.resolved?
45   -
  48 + err.problem.update_attributes(:resolved => false) if err.problem.resolved?
46 49 err.notices.create!({
  50 + :klass => hoptoad_notice['error']['class'],
47 51 :message => hoptoad_notice['error']['message'],
48 52 :backtrace => [hoptoad_notice['error']['backtrace']['line']].flatten,
49 53 :server_environment => hoptoad_notice['server-environment'],
... ... @@ -51,64 +55,97 @@ class Notice
51 55 :notifier => hoptoad_notice['notifier']
52 56 })
53 57 end
54   -
  58 +
  59 +
  60 +
55 61 def user_agent
56 62 agent_string = env_vars['HTTP_USER_AGENT']
57 63 agent_string.blank? ? nil : UserAgent.parse(agent_string)
58 64 end
59   -
  65 +
  66 + def environment_name
  67 + server_environment['server-environment'] || server_environment['environment-name']
  68 + end
  69 +
  70 + def component
  71 + request['component']
  72 + end
  73 +
  74 + def action
  75 + request['action']
  76 + end
  77 +
  78 + def where
  79 + where = component.to_s.dup
  80 + where << "##{action}" if action.present?
  81 + where
  82 + end
  83 +
  84 +
  85 +
60 86 def self.in_app_backtrace_line? line
61 87 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})
62 88 end
63   -
  89 +
  90 +
  91 +
64 92 def request
65 93 read_attribute(:request) || {}
66 94 end
67   -
  95 +
68 96 def env_vars
69 97 request['cgi-data'] || {}
70 98 end
71   -
  99 +
72 100 def params
73 101 request['params'] || {}
74 102 end
75   -
  103 +
76 104 def session
77 105 request['session'] || {}
78 106 end
79   -
  107 +
  108 +
  109 +
80 110 def deliver_notification
81 111 Mailer.err_notification(self).deliver
82 112 end
83   -
  113 +
84 114 def cache_last_notice_at
85 115 err.update_attributes(:last_notice_at => created_at)
86 116 end
87   -
  117 +
88 118 # Backtrace containing only files from the app itself (ignore gems)
89 119 def app_backtrace
90 120 backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") }
91 121 end
92   -
93   - protected
94   -
  122 +
  123 +
  124 +
  125 +protected
  126 +
  127 +
  128 +
95 129 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?
  130 + app.notify_on_errs? && (Errbit::Config.per_app_email_at_notices && app.email_at_notices || Errbit::Config.email_at_notices).include?(err.notices.count) && app.watchers.any?
97 131 end
98   -
99   -
  132 +
  133 +
100 134 def increase_counter_cache
101   - err.inc(:notices_count,1)
  135 + problem.inc(:notices_count, 1)
102 136 end
103   -
  137 +
  138 +
104 139 def decrease_counter_cache
105   - err.inc(:notices_count,-1)
  140 + problem.inc(:notices_count, -1)
106 141 end
107   -
108   - def cache_message
109   - err.update_attribute(:message, message) if err.notices_count == 1
  142 +
  143 +
  144 + def cache_attributes_on_problem
  145 + problem.cache_notice_attributes(self) if problem.notices_count == 1
110 146 end
111   -
  147 +
  148 +
112 149 def sanitize
113 150 [:server_environment, :request, :notifier].each do |h|
114 151 send("#{h}=",sanitize_hash(send(h)))
... ... @@ -116,7 +153,8 @@ class Notice
116 153 # Set unknown backtrace files
117 154 backtrace.each{|line| line['file'] = "[unknown source]" if line['file'].blank? }
118 155 end
119   -
  156 +
  157 +
120 158 def sanitize_hash(h)
121 159 h.recurse do
122 160 |h| h.inject({}) do |h,(k,v)|
... ... @@ -129,5 +167,6 @@ class Notice
129 167 end
130 168 end
131 169 end
  170 +
  171 +
132 172 end
133   -
... ...
app/models/problem.rb 0 → 100644
... ... @@ -0,0 +1,70 @@
  1 +# An Problem is a group of errs that the user
  2 +# has declared to be equal.
  3 +
  4 +class Problem
  5 + include Mongoid::Document
  6 + include Mongoid::Timestamps
  7 +
  8 + field :last_notice_at, :type => DateTime
  9 + field :resolved, :type => Boolean, :default => false
  10 + field :issue_link, :type => String
  11 +
  12 + # Cached fields
  13 + field :notices_count, :type => Integer, :default => 0
  14 + field :message
  15 + field :environment
  16 + field :klass
  17 + field :where
  18 +
  19 + index :last_notice_at
  20 + index :app_id
  21 +
  22 + belongs_to :app
  23 + has_many :errs, :inverse_of => :problem, :dependent => :destroy
  24 + has_many :comments, :inverse_of => :err, :dependent => :destroy
  25 +
  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 +
  36 +
  37 + def notices
  38 + Notice.for_errs(errs).ordered
  39 + end
  40 +
  41 +
  42 + def resolve!
  43 + self.update_attributes!(:resolved => true)
  44 + end
  45 +
  46 +
  47 + def unresolve!
  48 + self.update_attributes!(:resolved => false)
  49 + end
  50 +
  51 +
  52 + def unresolved?
  53 + !resolved?
  54 + end
  55 +
  56 +
  57 +
  58 + def cache_notice_attributes(notice=nil)
  59 + notice ||= notices.first
  60 + attrs = {:last_notice_at => notices.max(:created_at)}
  61 + attrs.merge!(
  62 + :message => notice.message,
  63 + :environment => notice.environment_name,
  64 + :klass => notice.klass,
  65 + :where => notice.where) if notice
  66 + update_attributes!(attrs)
  67 + end
  68 +
  69 +
  70 +end
0 71 \ No newline at end of file
... ...
app/views/apps/index.html.haml
... ... @@ -14,7 +14,7 @@
14 14 %td.name= link_to app.name, app_path(app)
15 15 %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro) << " (#{app.deploys.last.short_revision})", app_deploys_path(app)) : 'n/a'
16 16 %td.count
17   - - if app.errs.count > 0
  17 + - if app.problems.count > 0
18 18 - unresolved = @unresolved_counts[app.id]
19 19 = link_to unresolved, app_path(app), :class => (unresolved == 0 ? "resolved" : nil)
20 20 - 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
... ... @@ -10,28 +10,28 @@
10 10 %th Count
11 11 %th Resolve
12 12 %tbody
13   - - errs.each do |err|
14   - %tr{:class => err.resolved? ? 'resolved' : 'unresolved'}
  13 + - errs.each do |problem|
  14 + %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'}
15 15 %td.select
16   - = check_box_tag "errs[]", err.id, @selected_errs.member?(err.id.to_s)
  16 + = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s)
17 17 %td.app
18   - = link_to err.app.name, app_path(err.app)
  18 + = link_to problem.app.name, app_path(problem.app)
19 19 - if current_page?(:controller => 'errs')
20   - %span.environment= link_to err.environment, errs_path(environment: err.environment)
  20 + %span.environment= link_to problem.environment, errs_path(environment: problem.environment)
21 21 - else
22   - %span.environment= link_to err.environment, app_path(err.app, environment: err.environment)
  22 + %span.environment= link_to problem.environment, app_path(problem.app, environment: problem.environment)
23 23 %td.message
24   - = link_to err.message, app_err_path(err.app, err)
25   - %em= err.where
26   - %td.latest #{time_ago_in_words(last_notice_at err)} ago
27   - %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a'
28   - %td.count= link_to err.notices.count, app_err_path(err.app, err)
29   - %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?
  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 30 - if errs.none?
31 31 %tr
32 32 %td{:colspan => (@app ? 5 : 6)}
33 33 %em No errs here
34   - = will_paginate @errs, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'
  34 + = will_paginate @problems, :previous_label => '&laquo; Previous', :next_label => 'Next &raquo;'
35 35 .tab-bar
36 36 %ul
37 37 %li= submit_tag 'Resolve', :id => 'resolve_errs', :class => 'button', 'data-action' => resolve_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 !@app.issue_tracker_configured? || @err.comments.any?
  23 +- if !@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 - unless @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,13 +9,13 @@
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)
... ...
db/migrate/20110422152027_move_notices_to_separate_collection.rb
... ... @@ -17,10 +17,9 @@ 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
... ...
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.find(app_id)
  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
0 28 \ No newline at end of file
... ...
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
... ... @@ -4,7 +4,7 @@ namespace :errbit do
4 4 require 'factory_girl_rails'
5 5 Dir.glob(File.join(Rails.root,'spec/factories/*.rb')).each {|f| require f }
6 6 app = Factory(:app, :name => "Demo App #{Time.now.strftime("%N")}")
7   - Factory(:notice, :err => Factory(:err, :app => app))
  7 + Factory(:notice, :err => Factory(:err, :problem => Factory(:problem, :app => app)))
8 8 puts "=== Created demo app: '#{app.name}', with an example error."
9 9 end
10 10 end
... ...
public/javascripts/application.js
... ... @@ -62,7 +62,7 @@ $(function() {
62 62 function activateSelectableRows() {
63 63 $('.selectable tr').click(function(event) {
64 64 if(!_.include(['A', 'INPUT', 'BUTTON', 'TEXTAREA'], event.target.nodeName)) {
65   - var checkbox = $(this).find('input[name="errs[]"]');
  65 + var checkbox = $(this).find('input[name="problems[]"]');
66 66 checkbox.attr('checked', !checkbox.is(':checked'));
67 67 }
68 68 })
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -2,10 +2,11 @@ require &#39;spec_helper&#39;
2 2  
3 3 describe AppsController do
4 4 render_views
5   -
  5 +
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
... ... @@ -16,7 +17,7 @@ describe AppsController do
16 17 assigns(:apps).should == apps
17 18 end
18 19 end
19   -
  20 +
20 21 context 'when logged in as a regular user' do
21 22 it 'finds apps the user is watching' do
22 23 sign_in(user = Factory(:user))
... ... @@ -31,116 +32,116 @@ describe AppsController do
31 32 end
32 33 end
33 34 end
34   -
  35 +
35 36 describe "GET /apps/:id" do
36 37 context 'logged in as an admin' do
37 38 before(:each) 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
46 46 get :show, :id => @app.id
47 47 assigns(:app).should == @app
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   - context 'and all_errs=true params' do
  91 +
  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
98   -
  99 +
99 100 context 'with environment filters' 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
142 143 end
143   -
  144 +
144 145 context 'logged in as a user' do
145 146 it 'finds the app if the user is watching it' do
146 147 user = Factory(:user)
... ... @@ -150,7 +151,7 @@ describe AppsController do
150 151 get :show, :id => app.id
151 152 assigns(:app).should == app
152 153 end
153   -
  154 +
154 155 it 'does not find the app if the user is not watching it' do
155 156 sign_in Factory(:user)
156 157 app = Factory(:app)
... ... @@ -160,12 +161,12 @@ describe AppsController do
160 161 end
161 162 end
162 163 end
163   -
  164 +
164 165 context 'logged in as an admin' do
165 166 before do
166 167 sign_in Factory(:admin)
167 168 end
168   -
  169 +
169 170 describe "GET /apps/new" do
170 171 it 'instantiates a new app with a prebuilt watcher' do
171 172 get :new
... ... @@ -184,7 +185,7 @@ describe AppsController do
184 185 assigns(:app).github_url.should == "github.com/test/example"
185 186 end
186 187 end
187   -
  188 +
188 189 describe "GET /apps/:id/edit" do
189 190 it 'finds the correct app' do
190 191 app = Factory(:app)
... ... @@ -192,47 +193,47 @@ describe AppsController do
192 193 assigns(:app).should == app
193 194 end
194 195 end
195   -
  196 +
196 197 describe "POST /apps" do
197 198 before do
198 199 @app = Factory(:app)
199 200 App.stub(:new).and_return(@app)
200 201 end
201   -
  202 +
202 203 context "when the create is successful" do
203 204 before do
204 205 @app.should_receive(:save).and_return(true)
205 206 end
206   -
  207 +
207 208 it "should redirect to the app page" do
208 209 post :create, :app => {}
209 210 response.should redirect_to(app_path(@app))
210 211 end
211   -
  212 +
212 213 it "should display a message" do
213 214 post :create, :app => {}
214 215 request.flash[:success].should match(/success/)
215 216 end
216 217 end
217 218 end
218   -
  219 +
219 220 describe "PUT /apps/:id" do
220 221 before do
221 222 @app = Factory(:app)
222 223 end
223   -
  224 +
224 225 context "when the update is successful" do
225 226 it "should redirect to the app page" do
226 227 put :update, :id => @app.id, :app => {}
227 228 response.should redirect_to(app_path(@app))
228 229 end
229   -
  230 +
230 231 it "should display a message" do
231 232 put :update, :id => @app.id, :app => {}
232 233 request.flash[:success].should match(/success/)
233 234 end
234 235 end
235   -
  236 +
236 237 context "changing name" do
237 238 it "should redirect to app page" do
238 239 id = @app.id
... ... @@ -240,14 +241,14 @@ describe AppsController do
240 241 response.should redirect_to(app_path(id))
241 242 end
242 243 end
243   -
  244 +
244 245 context "when the update is unsuccessful" do
245 246 it "should render the edit page" do
246 247 put :update, :id => @app.id, :app => { :name => '' }
247 248 response.should render_template(:edit)
248 249 end
249 250 end
250   -
  251 +
251 252 context "changing email_at_notices" do
252 253 it "should parse legal csv values" do
253 254 put :update, :id => @app.id, :app => { :email_at_notices => '1, 4, 7,8, 10' }
... ... @@ -261,14 +262,14 @@ describe AppsController do
261 262 @app.reload
262 263 @app.email_at_notices.should == Errbit::Config.email_at_notices
263 264 end
264   -
  265 +
265 266 it "should display a message" do
266 267 put :update, :id => @app.id, :app => { :email_at_notices => 'qwertyuiop' }
267 268 request.flash[:error].should match(/Couldn't parse/)
268 269 end
269 270 end
270 271 end
271   -
  272 +
272 273 context "setting up issue tracker", :cur => true do
273 274 context "unknown tracker type" do
274 275 before(:each) do
... ... @@ -277,12 +278,12 @@ describe AppsController do
277 278 } }
278 279 @app.reload
279 280 end
280   -
  281 +
281 282 it "should not create issue tracker" do
282 283 @app.issue_tracker_configured?.should == false
283 284 end
284 285 end
285   -
  286 +
286 287 IssueTracker.subclasses.each do |tracker_klass|
287 288 context tracker_klass do
288 289 it "should save tracker params" do
... ... @@ -290,7 +291,7 @@ describe AppsController do
290 291 params['ticket_properties'] = "card_type = defect" if tracker_klass == MingleTracker
291 292 params['type'] = tracker_klass.to_s
292 293 put :update, :id => @app.id, :app => {:issue_tracker_attributes => params}
293   -
  294 +
294 295 @app.reload
295 296 tracker = @app.issue_tracker
296 297 tracker.should be_a(tracker_klass)
... ... @@ -301,13 +302,13 @@ describe AppsController do
301 302 end
302 303 end
303 304 end
304   -
  305 +
305 306 it "should show validation notice when sufficient params are not present" do
306 307 # Leave out one required param
307 308 params = tracker_klass::Fields[1..-1].inject({}){|hash,f| hash[f[0]] = "test_value"; hash }
308 309 params['type'] = tracker_klass.to_s
309 310 put :update, :id => @app.id, :app => {:issue_tracker_attributes => params}
310   -
  311 +
311 312 @app.reload
312 313 @app.issue_tracker_configured?.should == false
313 314 response.body.should match(/You must specify your/)
... ... @@ -316,34 +317,34 @@ describe AppsController do
316 317 end
317 318 end
318 319 end
319   -
  320 +
320 321 describe "DELETE /apps/:id" do
321 322 before do
322 323 @app = Factory(:app)
323 324 App.stub(:find).with(@app.id).and_return(@app)
324 325 end
325   -
  326 +
326 327 it "should find the app" do
327 328 delete :destroy, :id => @app.id
328 329 assigns(:app).should == @app
329 330 end
330   -
  331 +
331 332 it "should destroy the app" do
332 333 @app.should_receive(:destroy)
333 334 delete :destroy, :id => @app.id
334 335 end
335   -
  336 +
336 337 it "should display a message" do
337 338 delete :destroy, :id => @app.id
338 339 request.flash[:success].should match(/success/)
339 340 end
340   -
  341 +
341 342 it "should redirect to the apps page" do
342 343 delete :destroy, :id => @app.id
343 344 response.should redirect_to(apps_path)
344 345 end
345 346 end
346 347 end
347   -
  348 +
  349 +
348 350 end
349   -
... ...
spec/controllers/errs_controller_spec.rb
... ... @@ -6,443 +6,443 @@ describe ErrsController do
6 6 :index => :get, :all => :get, :show => :get, :resolve => :put
7 7 },
8 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9   -
  9 +
10 10 let(:app) { Factory(:app) }
11   - let(:err) { Factory(:err, :app => app) }
12   -
  11 + let(:err) { Factory(:err, :problem => Factory(:problem, :app => app, :environment => "production")) }
  12 +
  13 +
13 14 describe "GET /errs" do
14 15 render_views
15 16 context 'when logged in as an admin' 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
36 36 before(:each) do
37 37 35.times { Factory :err }
38 38 end
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 +
52 52 context 'with environment filters' 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
95 95 end
96   -
  96 +
97 97 context 'when logged in as a user' 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
109   -
  109 +
110 110 describe "GET /errs/all" do
111 111 context 'when logged in as an admin' 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
137   -
  137 +
138 138 describe "GET /apps/:app_id/errs/:id" do
139 139 render_views
140   -
  140 +
141 141 before do
142 142 3.times { Factory(:notice, :err => err)}
143 143 end
144   -
  144 +
145 145 context 'when logged in as an admin' do
146 146 before do
147 147 sign_in Factory(:admin)
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 +
165 165 context "create issue button" do
166 166 let(:button_matcher) { match(/create issue/) }
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
171   -
  170 + get :show, :app_id => err.app.id, :id => err.problem.id
  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
179   -
  177 + err = Factory(:err, :problem => Factory(:problem, :app => tracker.app))
  178 + get :show, :app_id => err.app.id, :id => err.problem.id
  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
187   -
  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 +
188 188 response.body.should_not button_matcher
189 189 end
190 190 end
191 191 end
192   -
  192 +
193 193 context 'when logged in as a user' do
194 194 before do
195 195 sign_in(@user = Factory(:user))
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
213 213 end
214   -
  214 +
215 215 describe "PUT /apps/:app_id/errs/:id/resolve" do
216 216 before do
217 217 sign_in Factory(:admin)
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!)
  218 +
  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
254   -
  254 +
255 255 describe "POST /apps/:app_id/errs/:id/create_issue" do
256 256 render_views
257   -
  257 +
258 258 before(:each) do
259 259 sign_in Factory(:admin)
260 260 end
261   -
  261 +
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 }
267   -
  265 + let(:tracker) { Factory :lighthouse_tracker, :app => notice.app }
  266 + let(:problem) { notice.problem }
  267 +
268 268 before(:each) do
269 269 number = 5
270 270 @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
271 271 body = "<ticket><number type=\"integer\">#{number}</number></ticket>"
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   -
275   - post :create_issue, :app_id => err.app.id, :id => err.id
276   - err.reload
  274 +
  275 + post :create_issue, :app_id => problem.app.id, :id => problem.id
  276 + problem.reload
277 277 end
278   -
279   - it "should redirect to err page" do
280   - response.should redirect_to( app_err_path(err.app, err) )
  278 +
  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 }
287   -
  286 + let(:problem) { Factory :problem }
  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   -
292   - it "should redirect to err page" do
293   - response.should redirect_to( app_err_path(err.app, err) )
  291 +
  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
297 297 flash[:error].should == "This app has no issue tracker setup."
298 298 end
299 299 end
300   -
  300 +
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 }
305   -
  304 + let(:err) { Factory(:err, :problem => Factory(:problem, :app => tracker.app)) }
  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   -
309   - post :create_issue, :app_id => err.app.id, :id => err.id
  308 +
  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
317 317 flash[:error].should == "There was an error during issue creation. Check your tracker settings or try again later."
318 318 end
319 319 end
320 320 end
321 321 end
322   -
  322 +
323 323 describe "DELETE /apps/:app_id/errs/:id/unlink_issue" do
324 324 before(:each) do
325 325 sign_in Factory(:admin)
326 326 end
327   -
  327 +
328 328 context "err with issue" do
329   - let(:err) { Factory :err, :issue_link => "http://some.host" }
330   -
  329 + let(:err) { Factory(:err, :problem => Factory(:problem, :issue_link => "http://some.host")) }
  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 +
345 345 context "err without issue" 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
358   -
359   -
  358 +
  359 +
360 360 describe "POST /apps/:app_id/errs/:id/create_comment" do
361 361 render_views
362   -
  362 +
363 363 before(:each) do
364 364 sign_in Factory(:admin)
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   -
381   - it "should redirect to err page" do
382   - response.should redirect_to( app_err_path(err.app, err) )
  380 +
  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
386   -
  386 +
387 387 describe "DELETE /apps/:app_id/errs/:id/destroy_comment" do
388 388 render_views
389   -
  389 +
390 390 before(:each) do
391 391 sign_in Factory(:admin)
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 }
397   -
  395 + let(:problem) { Factory(:problem_with_comments) }
  396 + let(:comment) { problem.comments.first }
  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   -
407   - it "should redirect to err page" do
408   - response.should redirect_to( app_err_path(err.app, err) )
  406 +
  407 + it "should redirect to problem page" do
  408 + response.should redirect_to( app_err_path(problem.app, problem) )
409 409 end
410 410 end
411 411 end
412   -
  412 +
413 413 describe "Bulk Actions" do
414 414 before(:each) do
415 415 sign_in Factory(:admin)
416   - @err1 = Factory(:err, :resolved => true)
417   - @err2 = Factory(:err, :resolved => false)
  416 + @problem1 = Factory(:problem, :resolved => true)
  417 + @problem2 = Factory(:problem, :resolved => false)
418 418 end
419 419  
420   - it "should apply to multiple errs" do
421   - post :resolve_several, :errs => [@err1.id.to_s, @err2.id.to_s]
422   - assigns(:selected_errs).should == [@err1, @err2]
  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 423 end
424 424  
425 425 context "POST /errs/resolve_several" do
426 426 it "should resolve the issue" do
427   - post :resolve_several, :errs => [@err2.id.to_s]
428   - @err2.reload.resolved?.should == true
  427 + post :resolve_several, :problems => [@problem2.id.to_s]
  428 + @problem2.reload.resolved?.should == true
429 429 end
430 430 end
431 431  
432 432 context "POST /errs/unresolve_several" do
433 433 it "should unresolve the issue" do
434   - post :unresolve_several, :errs => [@err1.id.to_s]
435   - @err1.reload.resolved?.should == false
  434 + post :unresolve_several, :problems => [@problem1.id.to_s]
  435 + @problem1.reload.resolved?.should == false
436 436 end
437 437 end
438 438  
439 439 context "POST /errs/destroy_several" do
440 440 it "should delete the errs" do
441 441 lambda {
442   - post :destroy_several, :errs => [@err1.id.to_s]
443   - }.should change(Err, :count).by(-1)
  442 + post :destroy_several, :problems => [@problem1.id.to_s]
  443 + }.should change(Problem, :count).by(-1)
444 444 end
445 445 end
446 446 end
  447 +
447 448 end
448   -
... ...
spec/controllers/notices_controller_spec.rb
... ... @@ -29,9 +29,9 @@ 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  
... ...
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'
19 27 n.notifier 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com'
20 28 end
21 29  
... ... @@ -28,4 +36,3 @@ def random_backtrace
28 36 }}
29 37 backtrace
30 38 end
31   -
... ...
spec/models/app_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe App do
4   -
  4 +
  5 +
5 6 context 'validations' do
6 7 it 'requires a name' do
7 8 app = Factory.build(:app, :name => nil)
8 9 app.should_not be_valid
9 10 app.errors[:name].should include("can't be blank")
10 11 end
11   -
  12 +
12 13 it 'requires unique names' do
13 14 Factory(:app, :name => 'Errbit')
14 15 app = Factory.build(:app, :name => 'Errbit')
15 16 app.should_not be_valid
16 17 app.errors[:name].should include('is already taken')
17 18 end
18   -
  19 +
19 20 it 'requires unique api_keys' do
20 21 Factory(:app, :api_key => 'APIKEY')
21 22 app = Factory.build(:app, :api_key => 'APIKEY')
... ... @@ -23,7 +24,8 @@ describe App do
23 24 app.errors[:api_key].should include('is already taken')
24 25 end
25 26 end
26   -
  27 +
  28 +
27 29 context 'being created' do
28 30 it 'generates a new api-key' do
29 31 app = Factory.build(:app)
... ... @@ -31,62 +33,62 @@ describe App do
31 33 app.save
32 34 app.api_key.should_not be_nil
33 35 end
34   -
  36 +
35 37 it 'generates a correct api-key' do
36 38 app = Factory(:app)
37 39 app.api_key.should match(/^[a-f0-9]{32}$/)
38 40 end
39   -
  41 +
40 42 it 'is fine with blank github urls' do
41 43 app = Factory.build(:app, :github_url => "")
42 44 app.save
43 45 app.github_url.should == ""
44 46 end
45   -
  47 +
46 48 it 'does not touch https github urls' do
47 49 app = Factory.build(:app, :github_url => "https://github.com/errbit/errbit")
48 50 app.save
49 51 app.github_url.should == "https://github.com/errbit/errbit"
50 52 end
51   -
  53 +
52 54 it 'normalizes http github urls' do
53 55 app = Factory.build(:app, :github_url => "http://github.com/errbit/errbit")
54 56 app.save
55 57 app.github_url.should == "https://github.com/errbit/errbit"
56 58 end
57   -
  59 +
58 60 it 'normalizes public git repo as a github url' do
59 61 app = Factory.build(:app, :github_url => "https://github.com/errbit/errbit.git")
60 62 app.save
61 63 app.github_url.should == "https://github.com/errbit/errbit"
62 64 end
63   -
  65 +
64 66 it 'normalizes private git repo as a github url' do
65 67 app = Factory.build(:app, :github_url => "git@github.com:errbit/errbit.git")
66 68 app.save
67 69 app.github_url.should == "https://github.com/errbit/errbit"
68 70 end
69 71 end
70   -
  72 +
71 73 context '#github_url_to_file' do
72 74 it 'resolves to full path to file' do
73 75 app = Factory(:app, :github_url => "https://github.com/errbit/errbit")
74 76 app.github_url_to_file('/path/to/file').should == "https://github.com/errbit/errbit/blob/master/path/to/file"
75 77 end
76 78 end
77   -
  79 +
78 80 context '#github_url?' do
79 81 it 'is true when there is a github_url' do
80 82 app = Factory(:app, :github_url => "https://github.com/errbit/errbit")
81 83 app.github_url?.should be_true
82 84 end
83   -
  85 +
84 86 it 'is false when no github_url' do
85 87 app = Factory(:app)
86 88 app.github_url?.should be_false
87 89 end
88 90 end
89   -
  91 +
90 92 context "notification recipients" do
91 93 it "should send notices to either all users, or the configured watchers" do
92 94 @app = Factory(:app)
... ... @@ -98,7 +100,8 @@ describe App do
98 100 @app.notification_recipients.size.should == 5
99 101 end
100 102 end
101   -
  103 +
  104 +
102 105 context "copying attributes from existing app" do
103 106 it "should only copy the necessary fields" do
104 107 @app, @copy_app = Factory(:app, :name => "app", :github_url => "url"),
... ... @@ -110,5 +113,36 @@ describe App do
110 113 @app.watchers.first.email.should == "copywatcher@example.com"
111 114 end
112 115 end
  116 +
  117 +
  118 + context '#find_or_create_err!' do
  119 + before do
  120 + @app = Factory(:app)
  121 + @conditions = {
  122 + :klass => 'Whoops',
  123 + :component => 'Foo',
  124 + :action => 'bar',
  125 + :environment => 'production'
  126 + }
  127 + end
  128 +
  129 + it 'returns the correct err if one already exists' do
  130 + existing = Factory(:err, @conditions.merge(:problem => Factory(:problem, :app => @app)))
  131 + Err.where(@conditions).first.should == existing
  132 + @app.find_or_create_err!(@conditions).should == existing
  133 + end
  134 +
  135 + it 'assigns the returned err to the given app' do
  136 + @app.find_or_create_err!(@conditions).app.should == @app
  137 + end
  138 +
  139 + it 'creates a new problem if a matching one does not already exist' do
  140 + Err.where(@conditions).first.should be_nil
  141 + lambda {
  142 + @app.find_or_create_err!(@conditions)
  143 + }.should change(Problem,:count).by(1)
  144 + end
  145 + end
  146 +
  147 +
113 148 end
114   -
... ...
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
1 1 require 'spec_helper'
2 2  
3 3 describe Err do
4   -
  4 +
5 5 context 'validations' do
6 6 it 'requires a klass' do
7 7 err = Factory.build(:err, :klass => nil)
8 8 err.should_not be_valid
9 9 err.errors[:klass].should include("can't be blank")
10 10 end
11   -
  11 +
12 12 it 'requires an environment' do
13 13 err = Factory.build(:err, :environment => nil)
14 14 err.should_not be_valid
15 15 err.errors[:environment].should include("can't be blank")
16 16 end
17 17 end
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   -
  18 +
171 19 end
172   -
... ...
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
8   -
  6 + tracker = Factory :fogbugz_tracker, :app => notice.app
  7 + problem = notice.problem
  8 +
9 9 number = 123
10 10 @issue_link = "https://#{tracker.account}.fogbugz.com/default.asp?#{number}"
11 11 response = "<response><token>12345</token><case><ixBug>123</ixBug></case></response>"
... ... @@ -13,11 +13,10 @@ describe FogbugzTracker do
13 13 http_mock.should_receive(:new).and_return(http_mock)
14 14 http_mock.should_receive(:request).twice.and_return(response)
15 15 Fogbugz.adapter[:http] = http_mock
16   -
17   - err.app.issue_tracker.create_issue(err)
18   - err.reload
19   -
20   - err.issue_link.should == @issue_link
  16 +
  17 + problem.app.issue_tracker.create_issue(problem)
  18 + problem.reload
  19 +
  20 + problem.issue_link.should == @issue_link
21 21 end
22 22 end
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
8   -
  6 + tracker = Factory :github_issues_tracker, :app => notice.app
  7 + problem = notice.problem
  8 +
9 9 number = 5
10 10 @issue_link = "https://github.com/#{tracker.project_id}/issues/#{number}"
11 11 body = <<EOF
... ... @@ -26,16 +26,15 @@ describe GithubIssuesTracker do
26 26 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   -
30   - err.app.issue_tracker.create_issue(err)
31   - err.reload
32   -
  29 +
  30 + problem.app.issue_tracker.create_issue(problem)
  31 + problem.reload
  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   -
38   - err.issue_link.should == @issue_link
  37 +
  38 + problem.issue_link.should == @issue_link
39 39 end
40 40 end
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
8   -
  6 + tracker = Factory :lighthouse_tracker, :app => notice.app
  7 + problem = notice.problem
  8 +
9 9 number = 5
10 10 @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
11 11 body = "<ticket><number type=\"integer\">#{number}</number></ticket>"
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   -
15   - err.app.issue_tracker.create_issue(err)
16   - err.reload
17   -
  14 +
  15 + problem.app.issue_tracker.create_issue(problem)
  16 + problem.reload
  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   -
24   - err.issue_link.should == @issue_link.sub(/\.xml$/, '')
  23 +
  24 + problem.issue_link.should == @issue_link.sub(/\.xml$/, '')
25 25 end
26 26 end
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
8   -
  6 + tracker = Factory :mingle_tracker, :app => notice.app
  7 + problem = notice.problem
  8 +
9 9 number = 5
10 10 @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/cards/#{number}.xml"
11 11 @basic_auth = tracker.account.gsub("://", "://#{tracker.username}:#{tracker.password}@")
12 12 body = "<card><id type=\"integer\">#{number}</id></card>"
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   -
16   - err.app.issue_tracker.create_issue(err)
17   - err.reload
18   -
  15 +
  16 + problem.app.issue_tracker.create_issue(problem)
  17 + problem.reload
  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'})
21 21 WebMock.should requested.with(:body => /FooError: Too Much Bar/)
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   -
25   - err.issue_link.should == @issue_link.sub(/\.xml$/, '')
  24 +
  25 + problem.issue_link.should == @issue_link.sub(/\.xml$/, '')
26 26 end
27 27 end
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
8   -
  6 + tracker = Factory :pivotal_labs_tracker, :app => notice.app, :project_id => 10
  7 + problem = notice.problem
  8 +
9 9 story_id = 5
10 10 @issue_link = "https://www.pivotaltracker.com/story/show/#{story_id}"
11 11 project_body = "<project><id>#{tracker.project_id}</id><name>TestProject</name></project>"
... ... @@ -14,17 +14,16 @@ describe PivotalLabsTracker do
14 14 story_body = "<story><name>Test Story</name><id>#{story_id}</id></story>"
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   -
18   - err.app.issue_tracker.create_issue(err)
19   - err.reload
20   -
  17 +
  18 + problem.app.issue_tracker.create_issue(problem)
  19 + problem.reload
  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   -
27   - err.issue_link.should == @issue_link
  26 +
  27 + problem.issue_link.should == @issue_link
28 28 end
29 29 end
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   -
14   - err.app.issue_tracker.create_issue(err)
15   - err.reload
16   -
  13 +
  14 + problem.app.issue_tracker.create_issue(problem)
  15 + problem.reload
  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   -
23   - err.issue_link.should == @issue_link.sub(/\.xml/, '')
  22 +
  23 + problem.issue_link.should == @issue_link.sub(/\.xml/, '')
24 24 end
25 25 end
26   -
... ...
spec/models/notice_spec.rb
1 1 require 'spec_helper'
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)
8 9 notice.should_not be_valid
9 10 notice.errors[:backtrace].should include("can't be blank")
10 11 end
11   -
  12 +
12 13 it 'requires the server_environment' do
13 14 notice = Factory.build(:notice, :server_environment => nil)
14 15 notice.should_not be_valid
15 16 notice.errors[:server_environment].should include("can't be blank")
16 17 end
17   -
  18 +
18 19 it 'requires the notifier' do
19 20 notice = Factory.build(:notice, :notifier => nil)
20 21 notice.should_not be_valid
21 22 notice.errors[:notifier].should include("can't be blank")
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),
... ... @@ -50,22 +52,23 @@ describe Notice do
50 52 Notice.in_app_backtrace_line?(backtrace[2]).should == true
51 53 end
52 54 end
53   -
  55 +
  56 +
54 57 context '#from_xml' do
55 58 before do
56 59 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
57 60 @app = Factory(:app, :api_key => 'APIKEY')
58 61 Digest::MD5.stub(:hexdigest).and_return('fingerprintdigest')
59 62 end
60   -
  63 +
61 64 it 'finds the correct app' do
62 65 @notice = Notice.from_xml(@xml)
63 66 @notice.err.app.should == @app
64 67 end
65   -
  68 +
66 69 it 'finds the correct err for the notice' do
67   - Err.should_receive(:for).with({
68   - :app => @app,
  70 + App.should_receive(:find_by_api_key!).and_return(@app)
  71 + @app.should_receive(:find_or_create_err!).with({
69 72 :klass => 'HoptoadTestingException',
70 73 :component => 'application',
71 74 :action => 'verify',
... ... @@ -75,70 +78,72 @@ describe Notice do
75 78 err.notices.stub(:create!)
76 79 @notice = Notice.from_xml(@xml)
77 80 end
78   -
79   - it 'marks the err as unresolve if it was previously resolved' do
80   - Err.should_receive(:for).with({
81   - :app => @app,
  81 +
  82 +
  83 + it 'marks the err as unresolved if it was previously resolved' do
  84 + App.should_receive(:find_by_api_key!).and_return(@app)
  85 + @app.should_receive(:find_or_create_err!).with({
82 86 :klass => 'HoptoadTestingException',
83 87 :component => 'application',
84 88 :action => 'verify',
85 89 :environment => 'development',
86 90 :fingerprint => 'fingerprintdigest'
87   - }).and_return(err = Factory(:err, :resolved => true))
  91 + }).and_return(err = Factory(:err, :problem => Factory(:problem, :resolved => true)))
88 92 err.should be_resolved
89 93 @notice = Notice.from_xml(@xml)
90 94 @notice.err.should == err
91 95 @notice.err.should_not be_resolved
92 96 end
93   -
  97 +
94 98 it 'should create a new notice' do
95 99 @notice = Notice.from_xml(@xml)
96 100 @notice.should be_persisted
97 101 end
98   -
  102 +
99 103 it 'assigns an err to the notice' do
100 104 @notice = Notice.from_xml(@xml)
101 105 @notice.err.should be_a(Err)
102 106 end
103   -
  107 +
104 108 it 'captures the err message' do
105 109 @notice = Notice.from_xml(@xml)
106 110 @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
107 111 end
108   -
  112 +
109 113 it 'captures the backtrace' do
110 114 @notice = Notice.from_xml(@xml)
111 115 @notice.backtrace.size.should == 73
112 116 @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake'
113 117 end
114   -
  118 +
115 119 it 'captures the server_environment' do
116 120 @notice = Notice.from_xml(@xml)
117 121 @notice.server_environment['environment-name'].should == 'development'
118 122 end
119   -
  123 +
120 124 it 'captures the request' do
121 125 @notice = Notice.from_xml(@xml)
122 126 @notice.request['url'].should == 'http://example.org/verify'
123 127 @notice.request['params']['controller'].should == 'application'
124 128 end
125   -
  129 +
126 130 it 'captures the notifier' do
127 131 @notice = Notice.from_xml(@xml)
128 132 @notice.notifier['name'].should == 'Hoptoad Notifier'
129 133 end
130   -
131   - it "should handle params without 'request' section" do
  134 +
  135 + it "should handle params withour 'request' section" do
132 136 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read
133 137 lambda { Notice.from_xml(@xml) }.should_not raise_error
134 138 end
135   -
  139 +
136 140 it "should raise ApiVersionError" do
137 141 @xml = Rails.root.join('spec', 'fixtures', 'hoptoad_test_notice_with_wrong_version.xml').read
138 142 expect { Notice.from_xml(@xml) }.to raise_error(Hoptoad::V2::ApiVersionError)
139 143 end
140 144 end
141   -
  145 +
  146 +
142 147 describe "key sanitization" do
143 148 before do
144 149 @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}}
... ... @@ -152,42 +157,44 @@ describe Notice do
152 157 end
153 158 end
154 159 end
155   -
  160 +
  161 +
156 162 describe "user agent" do
157 163 it "should be parsed and human-readable" do
158 164 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'}})
159 165 notice.user_agent.browser.should == 'Chrome'
160 166 notice.user_agent.version.to_s.should =~ /^10\.0/
161 167 end
162   -
  168 +
163 169 it "should be nil if HTTP_USER_AGENT is blank" do
164 170 notice = Factory.build(:notice)
165 171 notice.user_agent.should == nil
166 172 end
167 173 end
168   -
  174 +
  175 +
169 176 describe "email notifications (configured individually for each app)" do
170 177 custom_thresholds = [2, 4, 8, 16, 32, 64]
171   -
  178 +
172 179 before do
173 180 Errbit::Config.per_app_email_at_notices = true
174 181 @app = Factory(:app_with_watcher, :email_at_notices => custom_thresholds)
175   - @err = Factory(:err, :app => @app)
  182 + @problem = Factory(:err, :problem => @app.problems.create!)
176 183 end
177   -
  184 +
178 185 after do
179 186 Errbit::Config.per_app_email_at_notices = false
180 187 end
181   -
  188 +
182 189 custom_thresholds.each do |threshold|
183 190 it "sends an email notification after #{threshold} notice(s)" do
184   - @err.notices.stub(:count).and_return(threshold)
  191 + @problem.notices.stub(:count).and_return(threshold)
185 192 Mailer.should_receive(:err_notification).
186 193 and_return(mock('email', :deliver => true))
187   - Factory(:notice, :err => @err)
  194 + Factory(:notice, :err => @problem)
188 195 end
189 196 end
190 197 end
191   -
  198 +
  199 +
192 200 end
193   -
... ...
spec/models/problem_spec.rb 0 → 100644
... ... @@ -0,0 +1,130 @@
  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 "Scopes" do
  82 + context "resolved" do
  83 + it 'only finds resolved Problems' do
  84 + resolved = Factory(:problem, :resolved => true)
  85 + unresolved = Factory(:problem, :resolved => false)
  86 + Problem.resolved.all.should include(resolved)
  87 + Problem.resolved.all.should_not include(unresolved)
  88 + end
  89 + end
  90 +
  91 + context "unresolved" do
  92 + it 'only finds unresolved Problems' do
  93 + resolved = Factory(:problem, :resolved => true)
  94 + unresolved = Factory(:problem, :resolved => false)
  95 + Problem.unresolved.all.should_not include(resolved)
  96 + Problem.unresolved.all.should include(unresolved)
  97 + end
  98 + end
  99 + end
  100 +
  101 +
  102 + context "notice counter cache" do
  103 +
  104 + before do
  105 + @app = Factory(:app)
  106 + @problem = Factory(:problem, :app => @app)
  107 + @err = Factory(:err, :problem => @problem)
  108 + end
  109 +
  110 + it "#notices_count returns 0 by default" do
  111 + @problem.notices_count.should == 0
  112 + end
  113 +
  114 + it "adding a notice increases #notices_count by 1" do
  115 + lambda {
  116 + Factory(:notice, :err => @err, :message => 'ERR 1')
  117 + }.should change(@problem, :notices_count).from(0).to(1)
  118 + end
  119 +
  120 + it "removing a notice decreases #notices_count by 1" do
  121 + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')
  122 + lambda {
  123 + @err.notices.first.destroy
  124 + @problem.reload
  125 + }.should change(@problem, :notices_count).from(1).to(0)
  126 + end
  127 + end
  128 +
  129 +
  130 +end
... ...
spec/views/errs/show.html.haml_spec.rb
... ... @@ -3,23 +3,24 @@ 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 +
14 15 describe "content_for :action_bar" do
15   -
  16 +
16 17 it "should confirm the 'resolve' link by default" do
17 18 render
18 19 action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar])
19 20 resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0]
20 21 resolve_link.should =~ /data-confirm="Seriously\?"/
21 22 end
22   -
  23 +
23 24 it "should confirm the 'resolve' link if configuration is unset" do
24 25 Errbit::Config.stub(:confirm_resolve_err).and_return(nil)
25 26 render
... ... @@ -27,7 +28,7 @@ describe &quot;errs/show.html.haml&quot; do
27 28 resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0]
28 29 resolve_link.should =~ /data-confirm="Seriously\?"/
29 30 end
30   -
  31 +
31 32 it "should not confirm the 'resolve' link if configured not to" do
32 33 Errbit::Config.stub(:confirm_resolve_err).and_return(false)
33 34 render
... ... @@ -35,35 +36,37 @@ describe &quot;errs/show.html.haml&quot; do
35 36 resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0]
36 37 resolve_link.should_not =~ /data-confirm=/
37 38 end
38   -
  39 +
39 40 end
40   -
  41 +
41 42 describe "content_for :comments" do
42 43 it 'should display comments and new comment form when no issue tracker' do
43   - err = Factory(:err_with_comments)
44   - assign :err, err
45   - assign :app, err.app
  44 + problem = Factory(:problem_with_comments)
  45 + assign :problem, problem
  46 + assign :app, problem.app
46 47 render
47 48 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
48 49 comments_section.should =~ /Test comment/
49 50 comments_section.should =~ /Add a comment/
50 51 end
51   -
  52 +
52 53 context "with issue tracker" do
53   - def with_issue_tracker(err)
54   - err.app.issue_tracker = PivotalLabsTracker.new :api_token => "token token token", :project_id => "1234"
55   - assign :err, err
56   - assign :app, err.app
  54 + def with_issue_tracker(problem)
  55 + problem.app.issue_tracker = PivotalLabsTracker.new :api_token => "token token token", :project_id => "1234"
  56 + assign :problem, problem
  57 + assign :app, problem.app
57 58 end
58   -
  59 +
59 60 it 'should not display the comments section' do
60   - with_issue_tracker(Factory(:err))
  61 + problem = Factory(:problem)
  62 + with_issue_tracker(problem)
61 63 render
62 64 view.instance_variable_get(:@_content_for)[:comments].should be_blank
63 65 end
64   -
  66 +
65 67 it 'should display existing comments' do
66   - with_issue_tracker(Factory(:err_with_comments))
  68 + problem = Factory(:problem_with_comments)
  69 + with_issue_tracker(problem)
67 70 render
68 71 comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments])
69 72 comments_section.should =~ /Test comment/
... ... @@ -72,4 +75,3 @@ describe &quot;errs/show.html.haml&quot; do
72 75 end
73 76 end
74 77 end
75   -
... ...