Commit 40453cc3539b4c689aea482959a7f126727dfec8
Committed by
Karol Hosiawa
1 parent
bd17fa78
Exists in
master
and in
1 other branch
moved Notices to a separate collection
Showing
27 changed files
with
342 additions
and
167 deletions
Show diff stats
.gitignore
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 < 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 < 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 < 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 < 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 < 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 < 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(/\./,'.').gsub(/^\$/,'$')] = 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
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
app/views/errs/redmine_body.txt.erb
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
app/views/notices/_summary.html.haml
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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |
| ... | ... | @@ -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 | ... | ... |
| ... | ... | @@ -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.key" => { "$nested.key" => {"$Path" => "/", "some$key" => "key"}}} | |
| 112 | + end | |
| 113 | + [:server_environment, :request, :notifier].each do |key| | |
| 114 | + it "replaces . with . and $ with $ 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 | ... | ... |