Commit 44600a70dba245821081c7ff3de78b6f41b7d8c8

Authored by Nathan Broadbent
1 parent 49468ce4
Exists in master and in 1 other branch production

Configure "email_at_notices" independently for each app. We have a combination o…

…f both low and high traffic sites, so this feature would be useful so that we can stay on top of errors in some cases, and avoid getting spammed in others.
app/controllers/apps_controller.rb
@@ -2,6 +2,7 @@ class AppsController < ApplicationController @@ -2,6 +2,7 @@ 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 + before_filter :parse_email_at_notices_or_set_default, :only => [:create, :update]
5 6
6 def index 7 def index
7 @apps = current_user.admin? ? App.all : current_user.apps.all 8 @apps = current_user.admin? ? App.all : current_user.apps.all
@@ -73,4 +74,21 @@ class AppsController < ApplicationController @@ -73,4 +74,21 @@ class AppsController < ApplicationController
73 # apparently finding by 'watchers.email' and 'id' is broken 74 # apparently finding by 'watchers.email' and 'id' is broken
74 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app) 75 raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
75 end 76 end
  77 +
  78 + # email_at_notices is edited as a string, and stored as an array.
  79 + def parse_email_at_notices_or_set_default
  80 + if params[:app] && val = params[:app][:email_at_notices]
  81 + # Sanitize negative values, split on comma,
  82 + # strip, parse as integer, remove all '0's.
  83 + # If empty, set as default and show an error message.
  84 + email_at_notices = val.gsub(/-\d+/,"").split(",").map{|v| v.strip.to_i }.reject{|v| v == 0}
  85 + if email_at_notices.any?
  86 + params[:app][:email_at_notices] = email_at_notices
  87 + else
  88 + default_array = params[:app][:email_at_notices] = Errbit::Config.default_email_at_notices
  89 + flash[:error] = "Couldn't parse your notification frequency. Value was reset to default (#{default_array.join(', ')})."
  90 + end
  91 + end
  92 + end
76 end 93 end
  94 +
app/models/app.rb
@@ -8,6 +8,7 @@ class App @@ -8,6 +8,7 @@ class App
8 field :resolve_errs_on_deploy, :type => Boolean, :default => false 8 field :resolve_errs_on_deploy, :type => Boolean, :default => false
9 field :notify_on_errs, :type => Boolean, :default => true 9 field :notify_on_errs, :type => Boolean, :default => true
10 field :notify_on_deploys, :type => Boolean, :default => true 10 field :notify_on_deploys, :type => Boolean, :default => true
  11 + field :email_at_notices, :type => Array, :default => Errbit::Config.default_email_at_notices
11 12
12 # Some legacy apps may have sting as key instead of BSON::ObjectID 13 # Some legacy apps may have sting as key instead of BSON::ObjectID
13 identity :type => String 14 identity :type => String
@@ -68,7 +69,7 @@ class App @@ -68,7 +69,7 @@ class App
68 def github_url_to_file(file) 69 def github_url_to_file(file)
69 "#{self.github_url}/blob/master#{file}" 70 "#{self.github_url}/blob/master#{file}"
70 end 71 end
71 - 72 +
72 def issue_tracker_configured? 73 def issue_tracker_configured?
73 issue_tracker && !issue_tracker.project_id.blank? 74 issue_tracker && !issue_tracker.project_id.blank?
74 end 75 end
@@ -96,3 +97,4 @@ class App @@ -96,3 +97,4 @@ class App
96 end 97 end
97 98
98 end 99 end
  100 +
app/models/notice.rb
@@ -55,11 +55,11 @@ class Notice @@ -55,11 +55,11 @@ class Notice
55 agent_string = env_vars['HTTP_USER_AGENT'] 55 agent_string = env_vars['HTTP_USER_AGENT']
56 agent_string.blank? ? nil : UserAgent.parse(agent_string) 56 agent_string.blank? ? nil : UserAgent.parse(agent_string)
57 end 57 end
58 - 58 +
59 def self.in_app_backtrace_line? line 59 def self.in_app_backtrace_line? line
60 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))}) 60 !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})
61 end 61 end
62 - 62 +
63 def request 63 def request
64 read_attribute(:request) || {} 64 read_attribute(:request) || {}
65 end 65 end
@@ -83,11 +83,11 @@ class Notice @@ -83,11 +83,11 @@ class Notice
83 def cache_last_notice_at 83 def cache_last_notice_at
84 err.update_attributes(:last_notice_at => created_at) 84 err.update_attributes(:last_notice_at => created_at)
85 end 85 end
86 - 86 +
87 protected 87 protected
88 88
89 def should_notify? 89 def should_notify?
90 - err.app.notify_on_errs? && Errbit::Config.email_at_notices.include?(err.notices.count) && err.app.watchers.any? 90 + err.app.notify_on_errs? && err.app.email_at_notices.include?(err.notices.count) && err.app.watchers.any?
91 end 91 end
92 92
93 93
@@ -122,3 +122,4 @@ class Notice @@ -122,3 +122,4 @@ class Notice
122 end 122 end
123 end 123 end
124 end 124 end
  125 +
app/views/apps/_fields.html.haml
@@ -4,21 +4,34 @@ @@ -4,21 +4,34 @@
4 = f.label :name 4 = f.label :name
5 = f.text_field :name 5 = f.text_field :name
6 6
7 -%div.checkbox  
8 - = f.check_box :notify_on_errs  
9 - = f.label :notify_on_errs, 'Notify on errors'  
10 -  
11 %div 7 %div
12 = f.label :github_url 8 = f.label :github_url
13 = f.text_field :github_url 9 = f.text_field :github_url
14 10
  11 +%fieldset
  12 + %legend Notifications
  13 + %div.checkbox
  14 + = f.check_box :notify_on_errs
  15 + = f.label :notify_on_errs, 'Notify on errors'
  16 + %div.email_at_notices_nested{:style => f.object.notify_on_errs ? '' : 'display: none;'}
  17 + .field-helpertext Send a notification every
  18 + -# Edit the email_at_notices array as a CSV string
  19 + = f.text_field :email_at_notices, :value => f.object.email_at_notices.join(", ")
  20 + .field-helpertext times an error occurs. (CSV)
  21 + %div.checkbox
  22 + = f.check_box :notify_on_deploys
  23 + = f.label :notify_on_deploys, 'Notify on deploys'
  24 +
  25 +:javascript
  26 + $('#app_notify_on_errs').change(function(){
  27 + var el = $('.email_at_notices_nested');
  28 + this.checked ? el.show() : el.hide();
  29 + });
  30 +
15 %div.checkbox 31 %div.checkbox
16 = f.check_box :resolve_errs_on_deploy 32 = f.check_box :resolve_errs_on_deploy
17 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy' 33 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy'
18 34
19 -%div.checkbox  
20 - = f.check_box :notify_on_deploys  
21 - = f.label :notify_on_deploys, 'Notify on deploys'  
22 35
23 %fieldset.nested-wrapper 36 %fieldset.nested-wrapper
24 %legend Watchers 37 %legend Watchers
@@ -75,3 +88,4 @@ @@ -75,3 +88,4 @@
75 = w.text_field :username, :placeholder => 'Username/Email for your account' 88 = w.text_field :username, :placeholder => 'Username/Email for your account'
76 = w.label :password, 'account password' 89 = w.label :password, 'account password'
77 = w.password_field :password, :placeholder => 'Password for your account' 90 = w.password_field :password, :placeholder => 'Password for your account'
  91 +
config/config.example.yml
@@ -14,11 +14,13 @@ host: errbit.example.com @@ -14,11 +14,13 @@ host: errbit.example.com
14 # will be sent from. 14 # will be sent from.
15 email_from: errbit@example.com 15 email_from: errbit@example.com
16 16
17 -# Configure when emails are sent for an error. 17 +# Configure the default value for when emails are sent for an error.
18 # [1,3,7] = 1st, 3rd, and 7th occurence triggers 18 # [1,3,7] = 1st, 3rd, and 7th occurence triggers
19 # an email notification. 19 # an email notification.
20 -email_at_notices: [1, 10, 100] 20 +# Each app can be configured individually.
  21 +default_email_at_notices: [1, 10, 100]
21 22
22 -# Set to false to suppress confirmation when 23 +# Set to false to suppress confirmation when
23 # resolving errors. 24 # resolving errors.
24 confirm_resolve_err: true 25 confirm_resolve_err: true
  26 +
config/initializers/_load_config.rb
@@ -4,7 +4,7 @@ if ENV['HEROKU'] @@ -4,7 +4,7 @@ if ENV['HEROKU']
4 Errbit::Config = OpenStruct.new 4 Errbit::Config = OpenStruct.new
5 Errbit::Config.host = ENV['ERRBIT_HOST'] 5 Errbit::Config.host = ENV['ERRBIT_HOST']
6 Errbit::Config.email_from = ENV['ERRBIT_EMAIL_FROM'] 6 Errbit::Config.email_from = ENV['ERRBIT_EMAIL_FROM']
7 - Errbit::Config.email_at_notices = [1,3,10] #ENV['ERRBIT_EMAIL_AT_NOTICES'] 7 + Errbit::Config.default_email_at_notices = [1,3,10] #ENV['ERRBIT_EMAIL_AT_NOTICES']
8 Errbit::Application.config.action_mailer.smtp_settings = { 8 Errbit::Application.config.action_mailer.smtp_settings = {
9 :address => "smtp.sendgrid.net", 9 :address => "smtp.sendgrid.net",
10 :port => "25", 10 :port => "25",
@@ -25,4 +25,5 @@ end @@ -25,4 +25,5 @@ end
25 # Set config specific values 25 # Set config specific values
26 (Errbit::Application.config.action_mailer.default_url_options ||= {}).tap do |default| 26 (Errbit::Application.config.action_mailer.default_url_options ||= {}).tap do |default|
27 default.merge! :host => Errbit::Config.host if default[:host].blank? 27 default.merge! :host => Errbit::Config.host if default[:host].blank?
28 -end  
29 \ No newline at end of file 28 \ No newline at end of file
  29 +end
  30 +
public/stylesheets/application.css
@@ -350,6 +350,17 @@ form .error-messages ul { @@ -350,6 +350,17 @@ form .error-messages ul {
350 list-style-type: square; 350 list-style-type: square;
351 } 351 }
352 352
  353 +form .field-helpertext {
  354 + display: inline;
  355 + text-transform: uppercase;
  356 +}
  357 +
  358 +form input#app_email_at_notices {
  359 + width: 130px;
  360 + margin: 0 5px;
  361 +}
  362 +
  363 +
353 /* Tables */ 364 /* Tables */
354 table { 365 table {
355 width: 100%; 366 width: 100%;
@@ -683,3 +694,4 @@ span.click_span { @@ -683,3 +694,4 @@ span.click_span {
683 #deploys_div, #repository_div, #watchers_div { 694 #deploys_div, #repository_div, #watchers_div {
684 display: none; 695 display: none;
685 } 696 }
  697 +
spec/controllers/apps_controller_spec.rb
@@ -241,6 +241,27 @@ describe AppsController do @@ -241,6 +241,27 @@ describe AppsController do
241 end 241 end
242 end 242 end
243 243
  244 + context "changing email_at_notices" do
  245 + it "should parse legal csv values" do
  246 + put :update, :id => @app.id, :app => { :email_at_notices => '1, 4, 7,8, 10' }
  247 + @app.reload
  248 + @app.email_at_notices.should == [1, 4, 7, 8, 10]
  249 + end
  250 + context "failed parsing of CSV" do
  251 + it "should set the default value" do
  252 + @app = Factory(:app, :email_at_notices => [1, 2, 3, 4])
  253 + put :update, :id => @app.id, :app => { :email_at_notices => 'asdf, -1,0,foobar,gd00,0,abc' }
  254 + @app.reload
  255 + @app.email_at_notices.should == Errbit::Config.default_email_at_notices
  256 + end
  257 +
  258 + it "should display a message" do
  259 + put :update, :id => @app.id, :app => { :email_at_notices => 'qwertyuiop' }
  260 + request.flash[:error].should match(/Couldn't parse/)
  261 + end
  262 + end
  263 + end
  264 +
244 context "setting up issue tracker", :cur => true do 265 context "setting up issue tracker", :cur => true do
245 context "unknown tracker type" do 266 context "unknown tracker type" do
246 before(:each) do 267 before(:each) do
@@ -386,3 +407,4 @@ describe AppsController do @@ -386,3 +407,4 @@ describe AppsController do
386 end 407 end
387 408
388 end 409 end
  410 +
spec/factories/app_factories.rb
1 Factory.define(:app) do |p| 1 Factory.define(:app) do |p|
2 p.name { Factory.next :app_name } 2 p.name { Factory.next :app_name }
  3 + p.email_at_notices { [1, 10, 100] }
3 end 4 end
4 5
5 Factory.define(:app_with_watcher, :parent => :app) do |p| 6 Factory.define(:app_with_watcher, :parent => :app) do |p|
@@ -26,3 +27,4 @@ Factory.define(:deploy) do |d| @@ -26,3 +27,4 @@ Factory.define(:deploy) do |d|
26 d.environment 'production' 27 d.environment 'production'
27 d.revision ActiveSupport::SecureRandom.hex(10) 28 d.revision ActiveSupport::SecureRandom.hex(10)
28 end 29 end
  30 +
spec/models/notice_spec.rb
@@ -21,15 +21,15 @@ describe Notice do @@ -21,15 +21,15 @@ describe Notice do
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 '.in_app_backtrace_line?' do 25 context '.in_app_backtrace_line?' do
26 let(:backtrace) do [{ 26 let(:backtrace) do [{
27 'number' => rand(999), 27 'number' => rand(999),
28 - 'file' => '[GEM_ROOT]/gems/actionpack-3.0.4/lib/action_controller/metal/rescue.rb', 28 + 'file' => '[GEM_ROOT]/gems/actionpack-3.0.4/lib/action_controller/metal/rescue.rb',
29 'method' => ActiveSupport.methods.shuffle.first 29 'method' => ActiveSupport.methods.shuffle.first
30 }, { 30 }, {
31 'number' => rand(999), 31 'number' => rand(999),
32 - 'file' => '[PROJECT_ROOT]/vendor/plugins/seamless_database_pool/lib/seamless_database_pool/controller_filter.rb', 32 + 'file' => '[PROJECT_ROOT]/vendor/plugins/seamless_database_pool/lib/seamless_database_pool/controller_filter.rb',
33 'method' => ActiveSupport.methods.shuffle.first 33 'method' => ActiveSupport.methods.shuffle.first
34 }, { 34 }, {
35 'number' => rand(999), 35 'number' => rand(999),
@@ -50,7 +50,7 @@ describe Notice do @@ -50,7 +50,7 @@ describe Notice do
50 Notice.in_app_backtrace_line?(backtrace[2]).should == true 50 Notice.in_app_backtrace_line?(backtrace[2]).should == true
51 end 51 end
52 end 52 end
53 - 53 +
54 context '#from_xml' do 54 context '#from_xml' do
55 before do 55 before do
56 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read 56 @xml = Rails.root.join('spec','fixtures','hoptoad_test_notice.xml').read
@@ -154,20 +154,22 @@ describe Notice do @@ -154,20 +154,22 @@ describe Notice do
154 notice.user_agent.browser.should == 'Chrome' 154 notice.user_agent.browser.should == 'Chrome'
155 notice.user_agent.version.to_s.should =~ /^10\.0/ 155 notice.user_agent.version.to_s.should =~ /^10\.0/
156 end 156 end
157 - 157 +
158 it "should be nil if HTTP_USER_AGENT is blank" do 158 it "should be nil if HTTP_USER_AGENT is blank" do
159 notice = Factory.build(:notice) 159 notice = Factory.build(:notice)
160 notice.user_agent.should == nil 160 notice.user_agent.should == nil
161 end 161 end
162 end 162 end
163 -  
164 - describe "email notifications" do 163 +
  164 + describe "email notifications (configured individually for each app)" do
  165 + custom_thresholds = [2, 4, 8, 16, 32, 64]
  166 +
165 before do 167 before do
166 - @app = Factory(:app_with_watcher) 168 + @app = Factory(:app_with_watcher, :email_at_notices => custom_thresholds)
167 @err = Factory(:err, :app => @app) 169 @err = Factory(:err, :app => @app)
168 end 170 end
169 171
170 - Errbit::Config.email_at_notices.each do |threshold| 172 + custom_thresholds.each do |threshold|
171 it "sends an email notification after #{threshold} notice(s)" do 173 it "sends an email notification after #{threshold} notice(s)" do
172 @err.notices.stub(:count).and_return(threshold) 174 @err.notices.stub(:count).and_return(threshold)
173 Mailer.should_receive(:err_notification). 175 Mailer.should_receive(:err_notification).
@@ -178,3 +180,4 @@ describe Notice do @@ -178,3 +180,4 @@ describe Notice do
178 end 180 end
179 181
180 end 182 end
  183 +