Commit 76db16871d2e726d4bf1e31b935adb6ef98a5a2f

Authored by Stephen Crosby
2 parents 72a3058f aaf30eff
Exists in master and in 1 other branch production

Merge branch 'master' into features/extract_issue_tracker

Conflicts:
	Gemfile
	Gemfile.lock
	app/controllers/apps_controller.rb
	app/models/issue_trackers/github_issues_tracker.rb
	app/views/apps/_fields.html.haml
	app/views/apps/edit.html.haml
	spec/models/issue_trackers/github_issues_tracker_spec.rb
CHANGELOG.md
... ... @@ -13,12 +13,30 @@
13 13 use ([@shingara][])
14 14 - [#588][] Change documentation to see Airbrake gem instead of
15 15 hoptoad_nofier ([@ugisozols][])
  16 +- [#592][] Avoid taskmapper dependencies ([@arthurnn][])
  17 +- [#597][] Improve fingerprinting if only change by object memory adress
  18 + ([@1st8][])
  19 +- [#603][] Improve the page rendering of App list ([@arthurnn][])
  20 +- [#606][] Add ability to deploy on Cloud 66 ([@pkallberg][])
  21 +- [#613][] Improve text about delete thing ([@mildavw][])
  22 +- [#616][] Improvement about our test suite ([@durrantm][])
  23 +- [#620][] Add App version information in email notification
  24 + ([@soberstadt][])
  25 +- [#624][] Allow configuration of port if errbit not on 80
  26 + ([@rsutphin][])
  27 +- [#617][] Allow GET request on notice API ([@soberstadt][])
  28 +- [#618][] Improve the stack information in JS notifier ([@soberstadt][])
  29 +- [#619][] Handle the App version information in the API ([@soberstadt][])
16 30  
17 31 ### Bug Fixes
18 32  
19 33 - [#565][] Treat request URL as CDATA so query string params don't
20 34 result in invalid XML ([@mildavw][])
21 35 - [#594][] Dont use ^ and $ on email_regexp([@arthurnn][])
  36 +- [#598][] Fix issue about gitlab integration ([@jozefvaclavik][])
  37 +- [#609][] Fix issue about page system on search error ([@zhekanax][])
  38 +- [#611][] Fix issue about select all error in search page ([@zhekanax][])
  39 +- [#615][] Fix some LDAP information in README ([@felixbuenemann][])
22 40  
23 41  
24 42 [@arthurnn]: https://github.com/arthurnn
... ... @@ -26,13 +44,36 @@
26 44 [@numbata]: https://github.com/numbata
27 45 [@shingara]: https://github.com/shingara
28 46 [@ugisozols]: https://github.com/ugisozols
  47 +[@1st8]: https://github.com/1st8
  48 +[@jozefvaclavik]: https://github.com/jozefvaclavik
  49 +[@zhekanax]: https://github.com/zhekanax
  50 +[@pkallberg]: https://github.com/pkallberg
  51 +[@durrantm]: https://github.com/durrantm
  52 +[@felixbuenemann]: https://github.com/felixbuenemann
  53 +[@soberstadt]: https://github.com/soberstadt
  54 +[@rsutphin]: https://github.com/rsutphin
29 55  
30 56 [#289]: https://github.com/errbit/errbit/issues/289
31 57 [#515]: https://github.com/errbit/errbit/issues/515
32 58 [#547]: https://github.com/errbit/errbit/issues/547
33 59 [#565]: https://github.com/errbit/errbit/issues/565
34 60 [#588]: https://github.com/errbit/errbit/issues/588
  61 +[#592]: https://github.com/errbit/errbit/pull/592
35 62 [#594]: https://github.com/errbit/errbit/pull/594
  63 +[#597]: https://github.com/errbit/errbit/pull/597
  64 +[#598]: https://github.com/errbit/errbit/pull/598
  65 +[#603]: https://github.com/errbit/errbit/pull/603
  66 +[#606]: https://github.com/errbit/errbit/pull/606
  67 +[#609]: https://github.com/errbit/errbit/pull/609
  68 +[#611]: https://github.com/errbit/errbit/pull/611
  69 +[#613]: https://github.com/errbit/errbit/pull/613
  70 +[#616]: https://github.com/errbit/errbit/pull/616
  71 +[#615]: https://github.com/errbit/errbit/pull/615
  72 +[#617]: https://github.com/errbit/errbit/pull/617
  73 +[#618]: https://github.com/errbit/errbit/pull/618
  74 +[#619]: https://github.com/errbit/errbit/pull/619
  75 +[#620]: https://github.com/errbit/errbit/pull/620
  76 +[#624]: https://github.com/errbit/errbit/pull/624
36 77  
37 78 ## 0.2.1 - Not released Yet
38 79  
... ...
CONTRIBUTORS.md
... ... @@ -9,6 +9,14 @@
9 9 - [@shingara][]
10 10 - [@simi][]
11 11 - [@ugisozols][]
  12 +- [@1st8][]
  13 +- [@jozefvaclavik][]
  14 +- [@zhekanax][]
  15 +- [@pkallberg][]
  16 +- [@durrantm][]
  17 +- [@felixbuenemann][]
  18 +- [@soberstadt][]
  19 +- [@rsutphin][]
12 20  
13 21 [@arthurnn]: https://github.com/arthurnn
14 22 [@emgiezet]: https://github.com/emgiezet
... ... @@ -19,6 +27,13 @@
19 27 [@shingara]: https://github.com/shingara
20 28 [@simi]: https://github.com/simi
21 29 [@ugisozols]: https://github.com/ugisozols
  30 +[@1st8]: https://github.com/1st8
  31 +[@jozefvaclavik]: https://github.com/jozefvaclavik
  32 +[@zhekanax]: https://github.com/zhekanax
  33 +[@pkallberg]: https://github.com/pkallberg
  34 +[@durrantm]: https://github.com/durrantm
  35 +[@felixbuenemann]: https://github.com/felixbuenemann
  36 +
22 37  
23 38  
24 39 ## 0.2.1 - Not released yet
... ... @@ -92,3 +107,5 @@
92 107 [@tvdeyen]: https://github.com/tvdeyen
93 108 [@williamn]: https://github.com/williamn
94 109 [@xenji]: https://github.com/xenji
  110 +[@soberstadt]: https://github.com/soberstadt
  111 +[@rsutphin]: https://github.com/rsutphin
... ...
Gemfile
1 1 source 'https://rubygems.org'
2 2  
3   -RAILS_VERSION = '~> 3.2.15'
  3 +RAILS_VERSION = '~> 3.2.18'
4 4  
5 5 gem 'actionmailer', RAILS_VERSION
6 6 gem 'actionpack', RAILS_VERSION
... ... @@ -25,7 +25,6 @@ gem 'rails_autolink'
25 25 # Please don't update hoptoad_notifier to airbrake.
26 26 # It's for internal use only, and we monkeypatch certain methods
27 27 gem 'hoptoad_notifier', "~> 2.4"
28   -gem 'draper', :require => false
29 28  
30 29 gem 'errbit_plugin',
31 30 :git => 'https://github.com/errbit/errbit_plugin.git'
... ... @@ -62,7 +61,6 @@ group :development, :test do
62 61 gem 'rspec-rails'
63 62 gem 'webmock', :require => false
64 63 gem 'airbrake', :require => false
65   - gem 'debugger', :platform => :mri_19
66 64 gem 'pry-rails'
67 65 # gem 'rpm_contrib'
68 66 # gem 'newrelic_rpm'
... ... @@ -84,10 +82,9 @@ group :development do
84 82 end
85 83  
86 84 group :test do
87   - gem 'capybara', :require => false
88   - gem 'poltergeist', :require => false
89   - gem 'launchy', :require => false
90   - gem 'database_cleaner', :require => false
  85 + gem 'capybara'
  86 + gem 'launchy'
  87 + gem 'database_cleaner'
91 88 gem 'email_spec'
92 89 gem 'timecop'
93 90 gem 'coveralls', :require => false
... ... @@ -98,6 +95,7 @@ group :heroku, :production do
98 95 gem 'unicorn', :require => false
99 96 end
100 97  
  98 +
101 99 # Gems used only for assets and not required
102 100 # in production environments by default.
103 101 group :assets do
... ...
Gemfile.lock
... ... @@ -15,16 +15,16 @@ GIT
15 15 GEM
16 16 remote: https://rubygems.org/
17 17 specs:
18   - actionmailer (3.2.16)
19   - actionpack (= 3.2.16)
  18 + actionmailer (3.2.18)
  19 + actionpack (= 3.2.18)
20 20 mail (~> 2.5.4)
21 21 actionmailer_inline_css (1.5.3)
22 22 actionmailer (>= 3.0.0)
23 23 nokogiri (>= 1.4.4)
24 24 premailer (>= 1.7.1)
25   - actionpack (3.2.16)
26   - activemodel (= 3.2.16)
27   - activesupport (= 3.2.16)
  25 + actionpack (3.2.18)
  26 + activemodel (= 3.2.18)
  27 + activesupport (= 3.2.18)
28 28 builder (~> 3.0.0)
29 29 erubis (~> 2.7.0)
30 30 journey (~> 1.0.4)
... ... @@ -32,18 +32,18 @@ GEM
32 32 rack-cache (~> 1.2)
33 33 rack-test (~> 0.6.1)
34 34 sprockets (~> 2.2.1)
35   - activemodel (3.2.16)
36   - activesupport (= 3.2.16)
  35 + activemodel (3.2.18)
  36 + activesupport (= 3.2.18)
37 37 builder (~> 3.0.0)
38   - activerecord (3.2.16)
39   - activemodel (= 3.2.16)
40   - activesupport (= 3.2.16)
  38 + activerecord (3.2.18)
  39 + activemodel (= 3.2.18)
  40 + activesupport (= 3.2.18)
41 41 arel (~> 3.0.2)
42 42 tzinfo (~> 0.3.29)
43   - activeresource (3.2.16)
44   - activemodel (= 3.2.16)
45   - activesupport (= 3.2.16)
46   - activesupport (3.2.16)
  43 + activeresource (3.2.18)
  44 + activemodel (= 3.2.18)
  45 + activesupport (= 3.2.18)
  46 + activesupport (3.2.18)
47 47 i18n (~> 0.6, >= 0.6.4)
48 48 multi_json (~> 1.0)
49 49 addressable (2.3.5)
... ... @@ -73,9 +73,7 @@ GEM
73 73 rack (>= 1.0.0)
74 74 rack-test (>= 0.5.4)
75 75 xpath (~> 2.0)
76   - cliver (0.2.2)
77 76 coderay (1.0.9)
78   - columnize (0.3.6)
79 77 coveralls (0.7.0)
80 78 multi_json (~> 1.3)
81 79 rest-client
... ... @@ -88,12 +86,6 @@ GEM
88 86 addressable
89 87 database_cleaner (1.2.0)
90 88 debug_inspector (0.0.2)
91   - debugger (1.6.3)
92   - columnize (>= 0.3.1)
93   - debugger-linecache (~> 1.2.0)
94   - debugger-ruby_core_source (~> 1.2.4)
95   - debugger-linecache (1.2.0)
96   - debugger-ruby_core_source (1.2.4)
97 89 decent_exposure (2.3.0)
98 90 devise (3.1.1)
99 91 bcrypt-ruby (~> 3.0)
... ... @@ -103,11 +95,6 @@ GEM
103 95 warden (~> 1.2.3)
104 96 diff-lcs (1.2.4)
105 97 dotenv (0.9.0)
106   - draper (1.3.0)
107   - actionpack (>= 3.0)
108   - activemodel (>= 3.0)
109   - activesupport (>= 3.0)
110   - request_store (~> 1.0.3)
111 98 email_spec (1.5.0)
112 99 launchy (~> 2.1)
113 100 mail (~> 2.2)
... ... @@ -164,6 +151,7 @@ GEM
164 151 railties
165 152 method_source (0.8.2)
166 153 mime-types (1.25.1)
  154 + mini_portile (0.5.3)
167 155 mongoid (3.1.5)
168 156 activemodel (~> 3.2)
169 157 moped (~> 1.4)
... ... @@ -179,7 +167,7 @@ GEM
179 167 rails (>= 3.2.0)
180 168 railties (>= 3.2.0)
181 169 moped (1.5.1)
182   - multi_json (1.8.2)
  170 + multi_json (1.10.0)
183 171 multi_xml (0.5.5)
184 172 multipart-post (1.2.0)
185 173 net-scp (1.1.2)
... ... @@ -189,7 +177,8 @@ GEM
189 177 net-ssh (2.7.0)
190 178 net-ssh-gateway (1.2.0)
191 179 net-ssh (>= 2.6.5)
192   - nokogiri (1.5.10)
  180 + nokogiri (1.6.1)
  181 + mini_portile (~> 0.5.0)
193 182 oauth2 (0.8.1)
194 183 faraday (~> 0.8)
195 184 httpauth (~> 0.1)
... ... @@ -211,13 +200,8 @@ GEM
211 200 orm_adapter (0.4.0)
212 201 pjax_rails (0.3.4)
213 202 jquery-rails
214   - poltergeist (1.4.1)
215   - capybara (~> 2.1.0)
216   - cliver (~> 0.2.1)
217   - multi_json (~> 1.0)
218   - websocket-driver (>= 0.2.0)
219   - polyglot (0.3.3)
220   - premailer (1.7.9)
  203 + polyglot (0.3.4)
  204 + premailer (1.7.3)
221 205 css_parser (>= 1.1.9)
222 206 htmlentities (>= 4.0.0)
223 207 pry (0.9.12.2)
... ... @@ -235,33 +219,33 @@ GEM
235 219 rack (>= 0.4)
236 220 rack-contrib (1.1.0)
237 221 rack (>= 0.9.1)
238   - rack-ssl (1.3.3)
  222 + rack-ssl (1.3.4)
239 223 rack
240 224 rack-ssl-enforcer (0.2.6)
241 225 rack-test (0.6.2)
242 226 rack (>= 1.0)
243   - rails (3.2.16)
244   - actionmailer (= 3.2.16)
245   - actionpack (= 3.2.16)
246   - activerecord (= 3.2.16)
247   - activeresource (= 3.2.16)
248   - activesupport (= 3.2.16)
  227 + rails (3.2.18)
  228 + actionmailer (= 3.2.18)
  229 + actionpack (= 3.2.18)
  230 + activerecord (= 3.2.18)
  231 + activeresource (= 3.2.18)
  232 + activesupport (= 3.2.18)
249 233 bundler (~> 1.0)
250   - rails_autolink (1.1.5)
  234 + railties (= 3.2.18)
  235 + rails_autolink (1.1.4)
251 236 rails (> 3.1)
252   - railties (3.2.16)
253   - actionpack (= 3.2.16)
254   - activesupport (= 3.2.16)
  237 + railties (3.2.18)
  238 + actionpack (= 3.2.18)
  239 + activesupport (= 3.2.18)
255 240 rack-ssl (~> 1.3.2)
256 241 rake (>= 0.8.7)
257 242 rdoc (~> 3.4)
258 243 thor (>= 0.14.6, < 2.0)
259 244 raindrops (0.12.0)
260   - rake (10.1.0)
  245 + rake (10.3.1)
261 246 rdoc (3.12.2)
262 247 json (~> 1.4)
263 248 ref (1.0.5)
264   - request_store (1.0.5)
265 249 rest-client (1.6.7)
266 250 mime-types (>= 1.16)
267 251 ri_cal (0.8.8)
... ... @@ -306,7 +290,7 @@ GEM
306 290 therubyracer (0.12.0)
307 291 libv8 (~> 3.16.14.0)
308 292 ref
309   - thor (0.18.1)
  293 + thor (0.19.1)
310 294 thread_safe (0.1.3)
311 295 atomic
312 296 tilt (1.4.1)
... ... @@ -318,8 +302,8 @@ GEM
318 302 turbo-sprockets-rails3 (0.3.10)
319 303 railties (> 3.2.8, < 4.0.0)
320 304 sprockets (>= 2.0.0)
321   - tzinfo (0.3.38)
322   - uglifier (2.3.0)
  305 + tzinfo (0.3.39)
  306 + uglifier (2.2.1)
323 307 execjs (>= 0.3.0)
324 308 json (>= 1.8.0)
325 309 underscore-rails (1.5.2)
... ... @@ -333,7 +317,6 @@ GEM
333 317 webmock (1.15.0)
334 318 addressable (>= 2.2.7)
335 319 crack (>= 0.3.2)
336   - websocket-driver (0.3.0)
337 320 xmpp4r (0.5.5)
338 321 xpath (2.0.0)
339 322 nokogiri (~> 1.3)
... ... @@ -343,9 +326,9 @@ PLATFORMS
343 326 ruby
344 327  
345 328 DEPENDENCIES
346   - actionmailer (~> 3.2.15)
  329 + actionmailer (~> 3.2.18)
347 330 actionmailer_inline_css
348   - actionpack (~> 3.2.15)
  331 + actionpack (~> 3.2.18)
349 332 airbrake
350 333 better_errors
351 334 binding_of_caller
... ... @@ -354,10 +337,8 @@ DEPENDENCIES
354 337 capybara
355 338 coveralls
356 339 database_cleaner
357   - debugger
358 340 decent_exposure
359 341 devise
360   - draper
361 342 email_spec
362 343 errbit_github_plugin!
363 344 errbit_plugin!
... ... @@ -380,14 +361,13 @@ DEPENDENCIES
380 361 mongoid_rails_migrations
381 362 omniauth-github
382 363 pjax_rails
383   - poltergeist
384 364 pry-rails
385 365 puma
386 366 quiet_assets
387 367 rack-ssl
388 368 rack-ssl-enforcer
389 369 rails_autolink
390   - railties (~> 3.2.15)
  370 + railties (~> 3.2.18)
391 371 ri_cal
392 372 rspec-rails
393 373 rushover
... ...
README.md
1 1 # Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url] [![Coveralls][coveralls-img-url]][coveralls-url] [![Dependency Status][gemnasium-img-url]][gemnasium-url]
2 2  
3   -[travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master
  3 +[travis-img-url]: https://travis-ci.org/errbit/errbit.svg?branch=master
4 4 [travis-ci-url]: http://travis-ci.org/errbit/errbit
5 5 [codeclimate-img-url]: https://codeclimate.com/github/errbit/errbit.png
6 6 [codeclimate-url]: https://codeclimate.com/github/errbit/errbit
... ... @@ -409,6 +409,19 @@ end
409 409  
410 410 Then get the `notifier.js` from `errbit/public/javascript/notifier.js` and add to `application.js` on your rails app or include `http://YOUR-ERRBIT-HOST/javascripts/notifier.js` on your `application.html.erb.`
411 411  
  412 +Using custom fingerprinting methods
  413 +-----------------------------------
  414 +
  415 +Errbit now allows you to easily use your own Fingerprint Strategy if that's what you'd like to do. If you are upgrading from a very old version of errbit, you can use the `LegacyFingerprint` to provide yourself
  416 +with compatibility. The fingerprint strategy can be changed by adding an initializer to errbit:
  417 +
  418 +```ruby
  419 +# config/fingerprint.rb
  420 +ErrorReport.fingerprint_strategy = LegacyFingerprint
  421 +```
  422 +
  423 +The easiest way to add custom fingerprint methods is to simply subclass `Fingerprint`
  424 +
412 425 Issue Trackers
413 426 --------------
414 427  
... ... @@ -597,7 +610,3 @@ Copyright
597 610  
598 611 Copyright (c) 2010-2013 Errbit Team. See LICENSE for details.
599 612  
600   -
601   -
602   -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/errbit/errbit/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
603   -
... ...
app/assets/images/slack_create.png 0 → 100644

5.89 KB

app/assets/images/slack_goto.png 0 → 100644

5.89 KB

app/assets/images/slack_inactive.png 0 → 100644

5.68 KB

app/controllers/api/v1/notices_controller.rb
... ... @@ -16,8 +16,7 @@ class Api::V1::NoticesController &lt; ApplicationController
16 16 end
17 17  
18 18 respond_to do |format|
19   - format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
20   - format.json { render :json => Yajl.dump(results) }
  19 + format.any(:html, :json) { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
21 20 format.xml { render :xml => results }
22 21 end
23 22 end
... ...
app/controllers/api/v1/problems_controller.rb
... ... @@ -16,8 +16,7 @@ class Api::V1::ProblemsController &lt; ApplicationController
16 16 end
17 17  
18 18 respond_to do |format|
19   - format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
20   - format.json { render :json => Yajl.dump(results) }
  19 + format.any(:html, :json) { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
21 20 format.xml { render :xml => results }
22 21 end
23 22 end
... ...
app/controllers/api/v1/stats_controller.rb
... ... @@ -12,13 +12,13 @@ class Api::V1::StatsController &lt; ApplicationController
12 12  
13 13 stats = {
14 14 :name => @app.name,
  15 + :id => @app.id,
15 16 :last_error_time => @last_error_time,
16 17 :unresolved_errors => @app.unresolved_count
17 18 }
18 19  
19 20 respond_to do |format|
20   - format.html { render :json => Yajl.dump(stats) } # render JSON if no extension specified on path
21   - format.json { render :json => Yajl.dump(stats) }
  21 + format.any(:html, :json) { render :json => Yajl.dump(stats) } # render JSON if no extension specified on path
22 22 format.xml { render :xml => stats }
23 23 end
24 24 end
... ... @@ -37,5 +37,3 @@ class Api::V1::StatsController &lt; ApplicationController
37 37 end
38 38  
39 39 end
40   -
41   -
... ...
app/controllers/apps_controller.rb
... ... @@ -39,6 +39,10 @@ class AppsController &lt; ApplicationController
39 39 app.deploys.order_by(:created_at.desc).limit(5)
40 40 }
41 41  
  42 + expose(:users) {
  43 + User.all.sort_by {|u| u.name.downcase }
  44 + }
  45 +
42 46 def index; end
43 47 def show
44 48 app
... ... @@ -91,8 +95,10 @@ class AppsController &lt; ApplicationController
91 95 def initialize_subclassed_notification_service
92 96 # set the app's notification service
93 97 if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
94   - if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
95   - app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
  98 + available_notification_classes = [NotificationService] + NotificationService.subclasses
  99 + notification_class = available_notification_classes.detect{|c| c.name == notification_type}
  100 + if !notification_class.nil?
  101 + app.notification_service = notification_class.new(params[:app][:notification_service_attributes])
96 102 end
97 103 end
98 104 end
... ...
app/controllers/problems_controller.rb
... ... @@ -123,6 +123,14 @@ class ProblemsController &lt; ApplicationController
123 123 redirect_to :back
124 124 end
125 125  
  126 + def destroy_all
  127 + nb_problem_destroy = ProblemDestroy.execute(app.problems)
  128 + flash[:success] = "#{I18n.t(:n_errs_have, :count => nb_problem_destroy)} been deleted."
  129 + redirect_to :back
  130 + rescue ActionController::RedirectBackError
  131 + redirect_to app_path(app)
  132 + end
  133 +
126 134 def search
127 135 ps = Problem.search(params[:search]).for_apps(app_scope).in_env(params[:environment]).all_else_unresolved(params[:all_errs]).ordered_by(params_sort, params_order)
128 136 selected_problems = params[:problems] || []
... ...
app/models/error_report.rb
... ... @@ -17,6 +17,10 @@ require &#39;hoptoad_notifier&#39;
17 17 class ErrorReport
18 18 attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :framework
19 19  
  20 + cattr_accessor :fingerprint_strategy do
  21 + Fingerprint
  22 + end
  23 +
20 24 def initialize(xml_or_attributes)
21 25 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
22 26 @attributes.each{|k, v| instance_variable_set(:"@#{k}", v) }
... ... @@ -84,7 +88,7 @@ class ErrorReport
84 88 private
85 89  
86 90 def fingerprint
87   - @fingerprint ||= Fingerprint.generate(notice, api_key)
  91 + @fingerprint ||= fingerprint_strategy.generate(notice, api_key)
88 92 end
89 93  
90 94 end
... ...
app/models/fingerprints/legacy_fingerprint.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +require 'digest/md5'
  2 +
  3 +class LegacyFingerprint < Fingerprint
  4 + def to_s
  5 + Digest::MD5.hexdigest(fingerprint_source)
  6 + end
  7 +
  8 + def fingerprint_source
  9 + location['method'] &&= sanitized_method_signature
  10 + end
  11 +
  12 + private
  13 + def sanitized_method_signature
  14 + location['method'].gsub(/[0-9]+|FRAGMENT/, '#').gsub(/_+#/, '_#')
  15 + end
  16 +
  17 + def location
  18 + notice.backtrace.lines.first
  19 + end
  20 +end
... ...
app/models/notice.rb
... ... @@ -78,6 +78,17 @@ class Notice
78 78 "N/A"
79 79 end
80 80  
  81 + def to_curl
  82 + return "N/A" if url.blank?
  83 + headers = %w(Accept Accept-Encoding Accept-Language Cookie Referer User-Agent).each_with_object([]) do |name, h|
  84 + if value = env_vars["HTTP_#{name.underscore.upcase}"]
  85 + h << "-H '#{name}: #{value}'"
  86 + end
  87 + end
  88 +
  89 + "curl -X #{env_vars['REQUEST_METHOD'] || 'GET'} #{headers.join(' ')} #{url}"
  90 + end
  91 +
81 92 def env_vars
82 93 request['cgi-data'] || {}
83 94 end
... ...
app/models/notification_services/slack_service.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +class NotificationServices::SlackService < NotificationService
  2 + Label = "slack"
  3 + Fields += [
  4 + [:subdomain, {
  5 + :placeholder => 'subdomain',
  6 + :label => 'Subdomain portion for Slack service'
  7 + }],
  8 + [:api_token, {
  9 + :placeholder => 'Slack Integration Token',
  10 + :label => 'Token'
  11 + }],
  12 + [:room_id, {
  13 + :placeholder => '#general',
  14 + :label => 'Room where Slack should notify'
  15 + }]
  16 + ]
  17 +
  18 + def check_params
  19 + if Fields.detect {|f| self[f[0]].blank? unless f[0] == :room_id }
  20 + errors.add :base, "You must specify your Slack subdomain and token."
  21 + end
  22 + end
  23 +
  24 + def url
  25 + "https://#{subdomain}.slack.com/services/hooks/incoming-webhook?token=#{api_token}"
  26 + end
  27 +
  28 + def message_for_slack(problem)
  29 + "[#{problem.app.name}][#{problem.environment}][#{problem.where}]: #{problem.error_class} #{problem_url(problem)}"
  30 + end
  31 +
  32 + def post_payload(problem)
  33 + payload = {:text => message_for_slack(problem) }
  34 + payload[:channel] = room_id unless room_id.empty?
  35 + payload.to_json
  36 + end
  37 +
  38 + def create_notification(problem)
  39 + HTTParty.post(url, :body => post_payload(problem), :headers => { 'Content-Type' => 'application/json' })
  40 + end
  41 +end
... ...
app/views/apps/_fields.html.haml
... ... @@ -53,9 +53,9 @@
53 53 = w.radio_button :watcher_type, :user
54 54 = w.label :watcher_type_user, 'User'
55 55 = w.radio_button :watcher_type, :email
56   - = w.label :watcher_type_email, 'Email Address'
57   - %div.watcher_params.user{:class => w.object.email_choosen}
58   - = w.select :user_id, User.all.map{|u| [u.name,u.id.to_s]}, :include_blank => '-- Select a User --'
  56 + = label_tag :watcher_type_email, 'Email Address', :for => label_for_attr(w, 'watcher_type_email')
  57 + %div.watcher_params.user{:class => w.object.email.blank? ? 'chosen' : nil}
  58 + = w.select :user_id, users.map{|u| [u.name,u.id.to_s]}, :include_blank => '-- Select a User --'
59 59 %div.watcher_params.email{:class => w.object.email.present? ? 'chosen' : nil}
60 60 = w.text_field :email
61 61  
... ... @@ -65,4 +65,3 @@
65 65  
66 66 = render "issue_tracker_fields", :f => f
67 67 = render "service_notification_fields", :f => f
68   -
... ...
app/views/apps/edit.html.haml
1 1 - content_for :title, t('.title')
2 2 - content_for :action_bar do
3 3 = link_to_copy_attributes_from_other_app
4   - = link_to t('.destroy'), app_path(app), :method => :delete,
  4 + = link_to 'delete all errs', destroy_all_app_problems_path(app), :method => :post,
  5 + :data => { :confirm => t('apps.confirm_destroy_all_problems') }, :class => 'button'
  6 + = link_to 'delete application', app_path(app), :method => :delete,
5 7 :data => { :confirm => t('apps.confirm_delete') }, :class => 'button'
6 8 = link_to(t('.cancel'), app_path(app), :class => 'button')
7 9  
... ...
app/views/devise/mailer/reset_password_instructions.html.haml
... ... @@ -10,7 +10,7 @@
10 10 %p
11 11 We hear you'd like to change your password. You can do that by visiting the link below:
12 12 %p
13   - = edit_password_url @resource, :reset_password_token => @resource.reset_password_token
  13 + = edit_password_url @resource, :reset_password_token => @token
14 14 %tr
15 15 %td.content(valign="top")
16 16 %div
... ...
app/views/devise/mailer/reset_password_instructions.text.erb
... ... @@ -2,7 +2,7 @@ Hello,
2 2  
3 3 We hear you'd like to change your password. You can do that by visiting the link below:
4 4  
5   - <%= edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %>
  5 + <%= edit_password_url(@resource, :reset_password_token => @token) %>
6 6  
7 7 If you didn't request this, please ignore this email. Your password won't change unless you access the link above and create a new one.
8 8  
... ...
app/views/notices/_summary.html.haml
... ... @@ -10,6 +10,9 @@
10 10 %tr
11 11 %th URL
12 12 %td.nowrap= link_to notice.request['url'], notice.request['url']
  13 + %tr
  14 + %th &nbsp;
  15 + %td= notice.to_curl
13 16 %tr
14 17 %th Where
15 18 %td= notice.where
... ...
config/locales/en.yml
... ... @@ -79,11 +79,12 @@ en:
79 79 unresolve: "Unresolve selected issues? They can be resolved again later."
80 80  
81 81 comments:
82   - confirm_delete: "Premanently delete this comment?"
  82 + confirm_delete: "Permanently delete this comment?"
83 83 users:
84 84 confirm_delete: "Permanently delete this user?"
85 85 apps:
86 86 confirm_delete: "Permanently delete this app?"
  87 + confirm_destroy_all_problems: "Permanently delete all of this app's errors?"
87 88 index:
88 89 notify: Notification Service
89 90 tracker: Tracker
... ...
config/routes.rb
... ... @@ -30,6 +30,10 @@ Errbit::Application.routes.draw do
30 30 resources :notices
31 31 resources :comments, :only => [:create, :destroy]
32 32  
  33 + collection do
  34 + post :destroy_all
  35 + end
  36 +
33 37 member do
34 38 put :resolve
35 39 put :unresolve
... ...
db/migrate/20121003223358_extract_backtraces.rb
1 1 class ExtractBacktraces < Mongoid::Migration
2 2 def self.up
3 3 say "It could take long time (hours if you have many Notices)"
4   - Notice.unscoped.all.each do |notice|
  4 + Notice.unscoped.where(backtrace_id: nil).each do |notice|
5 5 next if notice.backtrace.present? || notice['backtrace'].nil?
6 6 backtrace = Backtrace.find_or_create(:raw => notice['backtrace'] || [])
  7 + notice.unset(:backtrace)
7 8 notice.backtrace = backtrace
8   - notice['backtrace'] = nil
9 9 notice.save!
10 10 end
11 11 say "run `db.repairDatabase()` (in mongodb console) to recover deleted space"
... ... @@ -13,4 +13,4 @@ class ExtractBacktraces &lt; Mongoid::Migration
13 13  
14 14 def self.down
15 15 end
16   -end
17 16 \ No newline at end of file
  17 +end
... ...
db/migrate/20121005142110_regenerate_err_fingerprints.rb
1 1 class RegenerateErrFingerprints < Mongoid::Migration
2 2 def self.up
3 3 Err.all.each do |err|
4   - if err.notices.any?
5   - fingerprint_source = {
6   - :backtrace => err.notices.first.backtrace_id,
7   - :error_class => err.error_class,
8   - :component => err.component,
9   - :action => err.action,
10   - :environment => err.environment,
11   - :api_key => err.app.api_key
12   - }
  4 + if err.notices.any? && err.problem
13 5 err.update_attribute(
14 6 :fingerprint,
15 7 Fingerprint.generate(err.notices.first, err.app.api_key)
... ...
lib/tasks/errbit/demo.rake
... ... @@ -49,7 +49,8 @@ namespace :errbit do
49 49 :backtrace => random_backtrace,
50 50 :request => {
51 51 'component' => 'main',
52   - 'action' => 'error'
  52 + 'action' => 'error',
  53 + 'url' => "http://example.com/post/#{[111, 222, 333].sample}",
53 54 },
54 55 :server_environment => {'environment-name' => Rails.env.to_s},
55 56 :notifier => {:name => "seeds.rb"},
... ...
public/javascripts/notifier.js
... ... @@ -870,6 +870,12 @@ printStackTrace.implementation.prototype = {
870 870 // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility)
871 871 Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config);
872 872  
  873 + Global._filters = [];
  874 +
  875 + Global.addFilter = function (cb) {
  876 + Global._filters.push(cb);
  877 + };
  878 +
873 879 function Notifier() {
874 880 this.options = Util.merge({}, Config.options);
875 881 this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData);
... ... @@ -921,7 +927,7 @@ printStackTrace.implementation.prototype = {
921 927 }
922 928  
923 929 return function (error) {
924   - var outputData = '',
  930 + var outputData = '', jsonData,
925 931 url = '';
926 932 //
927 933  
... ... @@ -934,9 +940,12 @@ printStackTrace.implementation.prototype = {
934 940  
935 941 switch (this.options['outputFormat']) {
936 942 case 'XML':
937   - outputData = encodeURIComponent(this.generateXML(this.generateDataJSON(error)));
938   - url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/notifier_api/v2/notices';
939   - _sendGETRequest(url, outputData);
  943 + jsonData = this.generateDataJSON(error);
  944 + if (this.shouldSendData(jsonData)){
  945 + outputData = encodeURIComponent(this.generateXML(jsonData));
  946 + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/notifier_api/v2/notices';
  947 + _sendGETRequest(url, outputData);
  948 + }
940 949 break;
941 950  
942 951 case 'JSON':
... ... @@ -945,9 +954,12 @@ printStackTrace.implementation.prototype = {
945 954 * http://collect.airbrake.io/api/v3/projects/[PROJECT_ID]/notices?key=[API_KEY]
946 955 * url = window.location.protocol + '://' + this.options.host + '/api/v3/projects' + this.options.projectId + '/notices?key=' + this.options.key;
947 956 */
948   - outputData = JSON.stringify(this.generateJSON(this.generateDataJSON(error)));
949   - url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/api/v3/projects/' + this.options.projectId + '/notices?key=' + this.xmlData.key;
950   - _sendPOSTRequest(url, outputData);
  957 + jsonData = this.generateDataJSON(error);
  958 + if (this.shouldSendData(jsonData)){
  959 + outputData = JSON.stringify(this.generateJSON(jsonData));
  960 + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/api/v3/projects/' + this.options.projectId + '/notices?key=' + this.xmlData.key;
  961 + _sendPOSTRequest(url, outputData);
  962 + }
951 963 break;
952 964  
953 965 default:
... ... @@ -1150,6 +1162,13 @@ printStackTrace.implementation.prototype = {
1150 1162 continue;
1151 1163 }
1152 1164  
  1165 + // Special case for sprocket coffee stacktrace:
  1166 + // "Function.foo (http://host/file.js?body=1:666:42)" becomes "Function.foo @http://host/file.js?body=1:666"
  1167 + if (stacktrace[i].match(/\([^\s]+:(\d+):(\d+)\)$/)) {
  1168 + stacktrace[i] = stacktrace[i].replace(/\((.+):(\d+):(\d+)\)$/, '@$1:$2')
  1169 + continue;
  1170 + }
  1171 +
1153 1172 if (stacktrace[i].indexOf('@') === -1) {
1154 1173 stacktrace[i] += '@unsupported.js';
1155 1174 }
... ... @@ -1168,16 +1187,31 @@ printStackTrace.implementation.prototype = {
1168 1187 }
1169 1188  
1170 1189 return true;
  1190 + },
  1191 +
  1192 + shouldSendData: function (jsonData) {
  1193 + var shouldSend = true, i;
  1194 +
  1195 + for ( i = 0; i < Global._filters.length; i++ ) {
  1196 + if ( ! Global._filters[i](jsonData) ){
  1197 + shouldSend = false;
  1198 + }
  1199 + }
  1200 +
  1201 + return shouldSend;
1171 1202 }
1172 1203 };
1173 1204  
  1205 + var oldOnerror = window.onerror;
1174 1206 window.onerror = function (message, file, line, code, error) {
1175 1207 setTimeout(function () {
1176 1208 var e = error || {stack: '()@' + file + ':' + line}
1177 1209 e.message = message
1178 1210 new Notifier().notify(e);
1179 1211 }, 0);
1180   -
  1212 + if (oldOnerror) {
  1213 + return oldOnerror(message, file, line, code, error);
  1214 + }
1181 1215 return true;
1182 1216 };
1183 1217 })();
... ...
spec/acceptance/reset_password_token.rb 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +require 'acceptance/acceptance_helper'
  2 +
  3 +feature 'password reset token' do
  4 + let(:user) { Fabricate :user }
  5 +
  6 + scenario 'receives correct password reset token' do
  7 + host = ActionMailer::Base.default_url_options.values_at(:host).first
  8 + port = ActionMailer::Base.default_url_options.values_at(:port).first
  9 + port = port.blank? ? '' : ':' + port
  10 + regex = %r{http://#{host}#{port}/users/password/edit\?reset_password_token=([A-Za-z0-9\-_]+)}
  11 +
  12 + visit 'https://brighterr.herokuapp.com/users/password/new'
  13 + fill_in 'Email', with: user.email
  14 + click_button 'Send me reset password instructions'
  15 + expect(page).to have_content I18n.t('devise.passwords.send_instructions')
  16 +
  17 + mail = ActionMailer::Base.deliveries.last
  18 + expect(mail.subject).to match(/Reset password instructions/)
  19 + expect(mail.body.encoded).to_not be_empty
  20 + expect(mail.body.encoded).to match(/change your password/)
  21 + expect(mail.body.encoded).to match(regex)
  22 + if mail.body.encoded =~ regex
  23 + visit "/users/password/edit?reset_password_token=#{$1}"
  24 + expect(page).to have_content 'Change your password'
  25 + fill_in 'New password', with: 'test12345'
  26 + fill_in 'Type your new password again', with: 'test12345'
  27 + click_button 'Change my password'
  28 + expect(page).to_not have_content 'Reset password token is invalid'
  29 + end
  30 + end
  31 +end
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -73,6 +73,16 @@ describe AppsController do
73 73 expect(response).to be_success
74 74 end
75 75  
  76 + it "should list available watchers by name" do
  77 + Fabricate(:user, :name => "Carol")
  78 + Fabricate(:user, :name => "Alice")
  79 + Fabricate(:user, :name => "Betty")
  80 +
  81 + get :show, :id => app.id
  82 +
  83 + expect(controller.users.to_a).to eq(User.all.to_a.sort_by(&:name))
  84 + end
  85 +
76 86 context "pagination" do
77 87 before(:each) do
78 88 35.times { Fabricate(:err, :problem => Fabricate(:problem, :app => app)) }
... ... @@ -395,4 +405,3 @@ describe AppsController do
395 405 end
396 406  
397 407 end
398   -
... ...
spec/controllers/problems_controller_spec.rb
... ... @@ -390,6 +390,34 @@ describe ProblemsController do
390 390 }.to change(Problem, :count).by(-1)
391 391 end
392 392 end
  393 +
  394 + describe "POST /apps/:app_id/problems/destroy_all" do
  395 + before do
  396 + sign_in Fabricate(:admin)
  397 + @app = Fabricate(:app)
  398 + @problem1 = Fabricate(:problem, :app=>@app)
  399 + @problem2 = Fabricate(:problem, :app=>@app)
  400 + end
  401 +
  402 + it "destroys all problems" do
  403 + expect {
  404 + post :destroy_all, :app_id => @app.id
  405 + }.to change(Problem, :count).by(-2)
  406 + expect(controller.app).to eq @app
  407 + end
  408 +
  409 + it "should display a message" do
  410 + put :destroy_all, :app_id => @app.id
  411 + expect(request.flash[:success]).to match(/been deleted/)
  412 + end
  413 +
  414 + it "should redirect back to the app page" do
  415 + request.env["Referer"] = edit_app_path(@app)
  416 + put :destroy_all, :app_id => @app.id
  417 + expect(response).to redirect_to(edit_app_path(@app))
  418 + end
  419 + end
  420 +
393 421 end
394 422  
395 423 end
... ...
spec/fabricators/notification_service_fabricator.rb
... ... @@ -12,6 +12,6 @@ Fabricator :gtalk_notification_service, :from =&gt; :notification_service, :class_n
12 12 service { sequence :word }
13 13 end
14 14  
15   -%w(campfire flowdock hipchat hoiio hubot pushover webhook).each do |t|
  15 +%w(campfire flowdock hipchat hoiio hubot pushover slack webhook).each do |t|
16 16 Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service"
17 17 end
... ...
spec/models/error_report_spec.rb
... ... @@ -47,6 +47,19 @@ describe ErrorReport do
47 47 end
48 48 end
49 49  
  50 + describe "#fingerprint_strategy" do
  51 + after(:all) {
  52 + error_report.fingerprint_strategy = Fingerprint
  53 + }
  54 +
  55 + it "should be possible to change how fingerprints are generated" do
  56 + strategy = double()
  57 + strategy.should_receive(:generate){ 'fingerprints' }
  58 + error_report.fingerprint_strategy = strategy
  59 + error_report.generate_notice!
  60 + end
  61 + end
  62 +
50 63 describe "#generate_notice!" do
51 64 it "save a notice" do
52 65 expect {
... ...
spec/models/fingerprints/legacy_fingerprint_spec.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +require 'spec_helper'
  2 +
  3 +describe LegacyFingerprint do
  4 + context 'being created' do
  5 + let(:backtrace) do
  6 + Backtrace.create(:raw => [
  7 + {
  8 + "number"=>"17",
  9 + "file"=>"[GEM_ROOT]/gems/activesupport/lib/active_support/callbacks.rb",
  10 + "method"=>"_run__2497084960985961383__process_action__2062871603614456254__callbacks"
  11 + }
  12 + ])
  13 + end
  14 + let(:notice1) { Fabricate.build(:notice, :backtrace => backtrace) }
  15 + let(:notice2) { Fabricate.build(:notice, :backtrace => backtrace_2) }
  16 +
  17 + context "with same backtrace" do
  18 + let(:backtrace_2) do
  19 + backtrace
  20 + backtrace.lines.last.method = '_run__FRAGMENT__process_action__FRAGMENT__callbacks'
  21 + backtrace.save
  22 + backtrace
  23 + end
  24 +
  25 + it "normalizes the fingerprint of generated methods" do
  26 + expect(LegacyFingerprint.generate(notice1, "api key")).to eql LegacyFingerprint.generate(notice2, "api key")
  27 + end
  28 + end
  29 +
  30 + context "with same backtrace where FRAGMENT has not been extracted" do
  31 + let(:backtrace_2) do
  32 + backtrace
  33 + backtrace.lines.last.method = '_run__998857585768765__process_action__1231231312321313__callbacks'
  34 + backtrace.save
  35 + backtrace
  36 + end
  37 +
  38 + it "normalizes the fingerprint of generated methods" do
  39 + expect(LegacyFingerprint.generate(notice1, "api key")).to eql LegacyFingerprint.generate(notice2, "api key")
  40 + end
  41 + end
  42 + end
  43 +end
... ...
spec/models/notice_spec.rb
... ... @@ -35,6 +35,28 @@ describe Notice do
35 35 end
36 36 end
37 37  
  38 + describe "to_curl" do
  39 + let(:notice) { Fabricate.build(:notice, request: request) }
  40 +
  41 + context "when it has a request url" do
  42 + let(:request) { {'url' => "http://example.com/resource/12", 'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0'}} }
  43 +
  44 + it 'has a curl representation' do
  45 + cmd = notice.to_curl
  46 + expect(cmd).to eq(%q[curl -X GET -H 'User-Agent: Mozilla/5.0' http://example.com/resource/12])
  47 + end
  48 + end
  49 +
  50 + context "when it has not a request url" do
  51 + let(:request) { {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0'}} }
  52 +
  53 + it 'has a curl representation' do
  54 + cmd = notice.to_curl
  55 + expect(cmd).to eq "N/A"
  56 + end
  57 + end
  58 + end
  59 +
38 60 describe "user agent" do
39 61 it "should be parsed and human-readable" do
40 62 notice = Fabricate.build(:notice, :request => {'cgi-data' => {
... ...
spec/models/notification_service/slack_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::SlackService do
  4 + it "it should send a notification to Slack with channel" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :slack_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + # faraday stubbing
  11 + payload = {:text => notification_service.message_for_slack(problem), :channel => notification_service.room_id}.to_json
  12 + expect(HTTParty).to receive(:post).with(notification_service.url, :body => payload, :headers => {"Content-Type" => "application/json"}).and_return(true)
  13 +
  14 + notification_service.create_notification(problem)
  15 + end
  16 +
  17 + it "it should send a notification to Slack without a channel" do
  18 + # setup
  19 + notice = Fabricate :notice
  20 + notification_service = Fabricate :slack_notification_service, :app => notice.app, :room_id => ""
  21 + problem = notice.problem
  22 +
  23 + # faraday stubbing
  24 + payload = {:text => notification_service.message_for_slack(problem)}.to_json
  25 + expect(HTTParty).to receive(:post).with(notification_service.url, :body => payload, :headers => {"Content-Type" => "application/json"}).and_return(true)
  26 +
  27 + notification_service.create_notification(problem)
  28 + end
  29 +end
... ...
spec/views/apps/edit.html.haml_spec.rb
... ... @@ -14,6 +14,11 @@ describe &quot;apps/edit.html.haml&quot; do
14 14 view.content_for(:action_bar)
15 15 end
16 16  
  17 + it "should confirm the 'reset' link" do
  18 + render
  19 + expect(action_bar).to have_selector('a.button[data-confirm="%s"]' % I18n.t('apps.confirm_destroy_all_problems'))
  20 + end
  21 +
17 22 it "should confirm the 'destroy' link" do
18 23 render
19 24 expect(action_bar).to have_selector('a.button[data-confirm="%s"]' % I18n.t('apps.confirm_delete'))
... ...