Commit 40453cc3539b4c689aea482959a7f126727dfec8

Authored by Karol Hosiawa
Committed by Karol Hosiawa
1 parent bd17fa78
Exists in master and in 1 other branch production

moved Notices to a separate collection

@@ -7,3 +7,4 @@ config/deploy.rb @@ -7,3 +7,4 @@ config/deploy.rb
7 config/mongoid.yml 7 config/mongoid.yml
8 .rvmrc 8 .rvmrc
9 *~ 9 *~
  10 +*.rbc
@@ -2,12 +2,13 @@ source 'http://rubygems.org' @@ -2,12 +2,13 @@ source 'http://rubygems.org'
2 2
3 gem 'rails', '3.0.5' 3 gem 'rails', '3.0.5'
4 gem 'nokogiri' 4 gem 'nokogiri'
5 -gem 'mongoid', '~> 2.0.0.rc.7' 5 +gem 'mongoid', '2.0.0.rc.8'
6 gem 'haml' 6 gem 'haml'
7 gem 'will_paginate' 7 gem 'will_paginate'
8 gem 'devise', '~> 1.1.8' 8 gem 'devise', '~> 1.1.8'
9 gem 'lighthouse-api' 9 gem 'lighthouse-api'
10 gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" 10 gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git"
  11 +gem 'mongoid_rails_migrations'
11 12
12 platform :ruby do 13 platform :ruby do
13 gem 'bson_ext', '~> 1.2' 14 gem 'bson_ext', '~> 1.2'
@@ -35,15 +35,15 @@ GEM @@ -35,15 +35,15 @@ GEM
35 activemodel (= 3.0.5) 35 activemodel (= 3.0.5)
36 activesupport (= 3.0.5) 36 activesupport (= 3.0.5)
37 activesupport (3.0.5) 37 activesupport (3.0.5)
38 - addressable (2.2.4) 38 + addressable (2.2.5)
39 arel (2.0.9) 39 arel (2.0.9)
40 bcrypt-ruby (2.1.4) 40 bcrypt-ruby (2.1.4)
41 - bson (1.2.4)  
42 - bson_ext (1.2.4) 41 + bson (1.3.0)
  42 + bson_ext (1.3.0)
43 builder (2.1.2) 43 builder (2.1.2)
44 crack (0.1.8) 44 crack (0.1.8)
45 - database_cleaner (0.6.5)  
46 - devise (1.1.8) 45 + database_cleaner (0.6.7)
  46 + devise (1.1.9)
47 bcrypt-ruby (~> 2.1.2) 47 bcrypt-ruby (~> 2.1.2)
48 warden (~> 1.0.2) 48 warden (~> 1.0.2)
49 diff-lcs (1.1.2) 49 diff-lcs (1.1.2)
@@ -58,23 +58,28 @@ GEM @@ -58,23 +58,28 @@ GEM
58 lighthouse-api (2.0) 58 lighthouse-api (2.0)
59 activeresource (>= 3.0.0) 59 activeresource (>= 3.0.0)
60 activesupport (>= 3.0.0) 60 activesupport (>= 3.0.0)
61 - mail (2.2.15) 61 + mail (2.2.17)
62 activesupport (>= 2.3.6) 62 activesupport (>= 2.3.6)
63 i18n (>= 0.4.0) 63 i18n (>= 0.4.0)
64 mime-types (~> 1.16) 64 mime-types (~> 1.16)
65 treetop (~> 1.4.8) 65 treetop (~> 1.4.8)
66 mime-types (1.16) 66 mime-types (1.16)
67 - mongo (1.2.4)  
68 - bson (>= 1.2.4)  
69 - mongoid (2.0.0.rc.7) 67 + mongo (1.3.0)
  68 + bson (>= 1.3.0)
  69 + mongoid (2.0.0.rc.8)
70 activemodel (~> 3.0) 70 activemodel (~> 3.0)
71 mongo (~> 1.2) 71 mongo (~> 1.2)
72 tzinfo (~> 0.3.22) 72 tzinfo (~> 0.3.22)
73 will_paginate (~> 3.0.pre) 73 will_paginate (~> 3.0.pre)
  74 + mongoid_rails_migrations (0.0.10)
  75 + activesupport (~> 3.0.0)
  76 + bundler (>= 0.9.19)
  77 + rails (~> 3.0.0)
  78 + railties (~> 3.0.0)
74 nokogiri (1.4.4) 79 nokogiri (1.4.4)
75 polyglot (0.3.1) 80 polyglot (0.3.1)
76 rack (1.2.2) 81 rack (1.2.2)
77 - rack-mount (0.6.13) 82 + rack-mount (0.6.14)
78 rack (>= 1.0.0) 83 rack (>= 1.0.0)
79 rack-test (0.5.7) 84 rack-test (0.5.7)
80 rack (>= 1.0) 85 rack (>= 1.0)
@@ -108,7 +113,7 @@ GEM @@ -108,7 +113,7 @@ GEM
108 thor (0.14.6) 113 thor (0.14.6)
109 treetop (1.4.9) 114 treetop (1.4.9)
110 polyglot (>= 0.3.1) 115 polyglot (>= 0.3.1)
111 - tzinfo (0.3.25) 116 + tzinfo (0.3.26)
112 warden (1.0.3) 117 warden (1.0.3)
113 rack (>= 1.0.0) 118 rack (>= 1.0.0)
114 webmock (1.6.2) 119 webmock (1.6.2)
@@ -126,7 +131,8 @@ DEPENDENCIES @@ -126,7 +131,8 @@ DEPENDENCIES
126 factory_girl_rails 131 factory_girl_rails
127 haml 132 haml
128 lighthouse-api 133 lighthouse-api
129 - mongoid (~> 2.0.0.rc.7) 134 + mongoid (= 2.0.0.rc.8)
  135 + mongoid_rails_migrations
130 nokogiri 136 nokogiri
131 rails (= 3.0.5) 137 rails (= 3.0.5)
132 redmine_client! 138 redmine_client!
@@ -89,17 +89,24 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at @@ -89,17 +89,24 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at
89 89
90 4. Enjoy! 90 4. Enjoy!
91 91
  92 +Upgrading
  93 +---------
  94 +*Note*: If upgrading from a version of Errbit that used Notices embedded in Errs please run:
  95 +
  96 + 1. git pull origin master ( assuming origin is the github.com/jdpace/errbit repo )
  97 + 2. rake db:migrate
  98 +
92 Lighthouseapp integration 99 Lighthouseapp integration
93 ------------------------- 100 -------------------------
94 101
95 -* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview 102 +* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
96 * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it. 103 * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it.
97 * Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview 104 * Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
98 105
99 Redmine integration 106 Redmine integration
100 ------------------------- 107 -------------------------
101 108
102 -* Account is the host of your redmine installation, i.e. **http://redmine.org** 109 +* Account is the host of your redmine installation, i.e. **http://redmine.org**
103 * Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it. 110 * Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it.
104 * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject 111 * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject
105 112
app/controllers/apps_controller.rb
1 class AppsController < ApplicationController 1 class AppsController < ApplicationController
2 - 2 +
3 before_filter :require_admin!, :except => [:index, :show] 3 before_filter :require_admin!, :except => [:index, :show]
4 before_filter :find_app, :except => [:index, :new, :create] 4 before_filter :find_app, :except => [:index, :new, :create]
5 - 5 +
6 def index 6 def index
7 @apps = current_user.admin? ? App.all : current_user.apps.all 7 @apps = current_user.admin? ? App.all : current_user.apps.all
8 end 8 end
9 - 9 +
10 def show 10 def show
11 respond_to do |format| 11 respond_to do |format|
12 format.html do 12 format.html do
@@ -18,21 +18,21 @@ class AppsController &lt; ApplicationController @@ -18,21 +18,21 @@ class AppsController &lt; ApplicationController
18 end 18 end
19 end 19 end
20 end 20 end
21 - 21 +
22 def new 22 def new
23 @app = App.new 23 @app = App.new
24 @app.watchers.build 24 @app.watchers.build
25 @app.issue_tracker = IssueTracker.new 25 @app.issue_tracker = IssueTracker.new
26 end 26 end
27 - 27 +
28 def edit 28 def edit
29 @app.watchers.build if @app.watchers.none? 29 @app.watchers.build if @app.watchers.none?
30 @app.issue_tracker = IssueTracker.new if @app.issue_tracker.nil? 30 @app.issue_tracker = IssueTracker.new if @app.issue_tracker.nil?
31 end 31 end
32 - 32 +
33 def create 33 def create
34 @app = App.new(params[:app]) 34 @app = App.new(params[:app])
35 - 35 +
36 if @app.save 36 if @app.save
37 flash[:success] = 'Great success! Configure your app with the API key below' 37 flash[:success] = 'Great success! Configure your app with the API key below'
38 redirect_to app_path(@app) 38 redirect_to app_path(@app)
@@ -40,8 +40,8 @@ class AppsController &lt; ApplicationController @@ -40,8 +40,8 @@ class AppsController &lt; ApplicationController
40 render :new 40 render :new
41 end 41 end
42 end 42 end
43 -  
44 - def update 43 +
  44 + def update
45 if @app.update_attributes(params[:app]) 45 if @app.update_attributes(params[:app])
46 flash[:success] = "Good news everyone! '#{@app.name}' was successfully updated." 46 flash[:success] = "Good news everyone! '#{@app.name}' was successfully updated."
47 redirect_to app_path(@app) 47 redirect_to app_path(@app)
@@ -49,18 +49,18 @@ class AppsController &lt; ApplicationController @@ -49,18 +49,18 @@ class AppsController &lt; ApplicationController
49 render :edit 49 render :edit
50 end 50 end
51 end 51 end
52 - 52 +
53 def destroy 53 def destroy
54 @app.destroy 54 @app.destroy
55 flash[:success] = "'#{@app.name}' was successfully destroyed." 55 flash[:success] = "'#{@app.name}' was successfully destroyed."
56 redirect_to apps_path 56 redirect_to apps_path
57 end 57 end
58 - 58 +
59 protected 59 protected
60 - 60 +
61 def find_app 61 def find_app
62 @app = App.find(params[:id]) 62 @app = App.find(params[:id])
63 - 63 +
64 # Mongoid Bug: could not chain: current_user.apps.find_by_id! 64 # Mongoid Bug: could not chain: current_user.apps.find_by_id!
65 # apparently finding by 'watchers.email' and 'id' is broken 65 # apparently finding by 'watchers.email' and 'id' is broken
66 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) 66 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
app/controllers/errs_controller.rb
1 class ErrsController < ApplicationController 1 class ErrsController < ApplicationController
2 - 2 +
3 before_filter :find_app, :except => [:index, :all] 3 before_filter :find_app, :except => [:index, :all]
4 before_filter :find_err, :except => [:index, :all] 4 before_filter :find_err, :except => [:index, :all]
5 - 5 +
6 def index 6 def index
7 app_scope = current_user.admin? ? App.all : current_user.apps 7 app_scope = current_user.admin? ? App.all : current_user.apps
8 respond_to do |format| 8 respond_to do |format|
@@ -14,14 +14,14 @@ class ErrsController &lt; ApplicationController @@ -14,14 +14,14 @@ class ErrsController &lt; ApplicationController
14 end 14 end
15 end 15 end
16 end 16 end
17 - 17 +
18 def all 18 def all
19 app_scope = current_user.admin? ? App.all : current_user.apps 19 app_scope = current_user.admin? ? App.all : current_user.apps
20 @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page) 20 @errs = Err.for_apps(app_scope).ordered.paginate(:page => params[:page], :per_page => current_user.per_page)
21 end 21 end
22 - 22 +
23 def show 23 def show
24 - page = (params[:notice] || @err.notices.count) 24 + page = (params[:notice] || @err.notices_count)
25 page = 1 if page.to_i.zero? 25 page = 1 if page.to_i.zero?
26 @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1) 26 @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1)
27 @notice = @notices.first 27 @notice = @notices.first
@@ -46,25 +46,25 @@ class ErrsController &lt; ApplicationController @@ -46,25 +46,25 @@ class ErrsController &lt; ApplicationController
46 @err.update_attribute :issue_link, nil 46 @err.update_attribute :issue_link, nil
47 redirect_to app_err_path(@app, @err) 47 redirect_to app_err_path(@app, @err)
48 end 48 end
49 - 49 +
50 def resolve 50 def resolve
51 - # Deal with bug in mogoid where find is returning an Enumberable obj 51 + # Deal with bug in mongoid where find is returning an Enumberable obj
52 @err = @err.first if @err.respond_to?(:first) 52 @err = @err.first if @err.respond_to?(:first)
53 - 53 +
54 @err.resolve! 54 @err.resolve!
55 - 55 +
56 flash[:success] = 'Great news everyone! The err has been resolved.' 56 flash[:success] = 'Great news everyone! The err has been resolved.'
57 57
58 redirect_to :back 58 redirect_to :back
59 rescue ActionController::RedirectBackError 59 rescue ActionController::RedirectBackError
60 redirect_to app_path(@app) 60 redirect_to app_path(@app)
61 end 61 end
62 - 62 +
63 protected 63 protected
64 - 64 +
65 def find_app 65 def find_app
66 @app = App.find(params[:app_id]) 66 @app = App.find(params[:app_id])
67 - 67 +
68 # Mongoid Bug: could not chain: current_user.apps.find_by_id! 68 # Mongoid Bug: could not chain: current_user.apps.find_by_id!
69 # apparently finding by 'watchers.email' and 'id' is broken 69 # apparently finding by 'watchers.email' and 'id' is broken
70 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) 70 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
@@ -79,5 +79,5 @@ class ErrsController &lt; ApplicationController @@ -79,5 +79,5 @@ class ErrsController &lt; ApplicationController
79 IssueTracker.default_url_options[:port] = request.port 79 IssueTracker.default_url_options[:port] = request.port
80 IssueTracker.default_url_options[:protocol] = request.scheme 80 IssueTracker.default_url_options[:protocol] = request.scheme
81 end 81 end
82 - 82 +
83 end 83 end
app/models/app.rb
1 class App 1 class App
2 include Mongoid::Document 2 include Mongoid::Document
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
4 - 4 +
5 field :name, :type => String 5 field :name, :type => String
6 field :api_key 6 field :api_key
7 field :resolve_errs_on_deploy, :type => Boolean, :default => false 7 field :resolve_errs_on_deploy, :type => Boolean, :default => false
@@ -21,29 +21,29 @@ class App @@ -21,29 +21,29 @@ class App
21 embeds_many :deploys 21 embeds_many :deploys
22 embeds_one :issue_tracker 22 embeds_one :issue_tracker
23 references_many :errs, :dependent => :destroy 23 references_many :errs, :dependent => :destroy
24 - 24 +
25 before_validation :generate_api_key, :on => :create 25 before_validation :generate_api_key, :on => :create
26 - 26 +
27 validates_presence_of :name, :api_key 27 validates_presence_of :name, :api_key
28 validates_uniqueness_of :name, :allow_blank => true 28 validates_uniqueness_of :name, :allow_blank => true
29 validates_uniqueness_of :api_key, :allow_blank => true 29 validates_uniqueness_of :api_key, :allow_blank => true
30 validates_associated :watchers 30 validates_associated :watchers
31 validate :check_issue_tracker 31 validate :check_issue_tracker
32 - 32 +
33 accepts_nested_attributes_for :watchers, :allow_destroy => true, 33 accepts_nested_attributes_for :watchers, :allow_destroy => true,
34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } 34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, 35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
36 :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } 36 :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) }
37 - 37 +
38 # Mongoid Bug: find(id) on association proxies returns an Enumerator 38 # Mongoid Bug: find(id) on association proxies returns an Enumerator
39 def self.find_by_id!(app_id) 39 def self.find_by_id!(app_id)
40 where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id)) 40 where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id))
41 end 41 end
42 - 42 +
43 def self.find_by_api_key!(key) 43 def self.find_by_api_key!(key)
44 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key)) 44 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key))
45 end 45 end
46 - 46 +
47 def last_deploy_at 47 def last_deploy_at
48 deploys.last && deploys.last.created_at 48 deploys.last && deploys.last.created_at
49 end 49 end
@@ -58,9 +58,9 @@ class App @@ -58,9 +58,9 @@ class App
58 !(self[:notify_on_deploys] == false) 58 !(self[:notify_on_deploys] == false)
59 end 59 end
60 alias :notify_on_deploys? :notify_on_deploys 60 alias :notify_on_deploys? :notify_on_deploys
61 - 61 +
62 protected 62 protected
63 - 63 +
64 def generate_api_key 64 def generate_api_key
65 self.api_key ||= ActiveSupport::SecureRandom.hex 65 self.api_key ||= ActiveSupport::SecureRandom.hex
66 end 66 end
app/models/err.rb
1 class Err 1 class Err
2 include Mongoid::Document 2 include Mongoid::Document
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
4 - 4 +
5 field :klass 5 field :klass
6 field :component 6 field :component
7 field :action 7 field :action
@@ -10,42 +10,44 @@ class Err @@ -10,42 +10,44 @@ class Err
10 field :last_notice_at, :type => DateTime 10 field :last_notice_at, :type => DateTime
11 field :resolved, :type => Boolean, :default => false 11 field :resolved, :type => Boolean, :default => false
12 field :issue_link, :type => String 12 field :issue_link, :type => String
  13 + field :notices_count, :type => Integer, :default => 0
  14 + field :message
13 15
14 index :last_notice_at 16 index :last_notice_at
15 index :app_id 17 index :app_id
16 18
17 referenced_in :app 19 referenced_in :app
18 - embeds_many :notices  
19 - 20 + references_many :notices
  21 +
20 validates_presence_of :klass, :environment 22 validates_presence_of :klass, :environment
21 - 23 +
22 scope :resolved, where(:resolved => true) 24 scope :resolved, where(:resolved => true)
23 scope :unresolved, where(:resolved => false) 25 scope :unresolved, where(:resolved => false)
24 scope :ordered, order_by(:last_notice_at.desc) 26 scope :ordered, order_by(:last_notice_at.desc)
25 scope :in_env, lambda {|env| where(:environment => env)} 27 scope :in_env, lambda {|env| where(:environment => env)}
26 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} 28 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
27 - 29 +
28 def self.for(attrs) 30 def self.for(attrs)
29 app = attrs.delete(:app) 31 app = attrs.delete(:app)
30 app.errs.where(attrs).first || app.errs.create!(attrs) 32 app.errs.where(attrs).first || app.errs.create!(attrs)
31 end 33 end
32 - 34 +
33 def resolve! 35 def resolve!
34 self.update_attributes!(:resolved => true) 36 self.update_attributes!(:resolved => true)
35 end 37 end
36 - 38 +
37 def unresolved? 39 def unresolved?
38 !resolved? 40 !resolved?
39 end 41 end
40 - 42 +
41 def where 43 def where
42 where = component.dup 44 where = component.dup
43 where << "##{action}" if action.present? 45 where << "##{action}" if action.present?
44 where 46 where
45 end 47 end
46 - 48 +
47 def message 49 def message
48 - notices.first.try(:message) || klass 50 + super || klass
49 end 51 end
50 -  
51 -end  
52 \ No newline at end of file 52 \ No newline at end of file
  53 +
  54 +end
app/models/notice.rb
1 require 'hoptoad' 1 require 'hoptoad'
  2 +require 'recurse'
2 3
3 class Notice 4 class Notice
4 include Mongoid::Document 5 include Mongoid::Document
5 include Mongoid::Timestamps 6 include Mongoid::Timestamps
6 - 7 +
7 field :message 8 field :message
8 field :backtrace, :type => Array 9 field :backtrace, :type => Array
9 field :server_environment, :type => Hash 10 field :server_environment, :type => Hash
10 field :request, :type => Hash 11 field :request, :type => Hash
11 field :notifier, :type => Hash 12 field :notifier, :type => Hash
12 -  
13 - embedded_in :err, :inverse_of => :notices  
14 - 13 +
  14 + referenced_in :err
  15 + index :err_id
  16 +
15 after_create :cache_last_notice_at 17 after_create :cache_last_notice_at
16 after_create :deliver_notification, :if => :should_notify? 18 after_create :deliver_notification, :if => :should_notify?
17 - 19 + before_create :increase_counter_cache, :cache_message
  20 + before_save :sanitize
  21 + before_destroy :decrease_counter_cache
  22 +
18 validates_presence_of :backtrace, :server_environment, :notifier 23 validates_presence_of :backtrace, :server_environment, :notifier
19 - 24 +
20 scope :ordered, order_by(:created_at.asc) 25 scope :ordered, order_by(:created_at.asc)
21 - 26 +
22 def self.from_xml(hoptoad_xml) 27 def self.from_xml(hoptoad_xml)
23 hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml) 28 hoptoad_notice = Hoptoad::V2.parse_xml(hoptoad_xml)
24 app = App.find_by_api_key!(hoptoad_notice['api-key']) 29 app = App.find_by_api_key!(hoptoad_notice['api-key'])
25 - 30 +
26 hoptoad_notice['request'] ||= {} 31 hoptoad_notice['request'] ||= {}
27 hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank? 32 hoptoad_notice['request']['component'] = 'unknown' if hoptoad_notice['request']['component'].blank?
28 hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank? 33 hoptoad_notice['request']['action'] = nil if hoptoad_notice['request']['action'].blank?
29 - 34 +
30 err = Err.for({ 35 err = Err.for({
31 :app => app, 36 :app => app,
32 :klass => hoptoad_notice['error']['class'], 37 :klass => hoptoad_notice['error']['class'],
@@ -36,7 +41,7 @@ class Notice @@ -36,7 +41,7 @@ class Notice
36 :fingerprint => hoptoad_notice['fingerprint'] 41 :fingerprint => hoptoad_notice['fingerprint']
37 }) 42 })
38 err.update_attributes(:resolved => false) if err.resolved? 43 err.update_attributes(:resolved => false) if err.resolved?
39 - 44 +
40 err.notices.create!({ 45 err.notices.create!({
41 :message => hoptoad_notice['error']['message'], 46 :message => hoptoad_notice['error']['message'],
42 :backtrace => hoptoad_notice['error']['backtrace']['line'], 47 :backtrace => hoptoad_notice['error']['backtrace']['line'],
@@ -45,35 +50,67 @@ class Notice @@ -45,35 +50,67 @@ class Notice
45 :notifier => hoptoad_notice['notifier'] 50 :notifier => hoptoad_notice['notifier']
46 }) 51 })
47 end 52 end
48 - 53 +
49 def request 54 def request
50 read_attribute(:request) || {} 55 read_attribute(:request) || {}
51 end 56 end
52 - 57 +
53 def env_vars 58 def env_vars
54 request['cgi-data'] || {} 59 request['cgi-data'] || {}
55 end 60 end
56 - 61 +
57 def params 62 def params
58 request['params'] || {} 63 request['params'] || {}
59 end 64 end
60 - 65 +
61 def session 66 def session
62 request['session'] || {} 67 request['session'] || {}
63 end 68 end
64 - 69 +
65 def deliver_notification 70 def deliver_notification
66 Mailer.err_notification(self).deliver 71 Mailer.err_notification(self).deliver
67 end 72 end
68 - 73 +
69 def cache_last_notice_at 74 def cache_last_notice_at
70 err.update_attributes(:last_notice_at => created_at) 75 err.update_attributes(:last_notice_at => created_at)
71 end 76 end
72 - 77 +
73 protected 78 protected
74 -  
75 - def should_notify?  
76 - err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? 79 +
  80 + def should_notify?
  81 + err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any?
  82 + end
  83 +
  84 +
  85 + def increase_counter_cache
  86 + err.inc(:notices_count,1)
  87 + end
  88 +
  89 + def decrease_counter_cache
  90 + err.inc(:notices_count,-1)
  91 + end
  92 +
  93 + def cache_message
  94 + err.update_attribute(:message, message) if err.notices_count == 1
  95 + end
  96 +
  97 + def sanitize
  98 + [:server_environment, :request, :notifier].each do |h|
  99 + send("#{h}=",sanitize_hash(send(h)))
77 end 100 end
78 -  
79 -end  
80 \ No newline at end of file 101 \ No newline at end of file
  102 + end
  103 +
  104 + def sanitize_hash(h)
  105 + h.recurse do
  106 + |h| h.inject({}) do |h,(k,v)|
  107 + if k.is_a?(String)
  108 + h[k.gsub(/\./,'&#46;').gsub(/^\$/,'&#36;')] = v
  109 + else
  110 + h[k] = v
  111 + end
  112 + h
  113 + end
  114 + end
  115 + end
  116 +end
  117 +
app/views/apps/index.html.haml
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 %td.name= link_to app.name, app_path(app) 14 %td.name= link_to app.name, app_path(app)
15 %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro), app_deploys_path(app)) : 'n/a' 15 %td.deploy= app.last_deploy_at ? link_to( app.last_deploy_at.to_s(:micro), app_deploys_path(app)) : 'n/a'
16 %td.count 16 %td.count
17 - - if app.errs.any? 17 + - if app.errs.count > 0
18 = link_to app.errs.unresolved.count, app_errs_path(app) 18 = link_to app.errs.unresolved.count, app_errs_path(app)
19 - else 19 - else
20 \- 20 \-
app/views/apps/show.html.haml
@@ -50,7 +50,7 @@ @@ -50,7 +50,7 @@
50 - else 50 - else
51 %h3 No deploys 51 %h3 No deploys
52 52
53 -- if @app.errs.any? 53 +- if @app.errs.count > 0
54 %h3.clear Errs 54 %h3.clear Errs
55 = render 'errs/table', :errs => @errs 55 = render 'errs/table', :errs => @errs
56 - else 56 - else
app/views/errs/_table.html.haml
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 %em= err.where 18 %em= err.where
19 %td.latest #{time_ago_in_words(last_notice_at err)} ago 19 %td.latest #{time_ago_in_words(last_notice_at err)} ago
20 %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a' 20 %td.deploy= err.app.last_deploy_at ? err.app.last_deploy_at.to_s(:micro) : 'n/a'
21 - %td.count= link_to err.notices.count, app_err_path(err.app, err) 21 + %td.count= link_to err.notices_count, app_err_path(err.app, err)
22 %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? 22 %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?
23 - if errs.none? 23 - if errs.none?
24 %tr 24 %tr
app/views/errs/lighthouseapp_body.txt.erb
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 <%= notice.created_at.to_s(:micro) %> 13 <%= notice.created_at.to_s(:micro) %>
14 14
15 ### Similar ### 15 ### Similar ###
16 - <%= (notice.err.notices.count - 1).to_s %> 16 + <%= (notice.err.notices_count - 1).to_s %>
17 17
18 ## Params ## 18 ## Params ##
19 <code><%= pretty_hash(notice.params) %></code> 19 <code><%= pretty_hash(notice.params) %></code>
app/views/errs/redmine_body.txt.erb
@@ -18,7 +18,7 @@ h3. Occured @@ -18,7 +18,7 @@ h3. Occured
18 18
19 h3. Similar 19 h3. Similar
20 20
21 -<%= (notice.err.notices.count - 1).to_s %> 21 +<%= (notice.err.notices_count - 1).to_s %>
22 22
23 h2. Params 23 h2. Params
24 24
app/views/errs/show.html.haml
@@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
20 %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' 20 %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve'
21 21
22 %h4= @notice.try(:message) 22 %h4= @notice.try(:message)
23 - 23 +
24 = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' 24 = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination'
25 viewing occurrence #{@notices.current_page} of #{@notices.total_pages} 25 viewing occurrence #{@notices.current_page} of #{@notices.total_pages}
26 26
@@ -36,19 +36,19 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages} @@ -36,19 +36,19 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages}
36 #summary 36 #summary
37 %h3 Summary 37 %h3 Summary
38 = render 'notices/summary', :notice => @notice 38 = render 'notices/summary', :notice => @notice
39 - 39 +
40 #backtrace 40 #backtrace
41 %h3 Backtrace 41 %h3 Backtrace
42 = render 'notices/backtrace', :lines => @notice.backtrace 42 = render 'notices/backtrace', :lines => @notice.backtrace
43 - 43 +
44 #environment 44 #environment
45 %h3 Environment 45 %h3 Environment
46 = render 'notices/environment', :notice => @notice 46 = render 'notices/environment', :notice => @notice
47 - 47 +
48 #params 48 #params
49 %h3 Parameters 49 %h3 Parameters
50 = render 'notices/params', :notice => @notice 50 = render 'notices/params', :notice => @notice
51 - 51 +
52 #session 52 #session
53 %h3 Session 53 %h3 Session
54 = render 'notices/session', :notice => @notice 54 = render 'notices/session', :notice => @notice
app/views/mailer/err_notification.text.erb
1 An err has just occurred in <%= @notice.err.environment %>: <%= @notice.err.message %> 1 An err has just occurred in <%= @notice.err.environment %>: <%= @notice.err.message %>
2 2
3 -This err has occurred <%= pluralize @notice.err.notices.count, 'time' %>. You should really look into it here: 3 +This err has occurred <%= pluralize @notice.err.notices_count, 'time' %>. You should really look into it here:
4 4
5 <%= app_err_url(@app, @notice.err) %> 5 <%= app_err_url(@app, @notice.err) %>
6 -  
7 -<%= render :partial => 'signature' %>  
8 \ No newline at end of file 6 \ No newline at end of file
  7 +
  8 +<%= render :partial => 'signature' %>
app/views/notices/_atom_entry.html.haml
@@ -6,13 +6,13 @@ @@ -6,13 +6,13 @@
6 = link_to(notice.request['url'], notice.request['url']) 6 = link_to(notice.request['url'], notice.request['url'])
7 %p 7 %p
8 %strong Where: 8 %strong Where:
9 - = notice.err.where 9 + = notice.err.where
10 %p 10 %p
11 %strong Occured: 11 %strong Occured:
12 = notice.created_at.to_s(:micro) 12 = notice.created_at.to_s(:micro)
13 %p 13 %p
14 %strong Similar: 14 %strong Similar:
15 - = notice.err.notices.count - 1 15 + = notice.err.notices_count - 1
16 16
17 %h3 Params 17 %h3 Params
18 %p= pretty_hash(notice.params) 18 %p= pretty_hash(notice.params)
app/views/notices/_environment.html.haml
@@ -2,5 +2,5 @@ @@ -2,5 +2,5 @@
2 %table.environment 2 %table.environment
3 - notice.env_vars.each do |key,val| 3 - notice.env_vars.each do |key,val|
4 %tr 4 %tr
5 - %th= key 5 + %th= raw key
6 %td.main= val 6 %td.main= val
7 \ No newline at end of file 7 \ No newline at end of file
app/views/notices/_summary.html.haml
@@ -15,4 +15,4 @@ @@ -15,4 +15,4 @@
15 %td= notice.created_at.to_s(:micro) 15 %td= notice.created_at.to_s(:micro)
16 %tr 16 %tr
17 %th Similar 17 %th Similar
18 - %td= notice.err.notices.count - 1  
19 \ No newline at end of file 18 \ No newline at end of file
  19 + %td= notice.err.notices_count - 1
20 \ No newline at end of file 20 \ No newline at end of file
db/migrate/20110422152027_move_notices_to_separate_collection.rb 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +class MoveNoticesToSeparateCollection < Mongoid::Migration
  2 + def self.up
  3 + # copy embedded Notices into a separate collection
  4 + mongo_db = Err.db
  5 + errs = mongo_db.collection("errs").find({ }, :fields => ["notices"])
  6 + errs.each do |err|
  7 + next unless err['notices']
  8 + e = Err.find(err['_id'])
  9 + puts "Copying notices for Err #{err['_id']}"
  10 + err['notices'].each do |notice|
  11 + e.notices.create!(notice)
  12 + end
  13 + mongo_db.collection("errs").update({ "_id" => err['_id']}, { "$unset" => { "notices" => 1}})
  14 + end
  15 + Rake::Task["errbit:db:update_notices_count"].invoke
  16 + Rake::Task["errbit:db:update_err_message"].invoke
  17 + end
  18 +
  19 + def self.down
  20 + end
  21 +
  22 +end
lib/hoptoad.rb
1 module Hoptoad 1 module Hoptoad
2 module V2 2 module V2
3 require 'digest/md5' 3 require 'digest/md5'
4 - 4 +
5 class ApiVersionError < StandardError 5 class ApiVersionError < StandardError
6 def initialize 6 def initialize
7 super "Wrong API Version: Expecting v2.0" 7 super "Wrong API Version: Expecting v2.0"
8 end 8 end
9 end 9 end
10 - 10 +
11 def self.parse_xml(xml) 11 def self.parse_xml(xml)
12 parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice'] 12 parsed = ActiveSupport::XmlMini.backend.parse(xml)['notice']
13 raise ApiVersionError unless parsed && parsed['version'] == '2.0' 13 raise ApiVersionError unless parsed && parsed['version'] == '2.0'
@@ -15,9 +15,9 @@ module Hoptoad @@ -15,9 +15,9 @@ module Hoptoad
15 rekeyed['fingerprint'] = Digest::MD5.hexdigest(rekeyed['error']['backtrace'].to_s) 15 rekeyed['fingerprint'] = Digest::MD5.hexdigest(rekeyed['error']['backtrace'].to_s)
16 rekeyed 16 rekeyed
17 end 17 end
18 - 18 +
19 private 19 private
20 - 20 +
21 def self.rekey(node) 21 def self.rekey(node)
22 if node.is_a?(Hash) && node.has_key?('var') && node.has_key?('key') 22 if node.is_a?(Hash) && node.has_key?('var') && node.has_key?('key')
23 {node['key'] => rekey(node['var'])} 23 {node['key'] => rekey(node['var'])}
@@ -42,4 +42,4 @@ module Hoptoad @@ -42,4 +42,4 @@ module Hoptoad
42 end 42 end
43 end 43 end
44 end 44 end
45 -end  
46 \ No newline at end of file 45 \ No newline at end of file
  46 +end
lib/recurse.rb 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +class Hash
  2 +
  3 + # Apply a block to hash, and recursively apply that block
  4 + # to each sub-hash or +types+.
  5 + #
  6 + # h = {:a=>1, :b=>{:b1=>1, :b2=>2}}
  7 + # g = h.recurse{|h| h.inject({}){|h,(k,v)| h[k.to_s] = v; h} }
  8 + # g #=> {"a"=>1, "b"=>{"b1"=>1, "b2"=>2}}
  9 + #
  10 + def recurse(*types, &block)
  11 + types = [self.class] if types.empty?
  12 + h = inject({}) do |hash, (key, value)|
  13 + case value
  14 + when *types
  15 + hash[key] = value.recurse(*types, &block)
  16 + else
  17 + hash[key] = value
  18 + end
  19 + hash
  20 + end
  21 + yield h
  22 + end
  23 +
  24 +end
lib/tasks/errbit/err_message.rake 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +namespace :errbit do
  2 +
  3 + namespace :db do
  4 + desc "Updates Err#notices_count"
  5 + task :update_err_message => :environment do
  6 + puts "Updating err.message"
  7 + Err.all.each do |e|
  8 + e.update_attributes(:message => e.notices.first.message) if e.notices.first
  9 + end
  10 + end
  11 + end
  12 +end
lib/tasks/errbit/notices_counter.rake 0 → 100644
@@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
  1 +namespace :errbit do
  2 +
  3 + namespace :db do
  4 + desc "Updates Err#notices_count"
  5 + task :update_notices_count => :environment do
  6 + puts "Updating err.notices_count"
  7 + Err.all.each do |e|
  8 + e.update_attributes(:notices_count => e.notices.count)
  9 + end
  10 + end
  11 + end
  12 +end
spec/controllers/errs_controller_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe ErrsController do 3 describe ErrsController do
4 - 4 +
5 it_requires_authentication :for => { 5 it_requires_authentication :for => {
6 :index => :get, :all => :get, :show => :get, :resolve => :put 6 :index => :get, :all => :get, :show => :get, :resolve => :put
7 }, 7 },
8 :params => {:app_id => 'dummyid', :id => 'dummyid'} 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9 - 9 +
10 let(:app) { Factory(:app) } 10 let(:app) { Factory(:app) }
11 let(:err) { Factory(:err, :app => app) } 11 let(:err) { Factory(:err, :app => app) }
12 - 12 +
13 describe "GET /errs" do 13 describe "GET /errs" do
14 render_views 14 render_views
15 context 'when logged in as an admin' do 15 context 'when logged in as an admin' do
@@ -31,7 +31,7 @@ describe ErrsController do @@ -31,7 +31,7 @@ describe ErrsController do
31 response.should be_success 31 response.should be_success
32 response.body.should match(@err.message) 32 response.body.should match(@err.message)
33 end 33 end
34 - 34 +
35 it "should handle lots of errors" do 35 it "should handle lots of errors" do
36 pending "Turning off long running spec" 36 pending "Turning off long running spec"
37 1000.times { Factory :notice } 37 1000.times { Factory :notice }
@@ -55,7 +55,7 @@ describe ErrsController do @@ -55,7 +55,7 @@ describe ErrsController do
55 end 55 end
56 end 56 end
57 end 57 end
58 - 58 +
59 context 'when logged in as a user' do 59 context 'when logged in as a user' do
60 it 'gets a paginated list of unresolved errs for the users apps' do 60 it 'gets a paginated list of unresolved errs for the users apps' do
61 sign_in(user = Factory(:user)) 61 sign_in(user = Factory(:user))
@@ -68,7 +68,7 @@ describe ErrsController do @@ -68,7 +68,7 @@ describe ErrsController do
68 end 68 end
69 end 69 end
70 end 70 end
71 - 71 +
72 describe "GET /errs/all" do 72 describe "GET /errs/all" do
73 context 'when logged in as an admin' do 73 context 'when logged in as an admin' do
74 it "gets a paginated list of all errs" do 74 it "gets a paginated list of all errs" do
@@ -83,7 +83,7 @@ describe ErrsController do @@ -83,7 +83,7 @@ describe ErrsController do
83 assigns(:errs).should == errs 83 assigns(:errs).should == errs
84 end 84 end
85 end 85 end
86 - 86 +
87 context 'when logged in as a user' do 87 context 'when logged in as a user' do
88 it 'gets a paginated list of all errs for the users apps' do 88 it 'gets a paginated list of all errs for the users apps' do
89 sign_in(user = Factory(:user)) 89 sign_in(user = Factory(:user))
@@ -96,29 +96,29 @@ describe ErrsController do @@ -96,29 +96,29 @@ describe ErrsController do
96 end 96 end
97 end 97 end
98 end 98 end
99 - 99 +
100 describe "GET /apps/:app_id/errs/:id" do 100 describe "GET /apps/:app_id/errs/:id" do
101 render_views 101 render_views
102 - 102 +
103 before do 103 before do
104 3.times { Factory(:notice, :err => err)} 104 3.times { Factory(:notice, :err => err)}
105 end 105 end
106 - 106 +
107 context 'when logged in as an admin' do 107 context 'when logged in as an admin' do
108 before do 108 before do
109 sign_in Factory(:admin) 109 sign_in Factory(:admin)
110 end 110 end
111 - 111 +
112 it "finds the app" do 112 it "finds the app" do
113 get :show, :app_id => app.id, :id => err.id 113 get :show, :app_id => app.id, :id => err.id
114 assigns(:app).should == app 114 assigns(:app).should == app
115 end 115 end
116 - 116 +
117 it "finds the err" do 117 it "finds the err" do
118 get :show, :app_id => app.id, :id => err.id 118 get :show, :app_id => app.id, :id => err.id
119 assigns(:err).should == err 119 assigns(:err).should == err
120 end 120 end
121 - 121 +
122 it "successfully render page" do 122 it "successfully render page" do
123 get :show, :app_id => app.id, :id => err.id 123 get :show, :app_id => app.id, :id => err.id
124 response.should be_success 124 response.should be_success
@@ -131,9 +131,9 @@ describe ErrsController do @@ -131,9 +131,9 @@ describe ErrsController do
131 err = Factory :err 131 err = Factory :err
132 get :show, :app_id => err.app.id, :id => err.id 132 get :show, :app_id => err.app.id, :id => err.id
133 133
134 - response.body.should_not button_matcher 134 + response.body.should_not button_matcher
135 end 135 end
136 - 136 +
137 it "should exist for err's app with issue tracker" do 137 it "should exist for err's app with issue tracker" do
138 tracker = Factory(:lighthouseapp_tracker) 138 tracker = Factory(:lighthouseapp_tracker)
139 err = Factory(:err, :app => tracker.app) 139 err = Factory(:err, :app => tracker.app)
@@ -141,7 +141,7 @@ describe ErrsController do @@ -141,7 +141,7 @@ describe ErrsController do
141 141
142 response.body.should button_matcher 142 response.body.should button_matcher
143 end 143 end
144 - 144 +
145 it "should not exist for err with issue_link" do 145 it "should not exist for err with issue_link" do
146 tracker = Factory(:lighthouseapp_tracker) 146 tracker = Factory(:lighthouseapp_tracker)
147 err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host") 147 err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host")
@@ -151,7 +151,7 @@ describe ErrsController do @@ -151,7 +151,7 @@ describe ErrsController do
151 end 151 end
152 end 152 end
153 end 153 end
154 - 154 +
155 context 'when logged in as a user' do 155 context 'when logged in as a user' do
156 before do 156 before do
157 sign_in(@user = Factory(:user)) 157 sign_in(@user = Factory(:user))
@@ -160,12 +160,12 @@ describe ErrsController do @@ -160,12 +160,12 @@ describe ErrsController do
160 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app) 160 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app)
161 @watched_err = Factory(:err, :app => @watched_app) 161 @watched_err = Factory(:err, :app => @watched_app)
162 end 162 end
163 - 163 +
164 it 'finds the err if the user is watching the app' do 164 it 'finds the err if the user is watching the app' do
165 get :show, :app_id => @watched_app.to_param, :id => @watched_err.id 165 get :show, :app_id => @watched_app.to_param, :id => @watched_err.id
166 assigns(:err).should == @watched_err 166 assigns(:err).should == @watched_err
167 end 167 end
168 - 168 +
169 it 'raises a DocumentNotFound error if the user is not watching the app' do 169 it 'raises a DocumentNotFound error if the user is not watching the app' do
170 lambda { 170 lambda {
171 get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id 171 get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id
@@ -173,17 +173,17 @@ describe ErrsController do @@ -173,17 +173,17 @@ describe ErrsController do
173 end 173 end
174 end 174 end
175 end 175 end
176 - 176 +
177 describe "PUT /apps/:app_id/errs/:id/resolve" do 177 describe "PUT /apps/:app_id/errs/:id/resolve" do
178 before do 178 before do
179 sign_in Factory(:admin) 179 sign_in Factory(:admin)
180 - 180 +
181 @err = Factory(:err) 181 @err = Factory(:err)
182 App.stub(:find).with(@err.app.id).and_return(@err.app) 182 App.stub(:find).with(@err.app.id).and_return(@err.app)
183 @err.app.errs.stub(:find).and_return(@err) 183 @err.app.errs.stub(:find).and_return(@err)
184 @err.stub(:resolve!) 184 @err.stub(:resolve!)
185 end 185 end
186 - 186 +
187 it 'finds the app and the err' do 187 it 'finds the app and the err' do
188 App.should_receive(:find).with(@err.app.id).and_return(@err.app) 188 App.should_receive(:find).with(@err.app.id).and_return(@err.app)
189 @err.app.errs.should_receive(:find).and_return(@err) 189 @err.app.errs.should_receive(:find).and_return(@err)
@@ -191,17 +191,17 @@ describe ErrsController do @@ -191,17 +191,17 @@ describe ErrsController do
191 assigns(:app).should == @err.app 191 assigns(:app).should == @err.app
192 assigns(:err).should == @err 192 assigns(:err).should == @err
193 end 193 end
194 - 194 +
195 it "should resolve the issue" do 195 it "should resolve the issue" do
196 @err.should_receive(:resolve!).and_return(true) 196 @err.should_receive(:resolve!).and_return(true)
197 put :resolve, :app_id => @err.app.id, :id => @err.id 197 put :resolve, :app_id => @err.app.id, :id => @err.id
198 end 198 end
199 - 199 +
200 it "should display a message" do 200 it "should display a message" do
201 put :resolve, :app_id => @err.app.id, :id => @err.id 201 put :resolve, :app_id => @err.app.id, :id => @err.id
202 request.flash[:success].should match(/Great news/) 202 request.flash[:success].should match(/Great news/)
203 end 203 end
204 - 204 +
205 it "should redirect to the app page" do 205 it "should redirect to the app page" do
206 put :resolve, :app_id => @err.app.id, :id => @err.id 206 put :resolve, :app_id => @err.app.id, :id => @err.id
207 response.should redirect_to(app_path(@err.app)) 207 response.should redirect_to(app_path(@err.app))
spec/models/err_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe Err do 3 describe Err do
4 - 4 +
5 context 'validations' do 5 context 'validations' do
6 it 'requires a klass' do 6 it 'requires a klass' do
7 err = Factory.build(:err, :klass => nil) 7 err = Factory.build(:err, :klass => nil)
8 err.should_not be_valid 8 err.should_not be_valid
9 err.errors[:klass].should include("can't be blank") 9 err.errors[:klass].should include("can't be blank")
10 end 10 end
11 - 11 +
12 it 'requires an environment' do 12 it 'requires an environment' do
13 err = Factory.build(:err, :environment => nil) 13 err = Factory.build(:err, :environment => nil)
14 err.should_not be_valid 14 err.should_not be_valid
15 err.errors[:environment].should include("can't be blank") 15 err.errors[:environment].should include("can't be blank")
16 end 16 end
17 end 17 end
18 - 18 +
19 context '#for' do 19 context '#for' do
20 before do 20 before do
21 @app = Factory(:app) 21 @app = Factory(:app)
@@ -27,16 +27,16 @@ describe Err do @@ -27,16 +27,16 @@ describe Err do
27 :environment => 'production' 27 :environment => 'production'
28 } 28 }
29 end 29 end
30 - 30 +
31 it 'returns the correct err if one already exists' do 31 it 'returns the correct err if one already exists' do
32 existing = Err.create(@conditions) 32 existing = Err.create(@conditions)
33 Err.for(@conditions).should == existing 33 Err.for(@conditions).should == existing
34 end 34 end
35 - 35 +
36 it 'assigns the returned err to the given app' do 36 it 'assigns the returned err to the given app' do
37 Err.for(@conditions).app.should == @app 37 Err.for(@conditions).app.should == @app
38 end 38 end
39 - 39 +
40 it 'creates a new err if a matching one does not already exist' do 40 it 'creates a new err if a matching one does not already exist' do
41 Err.where(@conditions.except(:app)).exists?.should == false 41 Err.where(@conditions.except(:app)).exists?.should == false
42 lambda { 42 lambda {
@@ -44,36 +44,47 @@ describe Err do @@ -44,36 +44,47 @@ describe Err do
44 }.should change(Err,:count).by(1) 44 }.should change(Err,:count).by(1)
45 end 45 end
46 end 46 end
47 - 47 +
48 context '#last_notice_at' do 48 context '#last_notice_at' do
49 it "returns the created_at timestamp of the latest notice" do 49 it "returns the created_at timestamp of the latest notice" do
50 err = Factory(:err) 50 err = Factory(:err)
51 err.last_notice_at.should be_nil 51 err.last_notice_at.should be_nil
52 - 52 +
53 notice1 = Factory(:notice, :err => err) 53 notice1 = Factory(:notice, :err => err)
54 err.last_notice_at.should == notice1.created_at 54 err.last_notice_at.should == notice1.created_at
55 - 55 +
56 notice2 = Factory(:notice, :err => err) 56 notice2 = Factory(:notice, :err => err)
57 err.last_notice_at.should == notice2.created_at 57 err.last_notice_at.should == notice2.created_at
58 end 58 end
59 end 59 end
60 - 60 +
61 context '#message' do 61 context '#message' do
  62 + it "returns klass by default" do
  63 + err = Factory(:err)
  64 + err.message.should == err.klass
  65 + end
  66 +
62 it 'returns the message from the first notice' do 67 it 'returns the message from the first notice' do
63 err = Factory(:err) 68 err = Factory(:err)
64 notice1 = Factory(:notice, :err => err, :message => 'ERR 1') 69 notice1 = Factory(:notice, :err => err, :message => 'ERR 1')
65 notice2 = Factory(:notice, :err => err, :message => 'ERR 2') 70 notice2 = Factory(:notice, :err => err, :message => 'ERR 2')
66 err.message.should == notice1.message 71 err.message.should == notice1.message
67 end 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
68 end 79 end
69 - 80 +
70 context "#resolved?" do 81 context "#resolved?" do
71 it "should start out as unresolved" do 82 it "should start out as unresolved" do
72 err = Err.new 83 err = Err.new
73 err.should_not be_resolved 84 err.should_not be_resolved
74 err.should be_unresolved 85 err.should be_unresolved
75 end 86 end
76 - 87 +
77 it "should be able to be resolved" do 88 it "should be able to be resolved" do
78 err = Factory(:err) 89 err = Factory(:err)
79 err.should_not be_resolved 90 err.should_not be_resolved
@@ -81,7 +92,7 @@ describe Err do @@ -81,7 +92,7 @@ describe Err do
81 err.reload.should be_resolved 92 err.reload.should be_resolved
82 end 93 end
83 end 94 end
84 - 95 +
85 context "resolve!" do 96 context "resolve!" do
86 it "marks the err as resolved" do 97 it "marks the err as resolved" do
87 err = Factory(:err) 98 err = Factory(:err)
@@ -89,7 +100,7 @@ describe Err do @@ -89,7 +100,7 @@ describe Err do
89 err.resolve! 100 err.resolve!
90 err.should be_resolved 101 err.should be_resolved
91 end 102 end
92 - 103 +
93 it "should throw an err if it's not successful" do 104 it "should throw an err if it's not successful" do
94 err = Factory(:err) 105 err = Factory(:err)
95 err.should_not be_resolved 106 err.should_not be_resolved
@@ -100,7 +111,7 @@ describe Err do @@ -100,7 +111,7 @@ describe Err do
100 }.should raise_error(Mongoid::Errors::Validations) 111 }.should raise_error(Mongoid::Errors::Validations)
101 end 112 end
102 end 113 end
103 - 114 +
104 context "Scopes" do 115 context "Scopes" do
105 context "resolved" do 116 context "resolved" do
106 it 'only finds resolved Errs' do 117 it 'only finds resolved Errs' do
@@ -110,7 +121,7 @@ describe Err do @@ -110,7 +121,7 @@ describe Err do
110 Err.resolved.all.should_not include(unresolved) 121 Err.resolved.all.should_not include(unresolved)
111 end 122 end
112 end 123 end
113 - 124 +
114 context "unresolved" do 125 context "unresolved" do
115 it 'only finds unresolved Errs' do 126 it 'only finds unresolved Errs' do
116 resolved = Factory(:err, :resolved => true) 127 resolved = Factory(:err, :resolved => true)
@@ -130,4 +141,30 @@ describe Err do @@ -130,4 +141,30 @@ describe Err do
130 end 141 end
131 end 142 end
132 end 143 end
133 -end  
134 \ No newline at end of file 144 \ No newline at end of file
  145 +
  146 + context "notice counter cache" do
  147 +
  148 + before do
  149 + @app = Factory(:app)
  150 + @err = Factory(:err, :app => @app)
  151 + end
  152 +
  153 + it "#notices_count returns 0 by default" do
  154 + @err.notices_count.should == 0
  155 + end
  156 +
  157 + it "adding a notice increases #notices_count by 1" do
  158 + lambda {
  159 + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')}.should change(@err, :notices_count).from(0).to(1)
  160 + end
  161 +
  162 + it "removing a notice decreases #notices_count by 1" do
  163 + notice1 = Factory(:notice, :err => @err, :message => 'ERR 1')
  164 + lambda {
  165 + @err.notices.first.destroy
  166 + }.should change(@err, :notices_count).from(1).to(0)
  167 + end
  168 + end
  169 +
  170 +
  171 +end
spec/models/notice_spec.rb
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe Notice do 3 describe Notice do
4 - 4 +
5 context 'validations' do 5 context 'validations' do
6 it 'requires a backtrace' do 6 it 'requires a backtrace' do
7 notice = Factory.build(:notice, :backtrace => nil) 7 notice = Factory.build(:notice, :backtrace => nil)
8 notice.should_not be_valid 8 notice.should_not be_valid
9 notice.errors[:backtrace].should include("can't be blank") 9 notice.errors[:backtrace].should include("can't be blank")
10 end 10 end
11 - 11 +
12 it 'requires the server_environment' do 12 it 'requires the server_environment' do
13 notice = Factory.build(:notice, :server_environment => nil) 13 notice = Factory.build(:notice, :server_environment => nil)
14 notice.should_not be_valid 14 notice.should_not be_valid
15 notice.errors[:server_environment].should include("can't be blank") 15 notice.errors[:server_environment].should include("can't be blank")
16 end 16 end
17 - 17 +
18 it 'requires the notifier' do 18 it 'requires the notifier' do
19 notice = Factory.build(:notice, :notifier => nil) 19 notice = Factory.build(:notice, :notifier => nil)
20 notice.should_not be_valid 20 notice.should_not be_valid
21 notice.errors[:notifier].should include("can't be blank") 21 notice.errors[:notifier].should include("can't be blank")
22 end 22 end
23 end 23 end
24 - 24 +
25 context '#from_xml' do 25 context '#from_xml' do
26 before do 26 before do
27 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read 27 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
28 @app = Factory(:app, :api_key => 'APIKEY') 28 @app = Factory(:app, :api_key => 'APIKEY')
29 Digest::MD5.stub(:hexdigest).and_return('fingerprintdigest') 29 Digest::MD5.stub(:hexdigest).and_return('fingerprintdigest')
30 end 30 end
31 - 31 +
32 it 'finds the correct app' do 32 it 'finds the correct app' do
33 @notice = Notice.from_xml(@xml) 33 @notice = Notice.from_xml(@xml)
34 @notice.err.app.should == @app 34 @notice.err.app.should == @app
35 end 35 end
36 - 36 +
37 it 'finds the correct err for the notice' do 37 it 'finds the correct err for the notice' do
38 Err.should_receive(:for).with({ 38 Err.should_receive(:for).with({
39 :app => @app, 39 :app => @app,
@@ -46,7 +46,7 @@ describe Notice do @@ -46,7 +46,7 @@ describe Notice do
46 err.notices.stub(:create!) 46 err.notices.stub(:create!)
47 @notice = Notice.from_xml(@xml) 47 @notice = Notice.from_xml(@xml)
48 end 48 end
49 - 49 +
50 it 'marks the err as unresolve if it was previously resolved' do 50 it 'marks the err as unresolve if it was previously resolved' do
51 Err.should_receive(:for).with({ 51 Err.should_receive(:for).with({
52 :app => @app, 52 :app => @app,
@@ -61,56 +61,70 @@ describe Notice do @@ -61,56 +61,70 @@ describe Notice do
61 @notice.err.should == err 61 @notice.err.should == err
62 @notice.err.should_not be_resolved 62 @notice.err.should_not be_resolved
63 end 63 end
64 - 64 +
65 it 'should create a new notice' do 65 it 'should create a new notice' do
66 @notice = Notice.from_xml(@xml) 66 @notice = Notice.from_xml(@xml)
67 @notice.should be_persisted 67 @notice.should be_persisted
68 end 68 end
69 - 69 +
70 it 'assigns an err to the notice' do 70 it 'assigns an err to the notice' do
71 @notice = Notice.from_xml(@xml) 71 @notice = Notice.from_xml(@xml)
72 @notice.err.should be_a(Err) 72 @notice.err.should be_a(Err)
73 end 73 end
74 - 74 +
75 it 'captures the err message' do 75 it 'captures the err message' do
76 @notice = Notice.from_xml(@xml) 76 @notice = Notice.from_xml(@xml)
77 @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.' 77 @notice.message.should == 'HoptoadTestingException: Testing hoptoad via "rake hoptoad:test". If you can see this, it works.'
78 end 78 end
79 - 79 +
80 it 'captures the backtrace' do 80 it 'captures the backtrace' do
81 @notice = Notice.from_xml(@xml) 81 @notice = Notice.from_xml(@xml)
82 @notice.backtrace.size.should == 73 82 @notice.backtrace.size.should == 73
83 @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake' 83 @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake'
84 end 84 end
85 - 85 +
86 it 'captures the server_environment' do 86 it 'captures the server_environment' do
87 @notice = Notice.from_xml(@xml) 87 @notice = Notice.from_xml(@xml)
88 @notice.server_environment['environment-name'].should == 'development' 88 @notice.server_environment['environment-name'].should == 'development'
89 end 89 end
90 - 90 +
91 it 'captures the request' do 91 it 'captures the request' do
92 @notice = Notice.from_xml(@xml) 92 @notice = Notice.from_xml(@xml)
93 @notice.request['url'].should == 'http://example.org/verify' 93 @notice.request['url'].should == 'http://example.org/verify'
94 @notice.request['params']['controller'].should == 'application' 94 @notice.request['params']['controller'].should == 'application'
95 end 95 end
96 - 96 +
97 it 'captures the notifier' do 97 it 'captures the notifier' do
98 @notice = Notice.from_xml(@xml) 98 @notice = Notice.from_xml(@xml)
99 @notice.notifier['name'].should == 'Hoptoad Notifier' 99 @notice.notifier['name'].should == 'Hoptoad Notifier'
100 end 100 end
101 101
102 - it "should handle params withour 'request' section" do 102 + it "should handle params without 'request' section" do
103 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read 103 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_without_request_section.xml').read
104 lambda { Notice.from_xml(@xml) }.should_not raise_error 104 lambda { Notice.from_xml(@xml) }.should_not raise_error
105 end 105 end
106 end 106 end
107 - 107 +
  108 + describe "key sanitization" do
  109 + before do
  110 + @hash = { "some.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}}
  111 + @hash_sanitized = { "some&#46;key" => { "&#36;nested&#46;key" => {"&#36;Path" => "/", "some$key" => "key"}}}
  112 + end
  113 + [:server_environment, :request, :notifier].each do |key|
  114 + it "replaces . with &#46; and $ with &#36; in keys used in #{key}" do
  115 + err = Factory(:err)
  116 + notice = Factory(:notice, :err => err, key => @hash)
  117 + notice.send(key).should == @hash_sanitized
  118 + end
  119 + end
  120 + end
  121 +
108 describe "email notifications" do 122 describe "email notifications" do
109 before do 123 before do
110 @app = Factory(:app_with_watcher) 124 @app = Factory(:app_with_watcher)
111 @err = Factory(:err, :app => @app) 125 @err = Factory(:err, :app => @app)
112 end 126 end
113 - 127 +
114 Errbit::Config.email_at_notices.each do |threshold| 128 Errbit::Config.email_at_notices.each do |threshold|
115 it "sends an email notification after #{threshold} notice(s)" do 129 it "sends an email notification after #{threshold} notice(s)" do
116 @err.notices.stub(:count).and_return(threshold) 130 @err.notices.stub(:count).and_return(threshold)
@@ -120,5 +134,5 @@ describe Notice do @@ -120,5 +134,5 @@ describe Notice do
120 end 134 end
121 end 135 end
122 end 136 end
123 -  
124 -end  
125 \ No newline at end of file 137 \ No newline at end of file
  138 +
  139 +end