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

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