Commit 9523305afc7cc37ec1ad77aede09f3b9a196d1e0
Exists in
master
and in
1 other branch
fixed conflict
Showing
62 changed files
with
1534 additions
and
346 deletions
Show diff stats
Gemfile
1 | source 'http://rubygems.org' | 1 | source 'http://rubygems.org' |
2 | 2 | ||
3 | -gem 'rails', '3.2.11' | 3 | +gem 'rails', '3.2.12' |
4 | gem 'mongoid', '~> 2.4.10' | 4 | gem 'mongoid', '~> 2.4.10' |
5 | gem 'mongoid_rails_migrations' | 5 | gem 'mongoid_rails_migrations' |
6 | gem 'devise', '~> 1.5.4' | 6 | gem 'devise', '~> 1.5.4' |
@@ -8,7 +8,7 @@ gem 'haml' | @@ -8,7 +8,7 @@ gem 'haml' | ||
8 | gem 'htmlentities', "~> 4.3.0" | 8 | gem 'htmlentities', "~> 4.3.0" |
9 | gem 'rack-ssl', :require => 'rack/ssl' # force SSL | 9 | gem 'rack-ssl', :require => 'rack/ssl' # force SSL |
10 | 10 | ||
11 | -gem 'useragent', '~> 0.3.1' | 11 | +gem 'useragent', '~> 0.4.16' |
12 | gem 'inherited_resources' | 12 | gem 'inherited_resources' |
13 | gem 'SystemTimer', :platform => :ruby_18 | 13 | gem 'SystemTimer', :platform => :ruby_18 |
14 | gem 'actionmailer_inline_css', "~> 1.3.0" | 14 | gem 'actionmailer_inline_css', "~> 1.3.0" |
@@ -83,6 +83,11 @@ group :development, :test do | @@ -83,6 +83,11 @@ group :development, :test do | ||
83 | # gem 'rpm_contrib' | 83 | # gem 'rpm_contrib' |
84 | # gem 'newrelic_rpm' | 84 | # gem 'newrelic_rpm' |
85 | gem 'capistrano' | 85 | gem 'capistrano' |
86 | + | ||
87 | + # better errors | ||
88 | + gem 'better_errors', :platform => :ruby_19 | ||
89 | + gem 'binding_of_caller', :platform => :ruby_19 | ||
90 | + gem 'meta_request', :platform => :ruby_19 | ||
86 | end | 91 | end |
87 | 92 | ||
88 | gem 'foreman', :group => :development | 93 | gem 'foreman', :group => :development |
Gemfile.lock
@@ -9,40 +9,45 @@ GEM | @@ -9,40 +9,45 @@ GEM | ||
9 | remote: http://rubygems.org/ | 9 | remote: http://rubygems.org/ |
10 | specs: | 10 | specs: |
11 | SystemTimer (1.2.3) | 11 | SystemTimer (1.2.3) |
12 | - actionmailer (3.2.11) | ||
13 | - actionpack (= 3.2.11) | 12 | + actionmailer (3.2.12) |
13 | + actionpack (= 3.2.12) | ||
14 | mail (~> 2.4.4) | 14 | mail (~> 2.4.4) |
15 | actionmailer_inline_css (1.3.1) | 15 | actionmailer_inline_css (1.3.1) |
16 | actionmailer (>= 3.0.0) | 16 | actionmailer (>= 3.0.0) |
17 | nokogiri (>= 1.4.4) | 17 | nokogiri (>= 1.4.4) |
18 | premailer (>= 1.7.1) | 18 | premailer (>= 1.7.1) |
19 | - actionpack (3.2.11) | ||
20 | - activemodel (= 3.2.11) | ||
21 | - activesupport (= 3.2.11) | 19 | + actionpack (3.2.12) |
20 | + activemodel (= 3.2.12) | ||
21 | + activesupport (= 3.2.12) | ||
22 | builder (~> 3.0.0) | 22 | builder (~> 3.0.0) |
23 | erubis (~> 2.7.0) | 23 | erubis (~> 2.7.0) |
24 | journey (~> 1.0.4) | 24 | journey (~> 1.0.4) |
25 | - rack (~> 1.4.0) | 25 | + rack (~> 1.4.5) |
26 | rack-cache (~> 1.2) | 26 | rack-cache (~> 1.2) |
27 | rack-test (~> 0.6.1) | 27 | rack-test (~> 0.6.1) |
28 | sprockets (~> 2.2.1) | 28 | sprockets (~> 2.2.1) |
29 | - activemodel (3.2.11) | ||
30 | - activesupport (= 3.2.11) | 29 | + activemodel (3.2.12) |
30 | + activesupport (= 3.2.12) | ||
31 | builder (~> 3.0.0) | 31 | builder (~> 3.0.0) |
32 | - activerecord (3.2.11) | ||
33 | - activemodel (= 3.2.11) | ||
34 | - activesupport (= 3.2.11) | 32 | + activerecord (3.2.12) |
33 | + activemodel (= 3.2.12) | ||
34 | + activesupport (= 3.2.12) | ||
35 | arel (~> 3.0.2) | 35 | arel (~> 3.0.2) |
36 | tzinfo (~> 0.3.29) | 36 | tzinfo (~> 0.3.29) |
37 | - activeresource (3.2.11) | ||
38 | - activemodel (= 3.2.11) | ||
39 | - activesupport (= 3.2.11) | ||
40 | - activesupport (3.2.11) | 37 | + activeresource (3.2.12) |
38 | + activemodel (= 3.2.12) | ||
39 | + activesupport (= 3.2.12) | ||
40 | + activesupport (3.2.12) | ||
41 | i18n (~> 0.6) | 41 | i18n (~> 0.6) |
42 | multi_json (~> 1.0) | 42 | multi_json (~> 1.0) |
43 | addressable (2.3.2) | 43 | addressable (2.3.2) |
44 | arel (3.0.2) | 44 | arel (3.0.2) |
45 | bcrypt-ruby (3.0.1) | 45 | bcrypt-ruby (3.0.1) |
46 | + better_errors (0.7.0) | ||
47 | + coderay (>= 1.0.0) | ||
48 | + erubis (>= 2.6.6) | ||
49 | + binding_of_caller (0.7.1) | ||
50 | + debug_inspector (>= 0.0.1) | ||
46 | bitbucket_rest_api (0.1.1) | 51 | bitbucket_rest_api (0.1.1) |
47 | faraday (~> 0.8.1) | 52 | faraday (~> 0.8.1) |
48 | faraday_middleware (~> 0.8.1) | 53 | faraday_middleware (~> 0.8.1) |
@@ -79,13 +84,14 @@ GEM | @@ -79,13 +84,14 @@ GEM | ||
79 | rdoc | 84 | rdoc |
80 | daemons (1.1.8) | 85 | daemons (1.1.8) |
81 | database_cleaner (0.6.7) | 86 | database_cleaner (0.6.7) |
82 | - debugger (1.2.1) | 87 | + debug_inspector (0.0.2) |
88 | + debugger (1.3.0) | ||
83 | columnize (>= 0.3.1) | 89 | columnize (>= 0.3.1) |
84 | debugger-linecache (~> 1.1.1) | 90 | debugger-linecache (~> 1.1.1) |
85 | - debugger-ruby_core_source (~> 1.1.4) | 91 | + debugger-ruby_core_source (~> 1.1.7) |
86 | debugger-linecache (1.1.2) | 92 | debugger-linecache (1.1.2) |
87 | debugger-ruby_core_source (>= 1.1.1) | 93 | debugger-ruby_core_source (>= 1.1.1) |
88 | - debugger-ruby_core_source (1.1.4) | 94 | + debugger-ruby_core_source (1.1.7) |
89 | devise (1.5.4) | 95 | devise (1.5.4) |
90 | bcrypt-ruby (~> 3.0) | 96 | bcrypt-ruby (~> 3.0) |
91 | orm_adapter (~> 0.0.3) | 97 | orm_adapter (~> 0.0.3) |
@@ -131,7 +137,7 @@ GEM | @@ -131,7 +137,7 @@ GEM | ||
131 | has_scope (~> 0.5.0) | 137 | has_scope (~> 0.5.0) |
132 | responders (~> 0.6) | 138 | responders (~> 0.6) |
133 | journey (1.0.4) | 139 | journey (1.0.4) |
134 | - json (1.7.6) | 140 | + json (1.7.7) |
135 | jwt (0.1.5) | 141 | jwt (0.1.5) |
136 | multi_json (>= 1.0) | 142 | multi_json (>= 1.0) |
137 | kaminari (0.14.1) | 143 | kaminari (0.14.1) |
@@ -140,7 +146,7 @@ GEM | @@ -140,7 +146,7 @@ GEM | ||
140 | kgio (2.7.4) | 146 | kgio (2.7.4) |
141 | launchy (2.1.2) | 147 | launchy (2.1.2) |
142 | addressable (~> 2.3) | 148 | addressable (~> 2.3) |
143 | - libv8 (3.3.10.4) | 149 | + libv8 (3.11.8.13) |
144 | libwebsocket (0.1.5) | 150 | libwebsocket (0.1.5) |
145 | addressable | 151 | addressable |
146 | libxml-ruby (2.3.3) | 152 | libxml-ruby (2.3.3) |
@@ -153,8 +159,11 @@ GEM | @@ -153,8 +159,11 @@ GEM | ||
153 | i18n (>= 0.4.0) | 159 | i18n (>= 0.4.0) |
154 | mime-types (~> 1.16) | 160 | mime-types (~> 1.16) |
155 | treetop (~> 1.4.8) | 161 | treetop (~> 1.4.8) |
162 | + meta_request (0.2.2) | ||
163 | + rack-contrib | ||
164 | + railties | ||
156 | method_source (0.7.1) | 165 | method_source (0.7.1) |
157 | - mime-types (1.19) | 166 | + mime-types (1.21) |
158 | mongo (1.6.2) | 167 | mongo (1.6.2) |
159 | bson (~> 1.6.2) | 168 | bson (~> 1.6.2) |
160 | mongoid (2.4.10) | 169 | mongoid (2.4.10) |
@@ -166,7 +175,7 @@ GEM | @@ -166,7 +175,7 @@ GEM | ||
166 | bundler (>= 1.0.0) | 175 | bundler (>= 1.0.0) |
167 | rails (>= 3.0.0) | 176 | rails (>= 3.0.0) |
168 | railties (>= 3.0.0) | 177 | railties (>= 3.0.0) |
169 | - multi_json (1.5.0) | 178 | + multi_json (1.5.1) |
170 | multi_xml (0.5.2) | 179 | multi_xml (0.5.2) |
171 | multipart-post (1.1.5) | 180 | multipart-post (1.1.5) |
172 | net-scp (1.0.4) | 181 | net-scp (1.0.4) |
@@ -220,27 +229,29 @@ GEM | @@ -220,27 +229,29 @@ GEM | ||
220 | slop (>= 2.4.4, < 3) | 229 | slop (>= 2.4.4, < 3) |
221 | pry-rails (0.2.0) | 230 | pry-rails (0.2.0) |
222 | pry | 231 | pry |
223 | - rack (1.4.3) | 232 | + rack (1.4.5) |
224 | rack-cache (1.2) | 233 | rack-cache (1.2) |
225 | rack (>= 0.4) | 234 | rack (>= 0.4) |
226 | - rack-ssl (1.3.2) | 235 | + rack-contrib (1.1.0) |
236 | + rack (>= 0.9.1) | ||
237 | + rack-ssl (1.3.3) | ||
227 | rack | 238 | rack |
228 | rack-ssl-enforcer (0.2.4) | 239 | rack-ssl-enforcer (0.2.4) |
229 | rack-test (0.6.2) | 240 | rack-test (0.6.2) |
230 | rack (>= 1.0) | 241 | rack (>= 1.0) |
231 | - rails (3.2.11) | ||
232 | - actionmailer (= 3.2.11) | ||
233 | - actionpack (= 3.2.11) | ||
234 | - activerecord (= 3.2.11) | ||
235 | - activeresource (= 3.2.11) | ||
236 | - activesupport (= 3.2.11) | 242 | + rails (3.2.12) |
243 | + actionmailer (= 3.2.12) | ||
244 | + actionpack (= 3.2.12) | ||
245 | + activerecord (= 3.2.12) | ||
246 | + activeresource (= 3.2.12) | ||
247 | + activesupport (= 3.2.12) | ||
237 | bundler (~> 1.0) | 248 | bundler (~> 1.0) |
238 | - railties (= 3.2.11) | 249 | + railties (= 3.2.12) |
239 | rails_autolink (1.0.9) | 250 | rails_autolink (1.0.9) |
240 | rails (~> 3.1) | 251 | rails (~> 3.1) |
241 | - railties (3.2.11) | ||
242 | - actionpack (= 3.2.11) | ||
243 | - activesupport (= 3.2.11) | 252 | + railties (3.2.12) |
253 | + actionpack (= 3.2.12) | ||
254 | + activesupport (= 3.2.12) | ||
244 | rack-ssl (~> 1.3.2) | 255 | rack-ssl (~> 1.3.2) |
245 | rake (>= 0.8.7) | 256 | rake (>= 0.8.7) |
246 | rdoc (~> 3.4) | 257 | rdoc (~> 3.4) |
@@ -248,8 +259,9 @@ GEM | @@ -248,8 +259,9 @@ GEM | ||
248 | raindrops (0.10.0) | 259 | raindrops (0.10.0) |
249 | rake (10.0.3) | 260 | rake (10.0.3) |
250 | rbx-require-relative (0.0.9) | 261 | rbx-require-relative (0.0.9) |
251 | - rdoc (3.12) | 262 | + rdoc (3.12.1) |
252 | json (~> 1.4) | 263 | json (~> 1.4) |
264 | + ref (1.0.2) | ||
253 | responders (0.9.2) | 265 | responders (0.9.2) |
254 | railties (~> 3.1) | 266 | railties (~> 3.1) |
255 | rest-client (1.6.7) | 267 | rest-client (1.6.7) |
@@ -276,7 +288,7 @@ GEM | @@ -276,7 +288,7 @@ GEM | ||
276 | ruby-fogbugz (0.1.1) | 288 | ruby-fogbugz (0.1.1) |
277 | crack | 289 | crack |
278 | rubyzip (0.9.9) | 290 | rubyzip (0.9.9) |
279 | - rushover (0.1.1) | 291 | + rushover (0.3.0) |
280 | json | 292 | json |
281 | rest-client | 293 | rest-client |
282 | selenium-webdriver (2.25.0) | 294 | selenium-webdriver (2.25.0) |
@@ -291,13 +303,14 @@ GEM | @@ -291,13 +303,14 @@ GEM | ||
291 | multi_json (~> 1.0) | 303 | multi_json (~> 1.0) |
292 | rack (~> 1.0) | 304 | rack (~> 1.0) |
293 | tilt (~> 1.1, != 1.3.0) | 305 | tilt (~> 1.1, != 1.3.0) |
294 | - therubyracer (0.10.2) | ||
295 | - libv8 (~> 3.3.10) | 306 | + therubyracer (0.11.4) |
307 | + libv8 (~> 3.11.8.12) | ||
308 | + ref | ||
296 | thin (1.4.1) | 309 | thin (1.4.1) |
297 | daemons (>= 1.0.9) | 310 | daemons (>= 1.0.9) |
298 | eventmachine (>= 0.12.6) | 311 | eventmachine (>= 0.12.6) |
299 | rack (>= 1.0.0) | 312 | rack (>= 1.0.0) |
300 | - thor (0.16.0) | 313 | + thor (0.17.0) |
301 | tilt (1.3.3) | 314 | tilt (1.3.3) |
302 | timecop (0.3.5) | 315 | timecop (0.3.5) |
303 | treetop (1.4.12) | 316 | treetop (1.4.12) |
@@ -315,7 +328,7 @@ GEM | @@ -315,7 +328,7 @@ GEM | ||
315 | kgio (~> 2.6) | 328 | kgio (~> 2.6) |
316 | rack | 329 | rack |
317 | raindrops (~> 0.7) | 330 | raindrops (~> 0.7) |
318 | - useragent (0.3.2) | 331 | + useragent (0.4.16) |
319 | warden (1.2.1) | 332 | warden (1.2.1) |
320 | rack (>= 1.0) | 333 | rack (>= 1.0) |
321 | webmock (1.8.7) | 334 | webmock (1.8.7) |
@@ -332,6 +345,8 @@ PLATFORMS | @@ -332,6 +345,8 @@ PLATFORMS | ||
332 | DEPENDENCIES | 345 | DEPENDENCIES |
333 | SystemTimer | 346 | SystemTimer |
334 | actionmailer_inline_css (~> 1.3.0) | 347 | actionmailer_inline_css (~> 1.3.0) |
348 | + better_errors | ||
349 | + binding_of_caller | ||
335 | bitbucket_rest_api | 350 | bitbucket_rest_api |
336 | bson (= 1.6.2) | 351 | bson (= 1.6.2) |
337 | bson_ext (= 1.6.2) | 352 | bson_ext (= 1.6.2) |
@@ -356,6 +371,7 @@ DEPENDENCIES | @@ -356,6 +371,7 @@ DEPENDENCIES | ||
356 | kaminari (>= 0.14.1) | 371 | kaminari (>= 0.14.1) |
357 | launchy | 372 | launchy |
358 | lighthouse-api | 373 | lighthouse-api |
374 | + meta_request | ||
359 | mongo (= 1.6.2) | 375 | mongo (= 1.6.2) |
360 | mongoid (~> 2.4.10) | 376 | mongoid (~> 2.4.10) |
361 | mongoid_rails_migrations | 377 | mongoid_rails_migrations |
@@ -366,7 +382,7 @@ DEPENDENCIES | @@ -366,7 +382,7 @@ DEPENDENCIES | ||
366 | pry-rails | 382 | pry-rails |
367 | rack-ssl | 383 | rack-ssl |
368 | rack-ssl-enforcer | 384 | rack-ssl-enforcer |
369 | - rails (= 3.2.11) | 385 | + rails (= 3.2.12) |
370 | rails_autolink (~> 1.0.9) | 386 | rails_autolink (~> 1.0.9) |
371 | ri_cal | 387 | ri_cal |
372 | rspec-rails (~> 2.6) | 388 | rspec-rails (~> 2.6) |
@@ -380,7 +396,7 @@ DEPENDENCIES | @@ -380,7 +396,7 @@ DEPENDENCIES | ||
380 | uglifier (>= 1.0.3) | 396 | uglifier (>= 1.0.3) |
381 | underscore-rails | 397 | underscore-rails |
382 | unicorn | 398 | unicorn |
383 | - useragent (~> 0.3.1) | 399 | + useragent (~> 0.4.16) |
384 | webmock | 400 | webmock |
385 | xmpp4r | 401 | xmpp4r |
386 | yajl-ruby | 402 | yajl-ruby |
README.md
@@ -443,6 +443,13 @@ TODO | @@ -443,6 +443,13 @@ TODO | ||
443 | * Add ability for watchers to be configured for types of notifications they should receive | 443 | * Add ability for watchers to be configured for types of notifications they should receive |
444 | 444 | ||
445 | 445 | ||
446 | +People using Errbit | ||
447 | +------------------- | ||
448 | + | ||
449 | +See our wiki page for a [list of people and companies around the world who use Errbit](https://github.com/errbit/errbit/wiki/People-using-Errbit). | ||
450 | +Feel free to [edit this page](https://github.com/errbit/errbit/wiki/People-using-Errbit/_edit), and add your name and country to the list if you are using Errbit. | ||
451 | + | ||
452 | + | ||
446 | Special Thanks | 453 | Special Thanks |
447 | -------------- | 454 | -------------- |
448 | 455 |
3.16 KB
3.16 KB
2.49 KB
app/assets/stylesheets/errbit.css
@@ -654,7 +654,6 @@ table.errs td.message a { | @@ -654,7 +654,6 @@ table.errs td.message a { | ||
654 | overflow: hidden; | 654 | overflow: hidden; |
655 | text-overflow: ellipsis; | 655 | text-overflow: ellipsis; |
656 | -o-text-overflow: ellipsis; | 656 | -o-text-overflow: ellipsis; |
657 | - white-space: nowrap; | ||
658 | /* ------ */ | 657 | /* ------ */ |
659 | } | 658 | } |
660 | table.errs td.message em { | 659 | table.errs td.message em { |
@@ -0,0 +1,41 @@ | @@ -0,0 +1,41 @@ | ||
1 | +class Api::V1::StatsController < ApplicationController | ||
2 | + respond_to :json, :xml | ||
3 | + | ||
4 | + # The stats API only requires an api_key for the given app. | ||
5 | + skip_before_filter :authenticate_user! | ||
6 | + before_filter :require_api_key_or_authenticate_user! | ||
7 | + | ||
8 | + def app | ||
9 | + if problem = @app.problems.order_by(:last_notice_at.desc).first | ||
10 | + @last_error_time = problem.last_notice_at | ||
11 | + end | ||
12 | + | ||
13 | + stats = { | ||
14 | + :name => @app.name, | ||
15 | + :last_error_time => @last_error_time, | ||
16 | + :unresolved_errors => @app.unresolved_count | ||
17 | + } | ||
18 | + | ||
19 | + 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) } | ||
22 | + format.xml { render :xml => stats } | ||
23 | + end | ||
24 | + end | ||
25 | + | ||
26 | + | ||
27 | + protected | ||
28 | + | ||
29 | + def require_api_key_or_authenticate_user! | ||
30 | + if params[:api_key].present? | ||
31 | + if @app = App.where(:api_key => params[:api_key]).first | ||
32 | + return true | ||
33 | + end | ||
34 | + end | ||
35 | + | ||
36 | + authenticate_user! | ||
37 | + end | ||
38 | + | ||
39 | +end | ||
40 | + | ||
41 | + |
app/helpers/application_helper.rb
@@ -57,7 +57,7 @@ module ApplicationHelper | @@ -57,7 +57,7 @@ module ApplicationHelper | ||
57 | total = (options[:total] || total_from_tallies(tallies)) | 57 | total = (options[:total] || total_from_tallies(tallies)) |
58 | percent = 100.0 / total.to_f | 58 | percent = 100.0 / total.to_f |
59 | rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ | 59 | rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ |
60 | - .sort {|a, b| a[0] <=> b[0]} | 60 | + .sort {|a, b| b[0] <=> a[0]} |
61 | render "problems/tally_table", :rows => rows | 61 | render "problems/tally_table", :rows => rows |
62 | end | 62 | end |
63 | 63 |
app/helpers/backtrace_line_helper.rb
1 | module BacktraceLineHelper | 1 | module BacktraceLineHelper |
2 | def link_to_source_file(line, &block) | 2 | def link_to_source_file(line, &block) |
3 | text = capture_haml(&block) | 3 | text = capture_haml(&block) |
4 | - line.in_app? ? link_to_in_app_source_file(line, text) : link_to_external_source_file(text) | 4 | + link_to_in_app_source_file(line, text) || link_to_external_source_file(text) |
5 | end | 5 | end |
6 | 6 | ||
7 | private | 7 | private |
8 | def link_to_in_app_source_file(line, text) | 8 | def link_to_in_app_source_file(line, text) |
9 | - link_to_repo_source_file(line, text) || link_to_issue_tracker_file(line, text) | 9 | + return unless line.in_app? |
10 | + if line.file_name =~ /\.js$/ | ||
11 | + link_to_hosted_javascript(line, text) | ||
12 | + else | ||
13 | + link_to_repo_source_file(line, text) || | ||
14 | + link_to_issue_tracker_file(line, text) | ||
15 | + end | ||
10 | end | 16 | end |
11 | 17 | ||
12 | def link_to_repo_source_file(line, text) | 18 | def link_to_repo_source_file(line, text) |
13 | link_to_github(line, text) || link_to_bitbucket(line, text) | 19 | link_to_github(line, text) || link_to_bitbucket(line, text) |
14 | end | 20 | end |
15 | 21 | ||
22 | + def link_to_hosted_javascript(line, text) | ||
23 | + if line.app.asset_host? | ||
24 | + link_to(text, "#{line.app.asset_host}/#{line.file_relative}", :target => '_blank') | ||
25 | + end | ||
26 | + end | ||
27 | + | ||
16 | def link_to_external_source_file(text) | 28 | def link_to_external_source_file(text) |
17 | text | 29 | text |
18 | end | 30 | end |
@@ -31,7 +43,7 @@ module BacktraceLineHelper | @@ -31,7 +43,7 @@ module BacktraceLineHelper | ||
31 | 43 | ||
32 | def link_to_issue_tracker_file(line, text = nil) | 44 | def link_to_issue_tracker_file(line, text = nil) |
33 | return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file) | 45 | return unless line.app.issue_tracker && line.app.issue_tracker.respond_to?(:url_to_file) |
34 | - href = line.app.issue_tracker.url_to_file(line.file, line.number) | 46 | + href = line.app.issue_tracker.url_to_file(line.file_relative, line.number) |
35 | link_to(text || line.file_name, href, :target => '_blank') | 47 | link_to(text || line.file_name, href, :target => '_blank') |
36 | end | 48 | end |
37 | 49 |
app/mailers/mailer.rb
@@ -3,14 +3,20 @@ | @@ -3,14 +3,20 @@ | ||
3 | require Rails.root.join('config/routes.rb') | 3 | require Rails.root.join('config/routes.rb') |
4 | 4 | ||
5 | class Mailer < ActionMailer::Base | 5 | class Mailer < ActionMailer::Base |
6 | + helper ApplicationHelper | ||
7 | + helper BacktraceLineHelper | ||
8 | + | ||
6 | default :from => Errbit::Config.email_from | 9 | default :from => Errbit::Config.email_from |
7 | 10 | ||
8 | def err_notification(notice) | 11 | def err_notification(notice) |
9 | @notice = notice | 12 | @notice = notice |
10 | @app = notice.app | 13 | @app = notice.app |
11 | 14 | ||
15 | + count = @notice.similar_count | ||
16 | + count = count > 1 ? "(#{count}) " : "" | ||
17 | + | ||
12 | mail :to => @app.notification_recipients, | 18 | mail :to => @app.notification_recipients, |
13 | - :subject => "[#{@app.name}][#{@notice.environment_name}] #{@notice.message.truncate(50)}" | 19 | + :subject => "#{count}[#{@app.name}][#{@notice.environment_name}] #{@notice.message.truncate(50)}" |
14 | end | 20 | end |
15 | 21 | ||
16 | def deploy_notification(deploy) | 22 | def deploy_notification(deploy) |
@@ -20,5 +26,18 @@ class Mailer < ActionMailer::Base | @@ -20,5 +26,18 @@ class Mailer < ActionMailer::Base | ||
20 | mail :to => @app.notification_recipients, | 26 | mail :to => @app.notification_recipients, |
21 | :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}" | 27 | :subject => "[#{@app.name}] Deployed to #{@deploy.environment} by #{@deploy.username}" |
22 | end | 28 | end |
23 | -end | ||
24 | 29 | ||
30 | + def comment_notification(comment) | ||
31 | + @comment = comment | ||
32 | + @user = comment.user | ||
33 | + @problem = comment.err | ||
34 | + @notice = @problem.notices.first | ||
35 | + @app = @problem.app | ||
36 | + | ||
37 | + # Don't send comment notification to user who posted the comment | ||
38 | + recipients = @app.notification_recipients - [comment.user.email] | ||
39 | + | ||
40 | + mail :to => recipients, | ||
41 | + :subject => "#{@user.name} commented on [#{@app.name}][#{@notice.environment_name}] #{@notice.message.truncate(50)}" | ||
42 | + end | ||
43 | +end |
app/models/app.rb
1 | class App | 1 | class App |
2 | + include Comparable | ||
2 | include Mongoid::Document | 3 | include Mongoid::Document |
3 | include Mongoid::Timestamps | 4 | include Mongoid::Timestamps |
4 | - include Comparable | ||
5 | 5 | ||
6 | field :name, :type => String | 6 | field :name, :type => String |
7 | field :api_key | 7 | field :api_key |
8 | field :github_repo | 8 | field :github_repo |
9 | field :bitbucket_repo | 9 | field :bitbucket_repo |
10 | + field :asset_host | ||
10 | field :repository_branch | 11 | field :repository_branch |
11 | field :resolve_errs_on_deploy, :type => Boolean, :default => false | 12 | field :resolve_errs_on_deploy, :type => Boolean, :default => false |
12 | field :notify_all_users, :type => Boolean, :default => false | 13 | field :notify_all_users, :type => Boolean, :default => false |
app/models/backtrace.rb
@@ -23,7 +23,7 @@ class Backtrace | @@ -23,7 +23,7 @@ class Backtrace | ||
23 | end | 23 | end |
24 | 24 | ||
25 | def raw=(raw) | 25 | def raw=(raw) |
26 | - raw.each do |raw_line| | 26 | + raw.compact.each do |raw_line| |
27 | lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call) | 27 | lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call) |
28 | end | 28 | end |
29 | end | 29 | end |
app/models/backtrace_line.rb
@@ -4,6 +4,7 @@ class BacktraceLine | @@ -4,6 +4,7 @@ class BacktraceLine | ||
4 | GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)} | 4 | GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)} |
5 | 5 | ||
6 | field :number, :type => Integer | 6 | field :number, :type => Integer |
7 | + field :column, :type => Integer | ||
7 | field :file | 8 | field :file |
8 | field :method | 9 | field :method |
9 | 10 | ||
@@ -14,7 +15,7 @@ class BacktraceLine | @@ -14,7 +15,7 @@ class BacktraceLine | ||
14 | delegate :app, :to => :backtrace | 15 | delegate :app, :to => :backtrace |
15 | 16 | ||
16 | def to_s | 17 | def to_s |
17 | - "#{file}:#{number}" | 18 | + "#{file_relative}:#{number}" << (column.present? ? ":#{column}" : "") |
18 | end | 19 | end |
19 | 20 | ||
20 | def in_app? | 21 | def in_app? |
app/models/backtrace_line_normalizer.rb
1 | class BacktraceLineNormalizer | 1 | class BacktraceLineNormalizer |
2 | def initialize(raw_line) | 2 | def initialize(raw_line) |
3 | - @raw_line = raw_line | 3 | + @raw_line = raw_line || {} |
4 | end | 4 | end |
5 | 5 | ||
6 | def call | 6 | def call |
@@ -12,7 +12,12 @@ class BacktraceLineNormalizer | @@ -12,7 +12,12 @@ class BacktraceLineNormalizer | ||
12 | if @raw_line['file'].blank? | 12 | if @raw_line['file'].blank? |
13 | "[unknown source]" | 13 | "[unknown source]" |
14 | else | 14 | else |
15 | - @raw_line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems') | 15 | + file = @raw_line['file'].to_s |
16 | + # Detect lines from gem | ||
17 | + file.gsub!(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems') | ||
18 | + # Strip any query strings | ||
19 | + file.gsub!(/\?[^\?]*$/, '') | ||
20 | + @raw_line['file'] = file | ||
16 | end | 21 | end |
17 | end | 22 | end |
18 | 23 |
app/models/comment.rb
@@ -10,6 +10,7 @@ class Comment | @@ -10,6 +10,7 @@ class Comment | ||
10 | 10 | ||
11 | belongs_to :err, :class_name => "Problem" | 11 | belongs_to :err, :class_name => "Problem" |
12 | belongs_to :user | 12 | belongs_to :user |
13 | + delegate :app, :to => :err | ||
13 | 14 | ||
14 | validates_presence_of :body | 15 | validates_presence_of :body |
15 | 16 |
app/models/error_report.rb
@@ -2,7 +2,7 @@ require 'digest/sha1' | @@ -2,7 +2,7 @@ require 'digest/sha1' | ||
2 | require 'hoptoad_notifier' | 2 | require 'hoptoad_notifier' |
3 | 3 | ||
4 | class ErrorReport | 4 | class ErrorReport |
5 | - attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :current_user, :framework | 5 | + attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :framework |
6 | 6 | ||
7 | def initialize(xml_or_attributes) | 7 | def initialize(xml_or_attributes) |
8 | @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access | 8 | @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access |
@@ -42,7 +42,6 @@ class ErrorReport | @@ -42,7 +42,6 @@ class ErrorReport | ||
42 | :server_environment => server_environment, | 42 | :server_environment => server_environment, |
43 | :notifier => notifier, | 43 | :notifier => notifier, |
44 | :user_attributes => user_attributes, | 44 | :user_attributes => user_attributes, |
45 | - :current_user => current_user, | ||
46 | :framework => framework | 45 | :framework => framework |
47 | ) | 46 | ) |
48 | 47 | ||
@@ -59,8 +58,17 @@ class ErrorReport | @@ -59,8 +58,17 @@ class ErrorReport | ||
59 | 58 | ||
60 | private | 59 | private |
61 | def fingerprint_source | 60 | def fingerprint_source |
61 | + # Find the first backtrace line with a file and line number. | ||
62 | + if line = backtrace.lines.detect {|l| l.number.present? && l.file.present? } | ||
63 | + # If line exists, only use file and number. | ||
64 | + file_or_message = "#{line.file}:#{line.number}" | ||
65 | + else | ||
66 | + # If no backtrace, use error message | ||
67 | + file_or_message = message | ||
68 | + end | ||
69 | + | ||
62 | { | 70 | { |
63 | - :backtrace => backtrace.id, | 71 | + :file_or_message => file_or_message, |
64 | :error_class => error_class, | 72 | :error_class => error_class, |
65 | :component => component, | 73 | :component => component, |
66 | :action => action, | 74 | :action => action, |
app/models/issue_trackers/redmine_tracker.rb
@@ -9,6 +9,12 @@ if defined? RedmineClient | @@ -9,6 +9,12 @@ if defined? RedmineClient | ||
9 | [:api_token, { | 9 | [:api_token, { |
10 | :placeholder => "API Token for your account" | 10 | :placeholder => "API Token for your account" |
11 | }], | 11 | }], |
12 | + [:username, { | ||
13 | + :placeholder => "Your username" | ||
14 | + }], | ||
15 | + [:password, { | ||
16 | + :placeholder => "Your password" | ||
17 | + }], | ||
12 | [:project_id, { | 18 | [:project_id, { |
13 | :label => "Ticket Project", | 19 | :label => "Ticket Project", |
14 | :placeholder => "Redmine Project where tickets will be created" | 20 | :placeholder => "Redmine Project where tickets will be created" |
@@ -22,15 +28,19 @@ if defined? RedmineClient | @@ -22,15 +28,19 @@ if defined? RedmineClient | ||
22 | 28 | ||
23 | def check_params | 29 | def check_params |
24 | if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]} | 30 | if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]} |
25 | - errors.add :base, 'You must specify your Redmine URL, API token and Project ID' | 31 | + errors.add :base, 'You must specify your Redmine URL, API token, Username, Password and Project ID' |
26 | end | 32 | end |
27 | end | 33 | end |
28 | 34 | ||
29 | def create_issue(problem, reported_by = nil) | 35 | def create_issue(problem, reported_by = nil) |
30 | token = api_token | 36 | token = api_token |
31 | acc = account | 37 | acc = account |
38 | + user = username | ||
39 | + passwd = password | ||
32 | RedmineClient::Base.configure do | 40 | RedmineClient::Base.configure do |
33 | self.token = token | 41 | self.token = token |
42 | + self.user = user | ||
43 | + self.password = passwd | ||
34 | self.site = acc | 44 | self.site = acc |
35 | self.format = :xml | 45 | self.format = :xml |
36 | end | 46 | end |
@@ -47,7 +57,7 @@ if defined? RedmineClient | @@ -47,7 +57,7 @@ if defined? RedmineClient | ||
47 | def url_to_file(file_path, line_number = nil) | 57 | def url_to_file(file_path, line_number = nil) |
48 | # alt_project_id let's users specify a different project for tickets / app files. | 58 | # alt_project_id let's users specify a different project for tickets / app files. |
49 | project = self.alt_project_id.present? ? self.alt_project_id : self.project_id | 59 | project = self.alt_project_id.present? ? self.alt_project_id : self.project_id |
50 | - url = "#{self.account}/projects/#{project}/repository/annotate/#{file_path.sub(/^\//,'')}" | 60 | + url = "#{self.account.gsub(/\/$/, '')}/projects/#{project}/repository/revisions/#{app.repository_branch}/changes/#{file_path.sub(/\[PROJECT_ROOT\]/, '').sub(/^\//,'')}" |
51 | line_number ? url << "#L#{line_number}" : url | 61 | line_number ? url << "#L#{line_number}" : url |
52 | end | 62 | end |
53 | 63 |
app/models/notice.rb
@@ -10,7 +10,6 @@ class Notice | @@ -10,7 +10,6 @@ class Notice | ||
10 | field :request, :type => Hash | 10 | field :request, :type => Hash |
11 | field :notifier, :type => Hash | 11 | field :notifier, :type => Hash |
12 | field :user_attributes, :type => Hash | 12 | field :user_attributes, :type => Hash |
13 | - field :current_user, :type => Hash | ||
14 | field :framework | 13 | field :framework |
15 | field :error_class | 14 | field :error_class |
16 | delegate :lines, :to => :backtrace, :prefix => true | 15 | delegate :lines, :to => :backtrace, :prefix => true |
@@ -43,7 +42,11 @@ class Notice | @@ -43,7 +42,11 @@ class Notice | ||
43 | end | 42 | end |
44 | 43 | ||
45 | def user_agent_string | 44 | def user_agent_string |
46 | - (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" | 45 | + if user_agent.nil? || user_agent.none? |
46 | + "N/A" | ||
47 | + else | ||
48 | + "#{user_agent.browser} #{user_agent.version} (#{user_agent.os})" | ||
49 | + end | ||
47 | end | 50 | end |
48 | 51 | ||
49 | def environment_name | 52 | def environment_name |
app/models/notification_service.rb
@@ -12,7 +12,6 @@ class NotificationService | @@ -12,7 +12,6 @@ class NotificationService | ||
12 | field :subdomain, :type => String | 12 | field :subdomain, :type => String |
13 | field :sender_name, :type => String | 13 | field :sender_name, :type => String |
14 | field :notify_at_notices, :type => Array, :default => Errbit::Config.notify_at_notices | 14 | field :notify_at_notices, :type => Array, :default => Errbit::Config.notify_at_notices |
15 | - | ||
16 | embedded_in :app, :inverse_of => :notification_service | 15 | embedded_in :app, :inverse_of => :notification_service |
17 | 16 | ||
18 | validate :check_params | 17 | validate :check_params |
@@ -52,4 +51,8 @@ class NotificationService | @@ -52,4 +51,8 @@ class NotificationService | ||
52 | def configured? | 51 | def configured? |
53 | api_token.present? | 52 | api_token.present? |
54 | end | 53 | end |
54 | + | ||
55 | + def problem_url(problem) | ||
56 | + "http://#{Errbit::Config.host}/apps/#{problem.app.id}/problems/#{problem.id}" | ||
57 | + end | ||
55 | end | 58 | end |
app/models/notification_services/hubot_service.rb
@@ -22,7 +22,7 @@ class NotificationServices::HubotService < NotificationService | @@ -22,7 +22,7 @@ class NotificationServices::HubotService < NotificationService | ||
22 | end | 22 | end |
23 | 23 | ||
24 | def message_for_hubot(problem) | 24 | def message_for_hubot(problem) |
25 | - notification_description(problem) | 25 | + "[#{problem.app.name}][#{problem.environment}][#{problem.where}]: #{problem.error_class} #{problem_url(problem)}" |
26 | end | 26 | end |
27 | 27 | ||
28 | def create_notification(problem) | 28 | def create_notification(problem) |
@@ -0,0 +1,19 @@ | @@ -0,0 +1,19 @@ | ||
1 | +class NotificationServices::WebhookService < NotificationService | ||
2 | + Label = "webhook" | ||
3 | + Fields = [ | ||
4 | + [:api_token, { | ||
5 | + :placeholder => 'URL to receive a POST request when an error occurs', | ||
6 | + :label => 'URL' | ||
7 | + }] | ||
8 | + ] | ||
9 | + | ||
10 | + def check_params | ||
11 | + if Fields.detect {|f| self[f[0]].blank? } | ||
12 | + errors.add :base, 'You must specify the URL' | ||
13 | + end | ||
14 | + end | ||
15 | + | ||
16 | + def create_notification(problem) | ||
17 | + HTTParty.post(api_token, :body => {:problem => problem.to_json}) | ||
18 | + end | ||
19 | +end |
app/models/user.rb
@@ -52,6 +52,13 @@ class User | @@ -52,6 +52,13 @@ class User | ||
52 | github_account? && Errbit::Config.github_access_scope.include?('repo') | 52 | github_account? && Errbit::Config.github_access_scope.include?('repo') |
53 | end | 53 | end |
54 | 54 | ||
55 | + def github_login=(login) | ||
56 | + if login.is_a?(String) && login.strip.empty? | ||
57 | + login = nil | ||
58 | + end | ||
59 | + self[:github_login] = login | ||
60 | + end | ||
61 | + | ||
55 | protected | 62 | protected |
56 | 63 | ||
57 | def destroy_watchers | 64 | def destroy_watchers |
app/views/apps/_fields.html.haml
@@ -13,6 +13,10 @@ | @@ -13,6 +13,10 @@ | ||
13 | %div | 13 | %div |
14 | = f.label :bitbucket_repo | 14 | = f.label :bitbucket_repo |
15 | = f.text_field :bitbucket_repo, :placeholder => "errbit/errbit from https://bitbucket.org/errbit/errbit" | 15 | = f.text_field :bitbucket_repo, :placeholder => "errbit/errbit from https://bitbucket.org/errbit/errbit" |
16 | +%div | ||
17 | + = f.label :asset_host | ||
18 | + %em Used to generate links for JavaScript errors | ||
19 | + = f.text_field :asset_host, :placeholder => "e.g. https://assets.example.com" | ||
16 | 20 | ||
17 | %fieldset | 21 | %fieldset |
18 | %legend Notifications | 22 | %legend Notifications |
app/views/kaminari/notices/_paginator.html.haml
@@ -6,9 +6,9 @@ | @@ -6,9 +6,9 @@ | ||
6 | -# remote: data-remote | 6 | -# remote: data-remote |
7 | -# paginator: the paginator that renders the pagination tags inside | 7 | -# paginator: the paginator that renders the pagination tags inside |
8 | = paginator.render do | 8 | = paginator.render do |
9 | - .notice-pagination< | 9 | + .notice-pagination |
10 | = next_page_tag | 10 | = next_page_tag |
11 | - | | 11 | + | |
12 | = prev_page_tag | 12 | = prev_page_tag |
13 | .notice-pagination-loader= image_tag 'loader.gif' | 13 | .notice-pagination-loader= image_tag 'loader.gif' |
14 | viewing occurrence #{page_count_from_end(current_page, total_pages)} of #{total_pages} | 14 | viewing occurrence #{page_count_from_end(current_page, total_pages)} of #{total_pages} |
@@ -0,0 +1,50 @@ | @@ -0,0 +1,50 @@ | ||
1 | +%tr | ||
2 | + %td.section | ||
3 | + %table(cellpadding="0" cellspacing="0" border="0" align="left") | ||
4 | + %tbody | ||
5 | + %tr | ||
6 | + %td.content(valign="top") | ||
7 | + %div | ||
8 | + %p | ||
9 | + = @user.name | ||
10 | + has just commented on an error that occurred in | ||
11 | + = link_to(@app.name, app_url(@app), :class => "bold") << "," | ||
12 | + on the | ||
13 | + %span.bold= @problem.environment | ||
14 | + environment. | ||
15 | + %br | ||
16 | + This err has occurred #{pluralize @problem.notices_count, 'time'}. | ||
17 | + %p | ||
18 | + = link_to("Click here to view the error and add a comment on Errbit", app_problem_url(@app, @problem), :class => "bold") << "." | ||
19 | + | ||
20 | +%tr | ||
21 | + %td.section | ||
22 | + %table(cellpadding="0" cellspacing="0" border="0" align="left") | ||
23 | + %tbody | ||
24 | + %tr | ||
25 | + %td.content(valign="top") | ||
26 | + %div | ||
27 | + %p.heading COMMENT: | ||
28 | + %br | ||
29 | + %p= @comment.body.to_s.gsub("\n", "<br/>").html_safe | ||
30 | + | ||
31 | +%tr | ||
32 | + %td.section | ||
33 | + %table(cellpadding="0" cellspacing="0" border="0" align="left") | ||
34 | + %tbody | ||
35 | + %tr | ||
36 | + %td.content(valign="top") | ||
37 | + %div | ||
38 | + %p.heading ERROR MESSAGE: | ||
39 | + %p= @problem.message | ||
40 | + %p.heading WHERE: | ||
41 | + %p.monospace | ||
42 | + = @problem.where | ||
43 | + %p.heading URL: | ||
44 | + %p.monospace | ||
45 | + - if @notice.request['url'].present? | ||
46 | + = link_to @notice.request['url'], @notice.request['url'] | ||
47 | + %p.heading BROWSER: | ||
48 | + %p.monospace | ||
49 | + = user_agent_graph(@problem) | ||
50 | + %br |
@@ -0,0 +1,36 @@ | @@ -0,0 +1,36 @@ | ||
1 | +<%= @user.name %> has just commented on an error that occurred in <%= @notice.environment_name %>: <%= raw(@notice.message) %> | ||
2 | + | ||
3 | +This err has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here: | ||
4 | + | ||
5 | + <%= app_problem_url(@app, @notice.problem) %> | ||
6 | + | ||
7 | + | ||
8 | +COMMENT: | ||
9 | + | ||
10 | +<%= @comment.body %> | ||
11 | + | ||
12 | + | ||
13 | +----------------------------------------------- | ||
14 | + | ||
15 | +ERROR MESSAGE: | ||
16 | + | ||
17 | +<%= raw(@notice.message) %> | ||
18 | + | ||
19 | + | ||
20 | +WHERE: | ||
21 | + | ||
22 | +<%= @notice.where %> | ||
23 | + | ||
24 | +<% @notice.in_app_backtrace_lines.each do |line| %> | ||
25 | + <%= line %> | ||
26 | +<% end %> | ||
27 | + | ||
28 | + | ||
29 | +URL: | ||
30 | + | ||
31 | +<%= @notice.request['url'] %> | ||
32 | + | ||
33 | + | ||
34 | +BROWSER: | ||
35 | + | ||
36 | +<%= @notice.user_agent_string %> |
app/views/mailer/err_notification.html.haml
@@ -28,14 +28,31 @@ | @@ -28,14 +28,31 @@ | ||
28 | %p.monospace | 28 | %p.monospace |
29 | = @notice.where | 29 | = @notice.where |
30 | - @notice.in_app_backtrace_lines.each do |line| | 30 | - @notice.in_app_backtrace_lines.each do |line| |
31 | - %p.backtrace= line | ||
32 | - %br | 31 | + %p.backtrace |
32 | + = link_to_source_file(line) do | ||
33 | + = line.to_s | ||
34 | + %br | ||
33 | %p.heading URL: | 35 | %p.heading URL: |
34 | %p.monospace | 36 | %p.monospace |
35 | - if @notice.request['url'].present? | 37 | - if @notice.request['url'].present? |
36 | = link_to @notice.request['url'], @notice.request['url'] | 38 | = link_to @notice.request['url'], @notice.request['url'] |
37 | - %p.heading BACKTRACE: | ||
38 | - - @notice.backtrace_lines.each do |line| | ||
39 | - %p.backtrace= line | 39 | + %p.heading BROWSER: |
40 | + %p.monospace | ||
41 | + = user_agent_graph(@notice.problem) | ||
40 | %br | 42 | %br |
41 | - | 43 | + - if @notice.user_attributes.present? |
44 | + %p.heading USER: | ||
45 | + %table | ||
46 | + - @notice.user_attributes.each do |key, value| | ||
47 | + %tr | ||
48 | + %td(style="text-align: right; padding-right: 10px; color: #6a6a6a;")= key.to_s.titleize + ":" | ||
49 | + %td= auto_link(value.to_s) | ||
50 | + %br | ||
51 | + - if @notice.backtrace_lines.any? | ||
52 | + %br | ||
53 | + %p.heading FULL BACKTRACE: | ||
54 | + - @notice.backtrace_lines.each do |line| | ||
55 | + %p.backtrace | ||
56 | + = link_to_source_file(line) do | ||
57 | + = line.to_s | ||
58 | + %br |
app/views/mailer/err_notification.text.erb
@@ -24,6 +24,20 @@ URL: | @@ -24,6 +24,20 @@ URL: | ||
24 | <%= @notice.request['url'] %> | 24 | <%= @notice.request['url'] %> |
25 | 25 | ||
26 | 26 | ||
27 | +BROWSER: | ||
28 | + | ||
29 | +<%= @notice.user_agent_string %> | ||
30 | + | ||
31 | + | ||
32 | +<%- if @notice.user_attributes.present? %> | ||
33 | +USER: | ||
34 | + | ||
35 | +<%- @notice.user_attributes.each do |key, value| %> | ||
36 | +<%= key.to_s.titleize %>: <%= value.to_s %> | ||
37 | +<%- end %> | ||
38 | + | ||
39 | +<%- end %> | ||
40 | + | ||
27 | BACKTRACE: | 41 | BACKTRACE: |
28 | 42 | ||
29 | <% @notice.backtrace_lines.each do |line| %> | 43 | <% @notice.backtrace_lines.each do |line| %> |
app/views/notices/_backtrace_line.html.haml
1 | %tr{:class => defined?(row_class) && row_class} | 1 | %tr{:class => defined?(row_class) && row_class} |
2 | %td.line{:class => line.in_app? && 'in-app' } | 2 | %td.line{:class => line.in_app? && 'in-app' } |
3 | = link_to_source_file(line) do | 3 | = link_to_source_file(line) do |
4 | - %span.path>=raw line.decorated_path | 4 | + %span.path>= raw line.decorated_path |
5 | %span.file>= line.file_name | 5 | %span.file>= line.file_name |
6 | - if line.number.present? | 6 | - if line.number.present? |
7 | %span.number>= ":#{line.number}" | 7 | %span.number>= ":#{line.number}" |
8 | + - if line.column.present? | ||
9 | + %span.number>= ":#{line.column}" | ||
8 | → | 10 | → |
9 | %span.method= line.method | 11 | %span.method= line.method |
app/views/notices/_user_attributes.html.haml
@@ -2,8 +2,8 @@ | @@ -2,8 +2,8 @@ | ||
2 | %table.user_attributes | 2 | %table.user_attributes |
3 | %tr | 3 | %tr |
4 | %td | 4 | %td |
5 | - %strong Information about the user who experienced the error. | ||
6 | - - user.each do |user_key, user_value| | 5 | + %strong The user who experienced the error: |
6 | + - user.each do |key, value| | ||
7 | %tr | 7 | %tr |
8 | - %th= user_key | ||
9 | - %td= auto_link(user_value.to_s).html_safe | 8 | + %th= key |
9 | + %td= auto_link(auto_link(value.to_s, :urls, :target => "_blank"), :email_addresses).html_safe |
app/views/problems/show.html.haml
@@ -55,7 +55,7 @@ | @@ -55,7 +55,7 @@ | ||
55 | %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button' | 55 | %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button' |
56 | %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button' | 56 | %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button' |
57 | - if @notice && @notice.user_attributes.present? | 57 | - if @notice && @notice.user_attributes.present? |
58 | - %li= link_to 'User Details', '#user_attributes', :rel => 'user_attributes', :class => 'button' | 58 | + %li= link_to 'User', '#user_attributes', :rel => 'user_attributes', :class => 'button' |
59 | %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button' | 59 | %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button' |
60 | %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' | 60 | %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button' |
61 | %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' | 61 | %li= link_to 'Session', '#session', :rel => 'session', :class => 'button' |
@@ -71,7 +71,7 @@ | @@ -71,7 +71,7 @@ | ||
71 | 71 | ||
72 | - if @notice.user_attributes.present? | 72 | - if @notice.user_attributes.present? |
73 | #user_attributes | 73 | #user_attributes |
74 | - %h3 User Details | 74 | + %h3 User |
75 | = render 'notices/user_attributes', :user => @notice.user_attributes | 75 | = render 'notices/user_attributes', :user => @notice.user_attributes |
76 | 76 | ||
77 | #environment | 77 | #environment |
config/application.rb
@@ -48,11 +48,14 @@ module Errbit | @@ -48,11 +48,14 @@ module Errbit | ||
48 | g.fixture_replacement :fabrication | 48 | g.fixture_replacement :fabrication |
49 | end | 49 | end |
50 | 50 | ||
51 | + # Enable the mongoid identity map for performance | ||
52 | + Mongoid.identity_map_enabled = true | ||
53 | + | ||
51 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. | 54 | # IssueTracker subclasses use inheritance, so preloading models provides querying consistency in dev mode. |
52 | config.mongoid.preload_models = true | 55 | config.mongoid.preload_models = true |
53 | 56 | ||
54 | # Set up observers | 57 | # Set up observers |
55 | - config.mongoid.observers = :deploy_observer, :notice_observer | 58 | + config.mongoid.observers = :deploy_observer, :notice_observer, :comment_observer |
56 | 59 | ||
57 | # Configure the default encoding used in templates for Ruby 1.9. | 60 | # Configure the default encoding used in templates for Ruby 1.9. |
58 | config.encoding = "utf-8" | 61 | config.encoding = "utf-8" |
config/config.example.yml
@@ -48,6 +48,12 @@ user_has_username: false | @@ -48,6 +48,12 @@ user_has_username: false | ||
48 | # but you want to leave a short comment. | 48 | # but you want to leave a short comment. |
49 | allow_comments_with_issue_tracker: true | 49 | allow_comments_with_issue_tracker: true |
50 | 50 | ||
51 | +# Display internal errors in production | ||
52 | +# Since this is an internal application, you might like to see what caused Errbit to crash. | ||
53 | +# Pull requests are always welcome! | ||
54 | +# However, you might be more comfortable setting this to false if your server can be accessed by anyone. | ||
55 | +display_internal_errors: true | ||
56 | + | ||
51 | # Enable Gravatar. | 57 | # Enable Gravatar. |
52 | use_gravatar: true | 58 | use_gravatar: true |
53 | # Default Gravatar image, can be: mm, identicon, monsterid, wavatar, retro. | 59 | # Default Gravatar image, can be: mm, identicon, monsterid, wavatar, retro. |
config/environment.rb
1 | -# Load the rails application | ||
2 | -require File.expand_path('../application', __FILE__) | ||
3 | if RUBY_VERSION.to_f >= 1.9 | 1 | if RUBY_VERSION.to_f >= 1.9 |
4 | require 'yaml' | 2 | require 'yaml' |
5 | YAML::ENGINE.yamler = 'syck' | 3 | YAML::ENGINE.yamler = 'syck' |
6 | end | 4 | end |
5 | + | ||
6 | +# Load the rails application | ||
7 | +require File.expand_path('../application', __FILE__) | ||
8 | + | ||
7 | # Initialize the rails application | 9 | # Initialize the rails application |
8 | Errbit::Application.initialize! | 10 | Errbit::Application.initialize! |
9 | - |
config/environments/development.rb
@@ -14,7 +14,7 @@ Errbit::Application.configure do | @@ -14,7 +14,7 @@ Errbit::Application.configure do | ||
14 | config.action_controller.perform_caching = false | 14 | config.action_controller.perform_caching = false |
15 | 15 | ||
16 | # Don't care if the mailer can't send | 16 | # Don't care if the mailer can't send |
17 | - config.action_mailer.raise_delivery_errors = true | 17 | + config.action_mailer.raise_delivery_errors = false |
18 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } | 18 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } |
19 | 19 | ||
20 | # Print deprecation notices to the Rails logger | 20 | # Print deprecation notices to the Rails logger |
config/environments/production.rb
@@ -5,8 +5,8 @@ Errbit::Application.configure do | @@ -5,8 +5,8 @@ Errbit::Application.configure do | ||
5 | # Code is not reloaded between requests | 5 | # Code is not reloaded between requests |
6 | config.cache_classes = true | 6 | config.cache_classes = true |
7 | 7 | ||
8 | - # Full error reports are enabled, since this is an internal application. | ||
9 | - config.consider_all_requests_local = true | 8 | + # Shows or hides all error details if something goes wrong inside Errbit |
9 | + config.consider_all_requests_local = false | ||
10 | # Caching is turned on | 10 | # Caching is turned on |
11 | config.action_controller.perform_caching = true | 11 | config.action_controller.perform_caching = true |
12 | 12 |
config/initializers/_load_config.rb
@@ -74,3 +74,6 @@ end | @@ -74,3 +74,6 @@ end | ||
74 | default.merge! :host => Errbit::Config.host if default[:host].blank? | 74 | default.merge! :host => Errbit::Config.host if default[:host].blank? |
75 | end | 75 | end |
76 | 76 | ||
77 | +if Rails.env.production? | ||
78 | + Rails.application.config.consider_all_requests_local = Errbit::Config.display_internal_errors | ||
79 | +end | ||
77 | \ No newline at end of file | 80 | \ No newline at end of file |
config/initializers/devise.rb
@@ -69,7 +69,7 @@ Devise.setup do |config| | @@ -69,7 +69,7 @@ Devise.setup do |config| | ||
69 | 69 | ||
70 | # ==> Configuration for :validatable | 70 | # ==> Configuration for :validatable |
71 | # Range for password length | 71 | # Range for password length |
72 | - config.password_length = 6..20 | 72 | + config.password_length = 6..1024 |
73 | 73 | ||
74 | # Regex to use to validate the email address | 74 | # Regex to use to validate the email address |
75 | config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i | 75 | config.email_regexp = /^([\w\.%\+\-]+)@([\w\-]+\.)+([\w]{2,})$/i |
config/initializers/ssl_enforcer.rb
1 | -# | ||
2 | # Enforce SSL connections, if configured | 1 | # Enforce SSL connections, if configured |
3 | if Errbit::Config.enforce_ssl | 2 | if Errbit::Config.enforce_ssl |
3 | + ActionMailer::Base.default_url_options.merge!(:protocol => 'https://') | ||
4 | Errbit::Application.configure do | 4 | Errbit::Application.configure do |
5 | config.middleware.use Rack::SslEnforcer, :except => /^\/deploys/ | 5 | config.middleware.use Rack::SslEnforcer, :except => /^\/deploys/ |
6 | end | 6 | end |
config/mongoid.example.yml
config/mongoid.mongohq.yml
config/mongoid.mongolab.yml
config/routes.rb
@@ -45,7 +45,12 @@ Errbit::Application.routes.draw do | @@ -45,7 +45,12 @@ Errbit::Application.routes.draw do | ||
45 | namespace :api do | 45 | namespace :api do |
46 | namespace :v1 do | 46 | namespace :v1 do |
47 | resources :problems, :only => [:index], :defaults => { :format => 'json' } | 47 | resources :problems, :only => [:index], :defaults => { :format => 'json' } |
48 | - resources :notices, :only => [:index], :defaults => { :format => 'json' } | 48 | + resources :notices, :only => [:index], :defaults => { :format => 'json' } |
49 | + resources :stats, :only => [], :defaults => { :format => 'json' } do | ||
50 | + collection do | ||
51 | + get :app | ||
52 | + end | ||
53 | + end | ||
49 | end | 54 | end |
50 | end | 55 | end |
51 | 56 |
config/unicorn.rb
@@ -3,3 +3,27 @@ | @@ -3,3 +3,27 @@ | ||
3 | worker_processes 3 # amount of unicorn workers to spin up | 3 | worker_processes 3 # amount of unicorn workers to spin up |
4 | timeout 30 # restarts workers that hang for 30 seconds | 4 | timeout 30 # restarts workers that hang for 30 seconds |
5 | preload_app true | 5 | preload_app true |
6 | + | ||
7 | +# Taken from github: https://github.com/blog/517-unicorn | ||
8 | +# Though everyone uses pretty miuch the same code | ||
9 | +before_fork do |server, worker| | ||
10 | + ## | ||
11 | + # When sent a USR2, Unicorn will suffix its pidfile with .oldbin and | ||
12 | + # immediately start loading up a new version of itself (loaded with a new | ||
13 | + # version of our app). When this new Unicorn is completely loaded | ||
14 | + # it will begin spawning workers. The first worker spawned will check to | ||
15 | + # see if an .oldbin pidfile exists. If so, this means we've just booted up | ||
16 | + # a new Unicorn and need to tell the old one that it can now die. To do so | ||
17 | + # we send it a QUIT. | ||
18 | + # | ||
19 | + # Using this method we get 0 downtime deploys. | ||
20 | + | ||
21 | + old_pid = "#{server.config[:pid]}.oldbin" | ||
22 | + if File.exists?(old_pid) && server.pid != old_pid | ||
23 | + begin | ||
24 | + Process.kill("QUIT", File.read(old_pid).to_i) | ||
25 | + rescue Errno::ENOENT, Errno::ESRCH | ||
26 | + # someone else did our job for us | ||
27 | + end | ||
28 | + end | ||
29 | +end |
lib/hoptoad/v2.rb
@@ -59,11 +59,10 @@ module Hoptoad | @@ -59,11 +59,10 @@ module Hoptoad | ||
59 | 59 | ||
60 | :api_key => notice['api-key'], | 60 | :api_key => notice['api-key'], |
61 | :notifier => notice['notifier'], | 61 | :notifier => notice['notifier'], |
62 | - :user_attributes => notice['user-attributes'] || {}, | ||
63 | - :current_user => notice['current-user'] || {}, | 62 | + # 'current-user' from airbrake, 'user-attributes' from airbrake_user_attributes gem |
63 | + :user_attributes => notice['current-user'] || notice['user-attributes'] || {}, | ||
64 | :framework => notice['framework'] | 64 | :framework => notice['framework'] |
65 | } | 65 | } |
66 | end | 66 | end |
67 | end | 67 | end |
68 | end | 68 | end |
69 | - |
public/javascripts/notifier.js
1 | -var Hoptoad = { | ||
2 | - VERSION : '2.0', | ||
3 | - NOTICE_XML : '<?xml version="1.0" encoding="UTF-8"?>\ | ||
4 | - <notice version="2.0">\ | ||
5 | - <api-key></api-key>\ | ||
6 | - <notifier>\ | ||
7 | - <name>errbit_notifier_js</name>\ | ||
8 | - <version>2.0</version>\ | ||
9 | - <url>https://github.com/errbit/errbit</url>\ | ||
10 | - </notifier>\ | ||
11 | - <error>\ | ||
12 | - <class>EXCEPTION_CLASS</class>\ | ||
13 | - <message>EXCEPTION_MESSAGE</message>\ | ||
14 | - <backtrace>BACKTRACE_LINES</backtrace>\ | ||
15 | - </error>\ | ||
16 | - <request>\ | ||
17 | - <url>REQUEST_URL</url>\ | ||
18 | - <component>REQUEST_COMPONENT</component>\ | ||
19 | - <action>REQUEST_ACTION</action>\ | ||
20 | - </request>\ | ||
21 | - <server-environment>\ | ||
22 | - <project-root>PROJECT_ROOT</project-root>\ | ||
23 | - <environment-name>production</environment-name>\ | ||
24 | - </server-environment>\ | ||
25 | - </notice>', | ||
26 | - ROOT : window.location.protocol + '//' + window.location.host, | ||
27 | - BACKTRACE_MATCHER : /^(.*)\@(.*)\:(\d+)$/, | ||
28 | - backtrace_filters : [/notifier\.js/], | ||
29 | - | ||
30 | - notify: function(error) { | ||
31 | - var xml = escape(Hoptoad.generateXML(error)); | ||
32 | - var host = Hoptoad.host; | ||
33 | - var url = '//' + host + '/notifier_api/v2/notices.xml?data=' + xml; | ||
34 | - var request = document.createElement('iframe'); | ||
35 | - | ||
36 | - request.style.width = '1px'; | ||
37 | - request.style.height = '1px'; | ||
38 | - request.style.display = 'none'; | ||
39 | - request.src = url; | ||
40 | - | ||
41 | - document.getElementsByTagName('head')[0].appendChild(request); | ||
42 | - }, | ||
43 | - | ||
44 | - setEnvironment: function(value) { | ||
45 | - var matcher = /<environment-name>.*<\/environment-name>/; | ||
46 | - | ||
47 | - Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher, | ||
48 | - '<environment-name>' + | ||
49 | - value + | ||
50 | - '</environment-name>') | ||
51 | - }, | ||
52 | - | ||
53 | - setHost: function(value) { | ||
54 | - Hoptoad.host = value; | ||
55 | - }, | ||
56 | - | ||
57 | - setKey: function(value) { | ||
58 | - var matcher = /<api-key>.*<\/api-key>/; | ||
59 | - | ||
60 | - Hoptoad.NOTICE_XML = Hoptoad.NOTICE_XML.replace(matcher, | ||
61 | - '<api-key>' + | ||
62 | - value + | ||
63 | - '</api-key>'); | ||
64 | - }, | ||
65 | - | ||
66 | - setErrorDefaults: function(value) { | ||
67 | - Hoptoad.errorDefaults = value; | ||
68 | - }, | ||
69 | - | ||
70 | - generateXML: function(errorWithoutDefaults) { | ||
71 | - var error = Hoptoad.mergeDefault(Hoptoad.errorDefaults, errorWithoutDefaults); | ||
72 | - | ||
73 | - var xml = Hoptoad.NOTICE_XML; | ||
74 | - var url = Hoptoad.escapeText(error.url || ''); | ||
75 | - var component = Hoptoad.escapeText(error.component || ''); | ||
76 | - var action = Hoptoad.escapeText(error.action || ''); | ||
77 | - var type = Hoptoad.escapeText(error.type || 'Error'); | ||
78 | - var message = Hoptoad.escapeText(error.message || 'Unknown error.'); | ||
79 | - var backtrace = Hoptoad.generateBacktrace(error); | ||
80 | - | ||
81 | - | ||
82 | - if (Hoptoad.trim(url) == '' && Hoptoad.trim(component) == '') { | ||
83 | - xml = xml.replace(/<request>.*<\/request>/, ''); | ||
84 | - } else { | ||
85 | - var data = ''; | ||
86 | - | ||
87 | - var cgi_data = error['cgi-data'] || {}; | ||
88 | - cgi_data["HTTP_USER_AGENT"] = navigator.userAgent; | ||
89 | - data += '<cgi-data>'; | ||
90 | - data += Hoptoad.generateVariables(cgi_data); | ||
91 | - data += '</cgi-data>'; | ||
92 | - | ||
93 | - var methods = ['params', 'session']; | ||
94 | - | ||
95 | - for (var i = 0; i < methods.length; i++) { | ||
96 | - var method = methods[i]; | ||
97 | - | ||
98 | - if (error[method]) { | ||
99 | - data += '<' + method + '>'; | ||
100 | - data += Hoptoad.generateVariables(error[method]); | ||
101 | - data += '</' + method + '>'; | ||
102 | - } | ||
103 | - } | ||
104 | - | ||
105 | - xml = xml.replace('</request>', data + '</request>') | ||
106 | - .replace('REQUEST_URL', url) | ||
107 | - .replace('REQUEST_ACTION', action) | ||
108 | - .replace('REQUEST_COMPONENT', component); | ||
109 | - } | 1 | +// Airbrake JavaScript Notifier Bundle |
2 | +(function(window, document, undefined) { | ||
3 | +// Domain Public by Eric Wendelin http://eriwen.com/ (2008) | ||
4 | +// Luke Smith http://lucassmith.name/ (2008) | ||
5 | +// Loic Dachary <loic@dachary.org> (2008) | ||
6 | +// Johan Euphrosine <proppy@aminche.com> (2008) | ||
7 | +// Øyvind Sean Kinsey http://kinsey.no/blog (2010) | ||
8 | +// Victor Homyakov (2010) | ||
9 | +// | ||
10 | +// Information and discussions | ||
11 | +// http://jspoker.pokersource.info/skin/test-printstacktrace.html | ||
12 | +// http://eriwen.com/javascript/js-stack-trace/ | ||
13 | +// http://eriwen.com/javascript/stacktrace-update/ | ||
14 | +// http://pastie.org/253058 | ||
15 | +// | ||
16 | +// guessFunctionNameFromLines comes from firebug | ||
17 | +// | ||
18 | +// Software License Agreement (BSD License) | ||
19 | +// | ||
20 | +// Copyright (c) 2007, Parakey Inc. | ||
21 | +// All rights reserved. | ||
22 | +// | ||
23 | +// Redistribution and use of this software in source and binary forms, with or without modification, | ||
24 | +// are permitted provided that the following conditions are met: | ||
25 | +// | ||
26 | +// * Redistributions of source code must retain the above | ||
27 | +// copyright notice, this list of conditions and the | ||
28 | +// following disclaimer. | ||
29 | +// | ||
30 | +// * Redistributions in binary form must reproduce the above | ||
31 | +// copyright notice, this list of conditions and the | ||
32 | +// following disclaimer in the documentation and/or other | ||
33 | +// materials provided with the distribution. | ||
34 | +// | ||
35 | +// * Neither the name of Parakey Inc. nor the names of its | ||
36 | +// contributors may be used to endorse or promote products | ||
37 | +// derived from this software without specific prior | ||
38 | +// written permission of Parakey Inc. | ||
39 | +// | ||
40 | +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR | ||
41 | +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | ||
42 | +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | ||
43 | +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
44 | +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
45 | +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
46 | +// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT | ||
47 | +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
110 | 48 | ||
111 | - return xml.replace('PROJECT_ROOT', Hoptoad.ROOT) | ||
112 | - .replace('EXCEPTION_CLASS', type) | ||
113 | - .replace('EXCEPTION_MESSAGE', message) | ||
114 | - .replace('BACKTRACE_LINES', backtrace.join('')); | ||
115 | - }, | ||
116 | - | ||
117 | - generateBacktrace: function(error) { | ||
118 | - error = error || {}; | ||
119 | - | ||
120 | - if (typeof error.stack != 'string') { | ||
121 | - try { | ||
122 | - (0)(); | ||
123 | - } catch(e) { | ||
124 | - error.stack = e.stack; | ||
125 | - } | ||
126 | - } | 49 | +/** |
50 | + * Main function giving a function stack trace with a forced or passed in Error | ||
51 | + * | ||
52 | + * @cfg {Error} e The error to create a stacktrace from (optional) | ||
53 | + * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions | ||
54 | + * @return {Array} of Strings with functions, lines, files, and arguments where possible | ||
55 | + */ | ||
56 | +function printStackTrace(options) { | ||
57 | + var ex = (options && options.e) ? options.e : null; | ||
58 | + var guess = options ? !!options.guess : true; | ||
59 | + | ||
60 | + var p = new printStackTrace.implementation(); | ||
61 | + var result = p.run(ex); | ||
62 | + return (guess) ? p.guessFunctions(result) : result; | ||
63 | +} | ||
127 | 64 | ||
128 | - var backtrace = []; | ||
129 | - var stacktrace = Hoptoad.getStackTrace(error); | 65 | +printStackTrace.implementation = function() {}; |
130 | 66 | ||
131 | - for (var i = 0, l = stacktrace.length; i < l; i++) { | ||
132 | - var line = stacktrace[i]; | ||
133 | - var matches = line.match(Hoptoad.BACKTRACE_MATCHER); | 67 | +printStackTrace.implementation.prototype = { |
68 | + run: function(ex) { | ||
69 | + ex = ex || | ||
70 | + (function() { | ||
71 | + try { | ||
72 | + var _err = __undef__ << 1; | ||
73 | + } catch (e) { | ||
74 | + return e; | ||
75 | + } | ||
76 | + })(); | ||
77 | + // Use either the stored mode, or resolve it | ||
78 | + var mode = this._mode || this.mode(ex); | ||
79 | + if (mode === 'other') { | ||
80 | + return this.other(arguments.callee); | ||
81 | + } else { | ||
82 | + return this[mode](ex); | ||
83 | + } | ||
84 | + }, | ||
85 | + | ||
86 | + /** | ||
87 | + * @return {String} mode of operation for the environment in question. | ||
88 | + */ | ||
89 | + mode: function(e) { | ||
90 | + if (e['arguments']) { | ||
91 | + return (this._mode = 'chrome'); | ||
92 | + } else if (window.opera && e.stacktrace) { | ||
93 | + return (this._mode = 'opera10'); | ||
94 | + } else if (e.stack) { | ||
95 | + return (this._mode = 'firefox'); | ||
96 | + } else if (window.opera && !('stacktrace' in e)) { //Opera 9- | ||
97 | + return (this._mode = 'opera'); | ||
98 | + } | ||
99 | + return (this._mode = 'other'); | ||
100 | + }, | ||
134 | 101 | ||
135 | - if (matches && Hoptoad.validBacktraceLine(line)) { | ||
136 | - var file = matches[2].replace(Hoptoad.ROOT, '[PROJECT_ROOT]'); | 102 | + /** |
103 | + * Given a context, function name, and callback function, overwrite it so that it calls | ||
104 | + * printStackTrace() first with a callback and then runs the rest of the body. | ||
105 | + * | ||
106 | + * @param {Object} context of execution (e.g. window) | ||
107 | + * @param {String} functionName to instrument | ||
108 | + * @param {Function} function to call with a stack trace on invocation | ||
109 | + */ | ||
110 | + instrumentFunction: function(context, functionName, callback) { | ||
111 | + context = context || window; | ||
112 | + context['_old' + functionName] = context[functionName]; | ||
113 | + context[functionName] = function() { | ||
114 | + callback.call(this, printStackTrace()); | ||
115 | + return context['_old' + functionName].apply(this, arguments); | ||
116 | + }; | ||
117 | + context[functionName]._instrumented = true; | ||
118 | + }, | ||
119 | + | ||
120 | + /** | ||
121 | + * Given a context and function name of a function that has been | ||
122 | + * instrumented, revert the function to it's original (non-instrumented) | ||
123 | + * state. | ||
124 | + * | ||
125 | + * @param {Object} context of execution (e.g. window) | ||
126 | + * @param {String} functionName to de-instrument | ||
127 | + */ | ||
128 | + deinstrumentFunction: function(context, functionName) { | ||
129 | + if (context[functionName].constructor === Function && | ||
130 | + context[functionName]._instrumented && | ||
131 | + context['_old' + functionName].constructor === Function) { | ||
132 | + context[functionName] = context['_old' + functionName]; | ||
133 | + } | ||
134 | + }, | ||
135 | + | ||
136 | + /** | ||
137 | + * Given an Error object, return a formatted Array based on Chrome's stack string. | ||
138 | + * | ||
139 | + * @param e - Error object to inspect | ||
140 | + * @return Array<String> of function calls, files and line numbers | ||
141 | + */ | ||
142 | + chrome: function(e) { | ||
143 | + return e.stack.replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n'); | ||
144 | + }, | ||
137 | 145 | ||
138 | - if (i == 0) { | ||
139 | - if (matches[2].match(document.location.href)) { | ||
140 | - backtrace.push('<line method="" file="internal: " number=""/>'); | ||
141 | - } | 146 | + /** |
147 | + * Given an Error object, return a formatted Array based on Firefox's stack string. | ||
148 | + * | ||
149 | + * @param e - Error object to inspect | ||
150 | + * @return Array<String> of function calls, files and line numbers | ||
151 | + */ | ||
152 | + firefox: function(e) { | ||
153 | + return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n'); | ||
154 | + }, | ||
155 | + | ||
156 | + /** | ||
157 | + * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. | ||
158 | + * | ||
159 | + * @param e - Error object to inspect | ||
160 | + * @return Array<String> of function calls, files and line numbers | ||
161 | + */ | ||
162 | + opera10: function(e) { | ||
163 | + var stack = e.stacktrace; | ||
164 | + var lines = stack.split('\n'), ANON = '{anonymous}', | ||
165 | + lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len; | ||
166 | + for (i = 2, j = 0, len = lines.length; i < len - 2; i++) { | ||
167 | + if (lineRE.test(lines[i])) { | ||
168 | + var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2; | ||
169 | + var fnName = RegExp.$3; | ||
170 | + fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON); | ||
171 | + lines[j++] = fnName + '@' + location; | ||
172 | + } | ||
173 | + } | ||
174 | + | ||
175 | + lines.splice(j, lines.length - j); | ||
176 | + return lines; | ||
177 | + }, | ||
178 | + | ||
179 | + // Opera 7.x-9.x only! | ||
180 | + opera: function(e) { | ||
181 | + var lines = e.message.split('\n'), ANON = '{anonymous}', | ||
182 | + lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i, | ||
183 | + i, j, len; | ||
184 | + | ||
185 | + for (i = 4, j = 0, len = lines.length; i < len; i += 2) { | ||
186 | + //TODO: RegExp.exec() would probably be cleaner here | ||
187 | + if (lineRE.test(lines[i])) { | ||
188 | + lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, ''); | ||
189 | + } | ||
190 | + } | ||
191 | + | ||
192 | + lines.splice(j, lines.length - j); | ||
193 | + return lines; | ||
194 | + }, | ||
195 | + | ||
196 | + // Safari, IE, and others | ||
197 | + other: function(curr) { | ||
198 | + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, | ||
199 | + stack = [], fn, args, maxStackSize = 10; | ||
200 | + | ||
201 | + while (curr && stack.length < maxStackSize) { | ||
202 | + fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; | ||
203 | + args = Array.prototype.slice.call(curr['arguments']); | ||
204 | + stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; | ||
205 | + curr = curr.caller; | ||
142 | } | 206 | } |
207 | + return stack; | ||
208 | + }, | ||
209 | + | ||
210 | + /** | ||
211 | + * Given arguments array as a String, subsituting type names for non-string types. | ||
212 | + * | ||
213 | + * @param {Arguments} object | ||
214 | + * @return {Array} of Strings with stringified arguments | ||
215 | + */ | ||
216 | + stringifyArguments: function(args) { | ||
217 | + for (var i = 0; i < args.length; ++i) { | ||
218 | + var arg = args[i]; | ||
219 | + if (arg === undefined) { | ||
220 | + args[i] = 'undefined'; | ||
221 | + } else if (arg === null) { | ||
222 | + args[i] = 'null'; | ||
223 | + } else if (arg.constructor) { | ||
224 | + if (arg.constructor === Array) { | ||
225 | + if (arg.length < 3) { | ||
226 | + args[i] = '[' + this.stringifyArguments(arg) + ']'; | ||
227 | + } else { | ||
228 | + args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']'; | ||
229 | + } | ||
230 | + } else if (arg.constructor === Object) { | ||
231 | + args[i] = '#object'; | ||
232 | + } else if (arg.constructor === Function) { | ||
233 | + args[i] = '#function'; | ||
234 | + } else if (arg.constructor === String) { | ||
235 | + args[i] = '"' + arg + '"'; | ||
236 | + } | ||
237 | + } | ||
238 | + } | ||
239 | + return args.join(','); | ||
240 | + }, | ||
241 | + | ||
242 | + sourceCache: {}, | ||
243 | + | ||
244 | + /** | ||
245 | + * @return the text from a given URL. | ||
246 | + */ | ||
247 | + ajax: function(url) { | ||
248 | + var req = this.createXMLHTTPObject(); | ||
249 | + if (!req) { | ||
250 | + return; | ||
251 | + } | ||
252 | + req.open('GET', url, false); | ||
253 | + // REMOVED FOR JS TEST. | ||
254 | + //req.setRequestHeader('User-Agent', 'XMLHTTP/1.0'); | ||
255 | + req.send(''); | ||
256 | + return req.responseText; | ||
257 | + }, | ||
258 | + | ||
259 | + /** | ||
260 | + * Try XHR methods in order and store XHR factory. | ||
261 | + * | ||
262 | + * @return <Function> XHR function or equivalent | ||
263 | + */ | ||
264 | + createXMLHTTPObject: function() { | ||
265 | + var xmlhttp, XMLHttpFactories = [ | ||
266 | + function() { | ||
267 | + return new XMLHttpRequest(); | ||
268 | + }, function() { | ||
269 | + return new ActiveXObject('Msxml2.XMLHTTP'); | ||
270 | + }, function() { | ||
271 | + return new ActiveXObject('Msxml3.XMLHTTP'); | ||
272 | + }, function() { | ||
273 | + return new ActiveXObject('Microsoft.XMLHTTP'); | ||
274 | + } | ||
275 | + ]; | ||
276 | + for (var i = 0; i < XMLHttpFactories.length; i++) { | ||
277 | + try { | ||
278 | + xmlhttp = XMLHttpFactories[i](); | ||
279 | + // Use memoization to cache the factory | ||
280 | + this.createXMLHTTPObject = XMLHttpFactories[i]; | ||
281 | + return xmlhttp; | ||
282 | + } catch (e) {} | ||
283 | + } | ||
284 | + }, | ||
143 | 285 | ||
144 | - backtrace.push('<line method="' + Hoptoad.escapeText(matches[1]) + | ||
145 | - '" file="' + Hoptoad.escapeText(file) + | ||
146 | - '" number="' + matches[3] + '" />'); | ||
147 | - } | 286 | + /** |
287 | + * Given a URL, check if it is in the same domain (so we can get the source | ||
288 | + * via Ajax). | ||
289 | + * | ||
290 | + * @param url <String> source url | ||
291 | + * @return False if we need a cross-domain request | ||
292 | + */ | ||
293 | + isSameDomain: function(url) { | ||
294 | + return url.indexOf(location.hostname) !== -1; | ||
295 | + }, | ||
296 | + | ||
297 | + /** | ||
298 | + * Get source code from given URL if in the same domain. | ||
299 | + * | ||
300 | + * @param url <String> JS source URL | ||
301 | + * @return <Array> Array of source code lines | ||
302 | + */ | ||
303 | + getSource: function(url) { | ||
304 | + if (!(url in this.sourceCache)) { | ||
305 | + this.sourceCache[url] = this.ajax(url).split('\n'); | ||
306 | + } | ||
307 | + return this.sourceCache[url]; | ||
308 | + }, | ||
309 | + | ||
310 | + guessFunctions: function(stack) { | ||
311 | + for (var i = 0; i < stack.length; ++i) { | ||
312 | + var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/; | ||
313 | + var frame = stack[i], m = reStack.exec(frame); | ||
314 | + if (m) { | ||
315 | + var file = m[1], lineno = m[4]; //m[7] is character position in Chrome | ||
316 | + if (file && this.isSameDomain(file) && lineno) { | ||
317 | + var functionName = this.guessFunctionName(file, lineno); | ||
318 | + stack[i] = frame.replace('{anonymous}', functionName); | ||
319 | + } | ||
320 | + } | ||
321 | + } | ||
322 | + return stack; | ||
323 | + }, | ||
324 | + | ||
325 | + guessFunctionName: function(url, lineNo) { | ||
326 | + try { | ||
327 | + return this.guessFunctionNameFromLines(lineNo, this.getSource(url)); | ||
328 | + } catch (e) { | ||
329 | + return 'getSource failed with url: ' + url + ', exception: ' + e.toString(); | ||
330 | + } | ||
331 | + }, | ||
332 | + | ||
333 | + guessFunctionNameFromLines: function(lineNo, source) { | ||
334 | + var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; | ||
335 | + var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; | ||
336 | + // Walk backwards from the first line in the function until we find the line which | ||
337 | + // matches the pattern above, which is the function definition | ||
338 | + var line = "", maxLines = 10; | ||
339 | + for (var i = 0; i < maxLines; ++i) { | ||
340 | + line = source[lineNo - i] + line; | ||
341 | + if (line !== undefined) { | ||
342 | + var m = reGuessFunction.exec(line); | ||
343 | + if (m && m[1]) { | ||
344 | + return m[1]; | ||
345 | + } else { | ||
346 | + m = reFunctionArgNames.exec(line); | ||
347 | + if (m && m[1]) { | ||
348 | + return m[1]; | ||
349 | + } | ||
350 | + } | ||
351 | + } | ||
352 | + } | ||
353 | + return '(?)'; | ||
148 | } | 354 | } |
355 | +};// Airbrake JavaScript Notifier | ||
356 | +(function() { | ||
357 | + "use strict"; | ||
358 | + | ||
359 | + var NOTICE_XML = '<?xml version="1.0" encoding="UTF-8"?>' + | ||
360 | + '<notice version="2.0">' + | ||
361 | + '<api-key>{key}</api-key>' + | ||
362 | + '<notifier>' + | ||
363 | + '<name>airbrake_js</name>' + | ||
364 | + '<version>0.2.0</version>' + | ||
365 | + '<url>http://airbrake.io</url>' + | ||
366 | + '</notifier>' + | ||
367 | + '<error>' + | ||
368 | + '<class>{exception_class}</class>' + | ||
369 | + '<message>{exception_message}</message>' + | ||
370 | + '<backtrace>{backtrace_lines}</backtrace>' + | ||
371 | + '</error>' + | ||
372 | + '<request>' + | ||
373 | + '<url>{request_url}</url>' + | ||
374 | + '<component>{request_component}</component>' + | ||
375 | + '<action>{request_action}</action>' + | ||
376 | + '{request}' + | ||
377 | + '</request>' + | ||
378 | + '<server-environment>' + | ||
379 | + '<project-root>{project_root}</project-root>' + | ||
380 | + '<environment-name>{environment}</environment-name>' + | ||
381 | + '</server-environment>' + | ||
382 | + '</notice>', | ||
383 | + REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>', | ||
384 | + REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>', | ||
385 | + BACKTRACE_LINE_XML = '<line method="{function}" file="{file}" number="{line}" />', | ||
386 | + Config, | ||
387 | + Global, | ||
388 | + Util, | ||
389 | + _publicAPI, | ||
390 | + | ||
391 | + NOTICE_JSON = { | ||
392 | + "notifier": { | ||
393 | + "name": "airbrake_js", | ||
394 | + "version": "0.2.0", | ||
395 | + "url": "http://airbrake.io" | ||
396 | + }, | ||
397 | + "error": [ | ||
398 | + { | ||
399 | + "type": "{exception_class}", | ||
400 | + "message": "{exception_message}", | ||
401 | + "backtrace": [] | ||
402 | + | ||
403 | + } | ||
404 | + ], | ||
405 | + "context": { | ||
406 | + "language": "JavaScript", | ||
407 | + "environment": "{environment}", | ||
408 | + | ||
409 | + "version": "1.1.1", | ||
410 | + "url": "{request_url}", | ||
411 | + "rootDirectory": "{project_root}", | ||
412 | + "action": "{request_action}", | ||
149 | 413 | ||
150 | - return backtrace; | ||
151 | - }, | 414 | + "userId": "{}", |
415 | + "userName": "{}", | ||
416 | + "userEmail": "{}", | ||
417 | + }, | ||
418 | + "environment": {}, | ||
419 | + //"session": "", | ||
420 | + "params": {}, | ||
421 | + }; | ||
152 | 422 | ||
153 | - getStackTrace: function(error) { | ||
154 | - var stacktrace = printStackTrace({ e : error, guess : false }); | 423 | + Util = { |
424 | + /* | ||
425 | + * Merge a number of objects into one. | ||
426 | + * | ||
427 | + * Usage example: | ||
428 | + * var obj1 = { | ||
429 | + * a: 'a' | ||
430 | + * }, | ||
431 | + * obj2 = { | ||
432 | + * b: 'b' | ||
433 | + * }, | ||
434 | + * obj3 = { | ||
435 | + * c: 'c' | ||
436 | + * }, | ||
437 | + * mergedObj = Util.merge(obj1, obj2, obj3); | ||
438 | + * | ||
439 | + * mergedObj is: { | ||
440 | + * a: 'a', | ||
441 | + * b: 'b', | ||
442 | + * c: 'c' | ||
443 | + * } | ||
444 | + * | ||
445 | + */ | ||
446 | + merge: (function() { | ||
447 | + function processProperty (key, dest, src) { | ||
448 | + if (src.hasOwnProperty(key)) { | ||
449 | + dest[key] = src[key]; | ||
450 | + } | ||
451 | + } | ||
155 | 452 | ||
156 | - for (var i = 0, l = stacktrace.length; i < l; i++) { | ||
157 | - if (stacktrace[i].match(/\:\d+$/)) { | ||
158 | - continue; | ||
159 | - } | 453 | + return function() { |
454 | + var objects = Array.prototype.slice.call(arguments), | ||
455 | + obj, | ||
456 | + key, | ||
457 | + result = {}; | ||
160 | 458 | ||
161 | - if (stacktrace[i].indexOf('@') == -1) { | ||
162 | - stacktrace[i] += '@unsupported.js'; | ||
163 | - } | 459 | + while (obj = objects.shift()) { |
460 | + for (key in obj) { | ||
461 | + processProperty(key, result, obj); | ||
462 | + } | ||
463 | + } | ||
164 | 464 | ||
165 | - stacktrace[i] += ':0'; | ||
166 | - } | 465 | + return result; |
466 | + }; | ||
467 | + })(), | ||
468 | + | ||
469 | + /* | ||
470 | + * Replace &, <, >, ', " characters with correspondent HTML entities. | ||
471 | + */ | ||
472 | + escape: function (text) { | ||
473 | + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | ||
474 | + .replace(/'/g, ''').replace(/"/g, '"'); | ||
475 | + }, | ||
476 | + | ||
477 | + /* | ||
478 | + * Remove leading and trailing space characters. | ||
479 | + */ | ||
480 | + trim: function (text) { | ||
481 | + return text.toString().replace(/^\s+/, '').replace(/\s+$/, ''); | ||
482 | + }, | ||
483 | + | ||
484 | + /* | ||
485 | + * Fill 'text' pattern with 'data' values. | ||
486 | + * | ||
487 | + * e.g. Utils.substitute('<{tag}></{tag}>', {tag: 'div'}, true) will return '<div></div>' | ||
488 | + * | ||
489 | + * emptyForUndefinedData - a flag, if true, all matched {<name>} without data.<name> value specified will be | ||
490 | + * replaced with empty string. | ||
491 | + */ | ||
492 | + substitute: function (text, data, emptyForUndefinedData) { | ||
493 | + return text.replace(/{([\w_.-]+)}/g, function(match, key) { | ||
494 | + return (key in data) ? data[key] : (emptyForUndefinedData ? '' : match); | ||
495 | + }); | ||
496 | + }, | ||
497 | + | ||
498 | + /* | ||
499 | + * Perform pattern rendering for an array of data objects. | ||
500 | + * Returns a concatenation of rendered strings of all objects in array. | ||
501 | + */ | ||
502 | + substituteArr: function (text, dataArr, emptyForUndefinedData) { | ||
503 | + var _i = 0, _l = 0, | ||
504 | + returnStr = ''; | ||
505 | + | ||
506 | + for (_i = 0, _l = dataArr.length; _i < _l; _i += 1) { | ||
507 | + returnStr += this.substitute(text, dataArr[_i], emptyForUndefinedData); | ||
508 | + } | ||
509 | + | ||
510 | + return returnStr; | ||
511 | + }, | ||
512 | + | ||
513 | + /* | ||
514 | + * Add hook for jQuery.fn.on function, to manualy call window.Airbrake.captureException() method | ||
515 | + * for every exception occurred. | ||
516 | + * | ||
517 | + * Let function 'f' be binded as an event handler: | ||
518 | + * | ||
519 | + * $(window).on 'click', f | ||
520 | + * | ||
521 | + * If an exception is occurred inside f's body, it will be catched here | ||
522 | + * and forwarded to captureException method. | ||
523 | + * | ||
524 | + * processjQueryEventHandlerWrapping is called every time window.Airbrake.setTrackJQ method is used, | ||
525 | + * if it switches previously setted value. | ||
526 | + */ | ||
527 | + processjQueryEventHandlerWrapping: function () { | ||
528 | + if (Config.options.trackJQ === true) { | ||
529 | + Config.jQuery_fn_on_original = Config.jQuery_fn_on_original || jQuery.fn.on; | ||
167 | 530 | ||
168 | - return stacktrace; | ||
169 | - }, | 531 | + jQuery.fn.on = function () { |
532 | + var args = Array.prototype.slice.call(arguments), | ||
533 | + fnArgIdx = 4; | ||
170 | 534 | ||
171 | - validBacktraceLine: function(line) { | ||
172 | - for (var i = 0; i < Hoptoad.backtrace_filters.length; i++) { | ||
173 | - if (line.match(Hoptoad.backtrace_filters[i])) { | ||
174 | - return false; | ||
175 | - } | ||
176 | - } | 535 | + // Search index of function argument |
536 | + while((--fnArgIdx > -1) && (typeof args[fnArgIdx] !== 'function')); | ||
537 | + | ||
538 | + // If the function is not found, then subscribe original event handler function | ||
539 | + if (fnArgIdx === -1) { | ||
540 | + return Config.jQuery_fn_on_original.apply(this, arguments); | ||
541 | + } | ||
542 | + | ||
543 | + // If the function is found, then subscribe wrapped event handler function | ||
544 | + args[fnArgIdx] = (function (fnOriginHandler) { | ||
545 | + return function() { | ||
546 | + try { | ||
547 | + fnOriginHandler.apply(this, arguments); | ||
548 | + } catch (e) { | ||
549 | + Global.captureException(e); | ||
550 | + } | ||
551 | + }; | ||
552 | + })(args[fnArgIdx]); | ||
553 | + | ||
554 | + // Call original jQuery.fn.on, with the same list of arguments, but | ||
555 | + // a function replaced with a proxy. | ||
556 | + return Config.jQuery_fn_on_original.apply(this, args); | ||
557 | + }; | ||
558 | + } else { | ||
559 | + // Recover original jQuery.fn.on if Config.options.trackJQ is set to false | ||
560 | + (typeof Config.jQuery_fn_on_original === 'function') && (jQuery.fn.on = Config.jQuery_fn_on_original); | ||
561 | + } | ||
562 | + }, | ||
177 | 563 | ||
178 | - return true; | ||
179 | - }, | 564 | + isjQueryPresent: function () { |
565 | + // Currently only 1.7.x version supported | ||
566 | + return (typeof jQuery === 'function') && ('fn' in jQuery) && ('jquery' in jQuery.fn) | ||
567 | + && (jQuery.fn.jquery.indexOf('1.7') === 0) | ||
568 | + }, | ||
569 | + | ||
570 | + /* | ||
571 | + * Make first letter in a string capital. e.g. 'guessFunctionName' -> 'GuessFunctionName' | ||
572 | + * Is used to generate getter and setter method names. | ||
573 | + */ | ||
574 | + capitalizeFirstLetter: function (str) { | ||
575 | + return str[0].toUpperCase() + str.slice(1); | ||
576 | + }, | ||
577 | + | ||
578 | + /* | ||
579 | + * Generate public API from an array of specifically formated objects, e.g. | ||
580 | + * | ||
581 | + * - this will generate 'setEnvironment' and 'getEnvironment' API methods for configObj.xmlData.environment variable: | ||
582 | + * { | ||
583 | + * variable: 'environment', | ||
584 | + * namespace: 'xmlData' | ||
585 | + * } | ||
586 | + * | ||
587 | + * - this will define 'method' function as 'captureException' API method | ||
588 | + * { | ||
589 | + * methodName: 'captureException', | ||
590 | + * method: (function (...) {...}); | ||
591 | + * } | ||
592 | + * | ||
593 | + */ | ||
594 | + generatePublicAPI: (function () { | ||
595 | + function _generateSetter (variable, namespace, configObj) { | ||
596 | + return function (value) { | ||
597 | + configObj[namespace][variable] = value; | ||
598 | + }; | ||
599 | + } | ||
600 | + | ||
601 | + function _generateGetter (variable, namespace, configObj) { | ||
602 | + return function (value) { | ||
603 | + return configObj[namespace][variable]; | ||
604 | + }; | ||
605 | + } | ||
606 | + | ||
607 | + /* | ||
608 | + * publicAPI: array of specifically formated objects | ||
609 | + * configObj: inner configuration object | ||
610 | + */ | ||
611 | + return function (publicAPI, configObj) { | ||
612 | + var _i = 0, _m = null, _capitalized = '', | ||
613 | + returnObj = {}; | ||
614 | + | ||
615 | + for (_i = 0; _i < publicAPI.length; _i += 1) { | ||
616 | + _m = publicAPI[_i]; | ||
617 | + | ||
618 | + switch (true) { | ||
619 | + case (typeof _m.variable !== 'undefined') && (typeof _m.methodName === 'undefined'): | ||
620 | + _capitalized = Util.capitalizeFirstLetter(_m.variable) | ||
621 | + returnObj['set' + _capitalized] = _generateSetter(_m.variable, _m.namespace, configObj); | ||
622 | + returnObj['get' + _capitalized] = _generateGetter(_m.variable, _m.namespace, configObj); | ||
623 | + | ||
624 | + break; | ||
625 | + case (typeof _m.methodName !== 'undefined') && (typeof _m.method !== 'undefined'): | ||
626 | + returnObj[_m.methodName] = _m.method | ||
627 | + | ||
628 | + break; | ||
629 | + | ||
630 | + default: | ||
631 | + } | ||
632 | + } | ||
633 | + | ||
634 | + return returnObj; | ||
635 | + }; | ||
636 | + } ()) | ||
637 | + }; | ||
638 | + | ||
639 | + /* | ||
640 | + * The object to store settings. Allocated from the Global (windows scope) so that users can change settings | ||
641 | + * only through the methods, rather than through a direct change of the object fileds. So that we can to handle | ||
642 | + * change settings event (in setter method). | ||
643 | + */ | ||
644 | + Config = { | ||
645 | + xmlData: { | ||
646 | + environment: 'environment' | ||
647 | + }, | ||
648 | + | ||
649 | + options: { | ||
650 | + trackJQ: false, // jQuery.fn.jquery | ||
651 | + host: 'api.airbrake.io', | ||
652 | + errorDefaults: {}, | ||
653 | + guessFunctionName: false, | ||
654 | + requestType: 'GET', // Can be 'POST' or 'GET' | ||
655 | + outputFormat: 'XML' // Can be 'XML' or 'JSON' | ||
656 | + } | ||
657 | + }; | ||
658 | + | ||
659 | + /* | ||
660 | + * The public API definition object. If no 'methodName' and 'method' values specified, | ||
661 | + * getter and setter for 'variable' will be defined. | ||
662 | + */ | ||
663 | + _publicAPI = [ | ||
664 | + { | ||
665 | + variable: 'environment', | ||
666 | + namespace: 'xmlData' | ||
667 | + }, { | ||
668 | + variable: 'key', | ||
669 | + namespace: 'xmlData' | ||
670 | + }, { | ||
671 | + variable: 'host', | ||
672 | + namespace: 'options' | ||
673 | + },{ | ||
674 | + variable: 'projectId', | ||
675 | + namespace: 'options' | ||
676 | + },{ | ||
677 | + variable: 'errorDefaults', | ||
678 | + namespace: 'options' | ||
679 | + }, { | ||
680 | + variable: 'guessFunctionName', | ||
681 | + namespace: 'options' | ||
682 | + }, { | ||
683 | + variable: 'outputFormat', | ||
684 | + namespace: 'options' | ||
685 | + }, { | ||
686 | + methodName: 'setTrackJQ', | ||
687 | + variable: 'trackJQ', | ||
688 | + namespace: 'options', | ||
689 | + method: (function (value) { | ||
690 | + if (!Util.isjQueryPresent()) { | ||
691 | + throw Error('Please do not call \'Airbrake.setTrackJQ\' if jQuery does\'t present'); | ||
692 | + } | ||
693 | + | ||
694 | + value = !!value; | ||
695 | + | ||
696 | + if (Config.options.trackJQ === value) { | ||
697 | + return; | ||
698 | + } | ||
699 | + | ||
700 | + Config.options.trackJQ = value; | ||
701 | + | ||
702 | + Util.processjQueryEventHandlerWrapping(); | ||
703 | + }) | ||
704 | + }, { | ||
705 | + methodName: 'captureException', | ||
706 | + method: (function (e) { | ||
707 | + new Notifier().notify({ | ||
708 | + message: e.message, | ||
709 | + stack: e.stack | ||
710 | + }); | ||
711 | + }) | ||
712 | + } | ||
713 | + ]; | ||
180 | 714 | ||
181 | - generateVariables: function(parameters) { | ||
182 | - var key; | ||
183 | - var result = ''; | 715 | + // Share to global scope as Airbrake ("window.Hoptoad" for backward compatibility) |
716 | + Global = window.Airbrake = window.Hoptoad = Util.generatePublicAPI(_publicAPI, Config); | ||
184 | 717 | ||
185 | - for (key in parameters) { | ||
186 | - result += '<var key="' + Hoptoad.escapeText(key) + '">' + | ||
187 | - Hoptoad.escapeText(parameters[key]) + | ||
188 | - '</var>'; | 718 | + function Notifier() { |
719 | + this.options = Util.merge({}, Config.options); | ||
720 | + this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData); | ||
189 | } | 721 | } |
722 | + | ||
723 | + Notifier.prototype = { | ||
724 | + constructor: Notifier, | ||
725 | + VERSION: '0.2.0', | ||
726 | + ROOT: window.location.protocol + '//' + window.location.host, | ||
727 | + BACKTRACE_MATCHER: /^(.*)\@(.*)\:(\d+)$/, | ||
728 | + backtrace_filters: [/notifier\.js/], | ||
729 | + DEF_XML_DATA: { | ||
730 | + request: {} | ||
731 | + }, | ||
190 | 732 | ||
191 | - return result; | ||
192 | - }, | 733 | + notify: (function () { |
734 | + /* | ||
735 | + * Emit GET request via <iframe> element. | ||
736 | + * Data is transmited as a part of query string. | ||
737 | + */ | ||
738 | + function _sendGETRequest (url, data) { | ||
739 | + var request = document.createElement('iframe'); | ||
740 | + | ||
741 | + request.style.display = 'none'; | ||
742 | + request.src = url + '?data=' + data; | ||
743 | + | ||
744 | + // When request has been sent, delete iframe | ||
745 | + request.onload = function () { | ||
746 | + // To avoid infinite progress indicator | ||
747 | + setTimeout(function() { | ||
748 | + document.body.removeChild(request); | ||
749 | + }, 0); | ||
750 | + }; | ||
751 | + | ||
752 | + document.body.appendChild(request); | ||
753 | + } | ||
754 | + | ||
755 | + /* | ||
756 | + * Cross-domain AJAX POST request. | ||
757 | + * | ||
758 | + * It requires a server setup as described in Cross-Origin Resource Sharing spec: | ||
759 | + * http://www.w3.org/TR/cors/ | ||
760 | + */ | ||
761 | + function _sendPOSTRequest (url, data) { | ||
762 | + var request = new XMLHttpRequest(); | ||
763 | + request.open('POST', url, true); | ||
764 | + request.setRequestHeader('Content-Type', 'application/json'); | ||
765 | + request.send(data); | ||
766 | + } | ||
767 | + | ||
768 | + return function (error) { | ||
769 | + var outputData = '', | ||
770 | + url = ''; | ||
771 | + // | ||
772 | + | ||
773 | + /* | ||
774 | + * Should be changed to url = '//' + ... | ||
775 | + * to use the protocol of current page (http or https). Only sends 'secure' if page is secure. | ||
776 | + * XML uses V2 API. http://collect.airbrake.io/notifier_api/v2/notices | ||
777 | + */ | ||
778 | + | ||
779 | + | ||
780 | + switch (this.options['outputFormat']) { | ||
781 | + case 'XML': | ||
782 | + outputData = encodeURIComponent(this.generateXML(this.generateDataJSON(error))); | ||
783 | + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/notifier_api/v2/notices'; | ||
784 | + _sendGETRequest(url, outputData); | ||
785 | + break; | ||
193 | 786 | ||
194 | - escapeText: function(text) { | ||
195 | - return text.replace(/&/g, '&') | ||
196 | - .replace(/</g, '<') | ||
197 | - .replace(/>/g, '>') | ||
198 | - .replace(/'/g, ''') | ||
199 | - .replace(/"/g, '"'); | ||
200 | - }, | 787 | + case 'JSON': |
788 | + /* | ||
789 | + * JSON uses API V3. Needs project in URL. | ||
790 | + * http://collect.airbrake.io/api/v3/projects/[PROJECT_ID]/notices?key=[API_KEY] | ||
791 | + * url = window.location.protocol + '://' + this.options.host + '/api/v3/projects' + this.options.projectId + '/notices?key=' + this.options.key; | ||
792 | + */ | ||
793 | + outputData = JSON.stringify(this.generateJSON(this.generateDataJSON(error))); | ||
794 | + url = ('https:' == document.location.protocol ? 'https://' : 'http://') + this.options.host + '/api/v3/projects/' + this.options.projectId + '/notices?key=' + this.xmlData.key; | ||
795 | + _sendPOSTRequest(url, outputData); | ||
796 | + break; | ||
201 | 797 | ||
202 | - trim: function(text) { | ||
203 | - return text.toString().replace(/^\s+/, '').replace(/\s+$/, ''); | ||
204 | - }, | 798 | + default: |
799 | + } | ||
205 | 800 | ||
206 | - mergeDefault: function(defaults, hash) { | ||
207 | - var cloned = {}; | ||
208 | - var key; | 801 | + }; |
802 | + } ()), | ||
803 | + | ||
804 | + /* | ||
805 | + * Generate inner JSON representation of exception data that can be rendered as XML or JSON. | ||
806 | + */ | ||
807 | + generateDataJSON: (function () { | ||
808 | + /* | ||
809 | + * Generate variables array for inputObj object. | ||
810 | + * | ||
811 | + * e.g. | ||
812 | + * | ||
813 | + * _generateVariables({a: 'a'}) -> [{key: 'a', value: 'a'}] | ||
814 | + * | ||
815 | + */ | ||
816 | + function _generateVariables (inputObj) { | ||
817 | + var key = '', returnArr = []; | ||
818 | + | ||
819 | + for (key in inputObj) { | ||
820 | + if (inputObj.hasOwnProperty(key)) { | ||
821 | + returnArr.push({ | ||
822 | + key: key, | ||
823 | + value: inputObj[key] | ||
824 | + }); | ||
825 | + } | ||
826 | + } | ||
827 | + | ||
828 | + return returnArr; | ||
829 | + } | ||
830 | + | ||
831 | + /* | ||
832 | + * Generate Request part of notification. | ||
833 | + */ | ||
834 | + function _composeRequestObj (methods, errorObj) { | ||
835 | + var _i = 0, | ||
836 | + returnObj = {}, | ||
837 | + type = ''; | ||
838 | + | ||
839 | + for (_i = 0; _i < methods.length; _i += 1) { | ||
840 | + type = methods[_i]; | ||
841 | + if (typeof errorObj[type] !== 'undefined') { | ||
842 | + returnObj[type] = _generateVariables(errorObj[type]); | ||
843 | + } | ||
844 | + } | ||
845 | + | ||
846 | + return returnObj; | ||
847 | + } | ||
848 | + | ||
849 | + return function (errorWithoutDefaults) { | ||
850 | + /* | ||
851 | + * A constructor line: | ||
852 | + * | ||
853 | + * this.xmlData = Util.merge(this.DEF_XML_DATA, Config.xmlData); | ||
854 | + */ | ||
855 | + var outputData = this.xmlData, | ||
856 | + error = Util.merge(this.options.errorDefaults, errorWithoutDefaults), | ||
857 | + | ||
858 | + component = error.component || '', | ||
859 | + request_url = (error.url || '' + location.hash), | ||
860 | + | ||
861 | + methods = ['cgi-data', 'params', 'session'], | ||
862 | + _outputData = null; | ||
863 | + | ||
864 | + _outputData = { | ||
865 | + request_url: request_url, | ||
866 | + request_action: (error.action || ''), | ||
867 | + request_component: component, | ||
868 | + request: (function () { | ||
869 | + if (request_url || component) { | ||
870 | + error['cgi-data'] = error['cgi-data'] || {}; | ||
871 | + error['cgi-data'].HTTP_USER_AGENT = navigator.userAgent; | ||
872 | + return Util.merge(outputData.request, _composeRequestObj(methods, error)); | ||
873 | + } else { | ||
874 | + return {} | ||
875 | + } | ||
876 | + } ()), | ||
877 | + | ||
878 | + project_root: this.ROOT, | ||
879 | + exception_class: (error.type || 'Error'), | ||
880 | + exception_message: (error.message || 'Unknown error.'), | ||
881 | + backtrace_lines: this.generateBacktrace(error) | ||
882 | + } | ||
883 | + | ||
884 | + outputData = Util.merge(outputData, _outputData); | ||
885 | + | ||
886 | + return outputData; | ||
887 | + }; | ||
888 | + } ()), | ||
889 | + | ||
890 | + /* | ||
891 | + * Generate XML notification from inner JSON representation. | ||
892 | + * NOTICE_XML is used as pattern. | ||
893 | + */ | ||
894 | + generateXML: (function () { | ||
895 | + function _generateRequestVariableGroups (requestObj) { | ||
896 | + var _group = '', | ||
897 | + returnStr = ''; | ||
898 | + | ||
899 | + for (_group in requestObj) { | ||
900 | + if (requestObj.hasOwnProperty(_group)) { | ||
901 | + returnStr += Util.substitute(REQUEST_VARIABLE_GROUP_XML, { | ||
902 | + group_name: _group, | ||
903 | + inner_content: Util.substituteArr(REQUEST_VARIABLE_XML, requestObj[_group], true) | ||
904 | + }, true); | ||
905 | + } | ||
906 | + } | ||
907 | + | ||
908 | + return returnStr; | ||
909 | + } | ||
910 | + | ||
911 | + return function (JSONdataObj) { | ||
912 | + JSONdataObj.request = _generateRequestVariableGroups(JSONdataObj.request); | ||
913 | + JSONdataObj.backtrace_lines = Util.substituteArr(BACKTRACE_LINE_XML, JSONdataObj.backtrace_lines, true); | ||
914 | + | ||
915 | + return Util.substitute(NOTICE_XML, JSONdataObj, true); | ||
916 | + }; | ||
917 | + } ()), | ||
918 | + | ||
919 | + /* | ||
920 | + * Generate JSON notification from inner JSON representation. | ||
921 | + * NOTICE_JSON is used as pattern. | ||
922 | + */ | ||
923 | + generateJSON: function (JSONdataObj) { | ||
924 | + // Pattern string is JSON.stringify(NOTICE_JSON) | ||
925 | + // The rendered string is parsed back as JSON. | ||
926 | + var outputJSON = JSON.parse(Util.substitute(JSON.stringify(NOTICE_JSON), JSONdataObj, true)); | ||
927 | + | ||
928 | + // REMOVED - Request from JSON. | ||
929 | + outputJSON.request = Util.merge(outputJSON.request, JSONdataObj.request); | ||
930 | + outputJSON.error.backtrace = JSONdataObj.backtrace_lines; | ||
931 | + | ||
932 | + return outputJSON; | ||
933 | + }, | ||
934 | + | ||
935 | + generateBacktrace: function (error) { | ||
936 | + var backtrace = [], | ||
937 | + file, | ||
938 | + i, | ||
939 | + matches, | ||
940 | + stacktrace; | ||
209 | 941 | ||
210 | - for (key in hash) { | ||
211 | - cloned[key] = hash[key]; | ||
212 | - } | 942 | + error = error || {}; |
213 | 943 | ||
214 | - for (key in defaults) { | ||
215 | - if (!cloned.hasOwnProperty(key)) { | ||
216 | - cloned[key] = defaults[key]; | ||
217 | - } | ||
218 | - } | 944 | + if (typeof error.stack !== 'string') { |
945 | + try { | ||
946 | + (0)(); | ||
947 | + } catch (e) { | ||
948 | + error.stack = e.stack; | ||
949 | + } | ||
950 | + } | ||
219 | 951 | ||
220 | - return cloned; | ||
221 | - } | ||
222 | -}; | 952 | + stacktrace = this.getStackTrace(error); |
223 | 953 | ||
224 | -// From: http://stacktracejs.com/ | ||
225 | -// | ||
226 | -// Domain Public by Eric Wendelin http://eriwen.com/ (2008) | ||
227 | -// Luke Smith http://lucassmith.name/ (2008) | ||
228 | -// Loic Dachary <loic@dachary.org> (2008) | ||
229 | -// Johan Euphrosine <proppy@aminche.com> (2008) | ||
230 | -// Oyvind Sean Kinsey http://kinsey.no/blog (2010) | ||
231 | -// Victor Homyakov <victor-homyakov@users.sourceforge.net> (2010) | ||
232 | - | ||
233 | -function printStackTrace(a){var a=a||{guess:!0},b=a.e||null,a=!!a.guess,d=new printStackTrace.implementation,b=d.run(b);return a?d.guessAnonymousFunctions(b):b}printStackTrace.implementation=function(){}; | ||
234 | -printStackTrace.implementation.prototype={run:function(a,b){a=a||this.createException();b=b||this.mode(a);return"other"===b?this.other(arguments.callee):this[b](a)},createException:function(){try{this.undef()}catch(a){return a}},mode:function(a){return a.arguments&&a.stack?"chrome":a.stack&&a.sourceURL?"safari":"string"===typeof a.message&&"undefined"!==typeof window&&window.opera?!a.stacktrace||-1<a.message.indexOf("\n")&&a.message.split("\n").length>a.stacktrace.split("\n").length?"opera9":!a.stack? | ||
235 | -"opera10a":0>a.stacktrace.indexOf("called from line")?"opera10b":"opera11":a.stack?"firefox":"other"},instrumentFunction:function(a,b,d){var a=a||window,c=a[b];a[b]=function(){d.call(this,printStackTrace().slice(4));return a[b]._instrumented.apply(this,arguments)};a[b]._instrumented=c},deinstrumentFunction:function(a,b){a[b].constructor===Function&&(a[b]._instrumented&&a[b]._instrumented.constructor===Function)&&(a[b]=a[b]._instrumented)},chrome:function(a){a=(a.stack+"\n").replace(/^\S[^\(]+?[\n$]/gm, | ||
236 | -"").replace(/^\s+(at eval )?at\s+/gm,"").replace(/^([^\(]+?)([\n$])/gm,"{anonymous}()@$1$2").replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm,"{anonymous}()@$1").split("\n");a.pop();return a},safari:function(a){return a.stack.replace(/\[native code\]\n/m,"").replace(/^@/gm,"{anonymous}()@").split("\n")},firefox:function(a){return a.stack.replace(/(?:\n@:0)?\s+$/m,"").replace(/^[\(@]/gm,"{anonymous}()@").split("\n")},opera11:function(a){for(var b=/^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/, | ||
237 | -a=a.stacktrace.split("\n"),d=[],c=0,f=a.length;c<f;c+=2){var e=b.exec(a[c]);if(e){var g=e[4]+":"+e[1]+":"+e[2],e=e[3]||"global code",e=e.replace(/<anonymous function: (\S+)>/,"$1").replace(/<anonymous function>/,"{anonymous}");d.push(e+"@"+g+" -- "+a[c+1].replace(/^\s+/,""))}}return d},opera10b:function(a){for(var b=/^(.*)@(.+):(\d+)$/,a=a.stacktrace.split("\n"),d=[],c=0,f=a.length;c<f;c++){var e=b.exec(a[c]);e&&d.push((e[1]?e[1]+"()":"global code")+"@"+e[2]+":"+e[3])}return d},opera10a:function(a){for(var b= | ||
238 | -/Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i,a=a.stacktrace.split("\n"),d=[],c=0,f=a.length;c<f;c+=2){var e=b.exec(a[c]);e&&d.push((e[3]||"{anonymous}")+"()@"+e[2]+":"+e[1]+" -- "+a[c+1].replace(/^\s+/,""))}return d},opera9:function(a){for(var b=/Line (\d+).*script (?:in )?(\S+)/i,a=a.message.split("\n"),d=[],c=2,f=a.length;c<f;c+=2){var e=b.exec(a[c]);e&&d.push("{anonymous}()@"+e[2]+":"+e[1]+" -- "+a[c+1].replace(/^\s+/,""))}return d},other:function(a){for(var b=/function\s*([\w\-$]+)?\s*\(/i, | ||
239 | -d=[],c,f;a&&a.arguments&&10>d.length;)c=b.test(a.toString())?RegExp.$1||"{anonymous}":"{anonymous}",f=Array.prototype.slice.call(a.arguments||[]),d[d.length]=c+"("+this.stringifyArguments(f)+")",a=a.caller;return d},stringifyArguments:function(a){for(var b=[],d=Array.prototype.slice,c=0;c<a.length;++c){var f=a[c];void 0===f?b[c]="undefined":null===f?b[c]="null":f.constructor&&(f.constructor===Array?b[c]=3>f.length?"["+this.stringifyArguments(f)+"]":"["+this.stringifyArguments(d.call(f,0,1))+"..."+ | ||
240 | -this.stringifyArguments(d.call(f,-1))+"]":f.constructor===Object?b[c]="#object":f.constructor===Function?b[c]="#function":f.constructor===String?b[c]='"'+f+'"':f.constructor===Number&&(b[c]=f))}return b.join(",")},sourceCache:{},ajax:function(a){var b=this.createXMLHTTPObject();if(b)try{return b.open("GET",a,!1),b.send(null),b.responseText}catch(d){}return""},createXMLHTTPObject:function(){for(var a,b=[function(){return new XMLHttpRequest},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")}, | ||
241 | -function(){return new ActiveXObject("Microsoft.XMLHTTP")}],d=0;d<b.length;d++)try{return a=b[d](),this.createXMLHTTPObject=b[d],a}catch(c){}},isSameDomain:function(a){return"undefined"!==typeof location&&-1!==a.indexOf(location.hostname)},getSource:function(a){a in this.sourceCache||(this.sourceCache[a]=this.ajax(a).split("\n"));return this.sourceCache[a]},guessAnonymousFunctions:function(a){for(var b=0;b<a.length;++b){var d=/^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/,c=a[b],f=/\{anonymous\}\(.*\)@(.*)/.exec(c); | ||
242 | -if(f){var e=d.exec(f[1]);e&&(d=e[1],f=e[2],e=e[3]||0,d&&(this.isSameDomain(d)&&f)&&(d=this.guessAnonymousFunction(d,f,e),a[b]=c.replace("{anonymous}",d)))}}return a},guessAnonymousFunction:function(a,b){var d;try{d=this.findFunctionName(this.getSource(a),b)}catch(c){d="getSource failed with url: "+a+", exception: "+c.toString()}return d},findFunctionName:function(a,b){for(var d=/function\s+([^(]*?)\s*\(([^)]*)\)/,c=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/,f=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/, | ||
243 | -e="",g,j=Math.min(b,20),h,i=0;i<j;++i)if(g=a[b-i-1],h=g.indexOf("//"),0<=h&&(g=g.substr(0,h)),g)if(e=g+e,(g=c.exec(e))&&g[1]||(g=d.exec(e))&&g[1]||(g=f.exec(e))&&g[1])return g[1];return"(?)"}}; | ||
244 | - | ||
245 | -window.onerror = function(message, file, line) { | ||
246 | - setTimeout(function() { | ||
247 | - Hoptoad.notify({ | ||
248 | - message : message, | ||
249 | - stack : '()@' + file + ':' + line, | ||
250 | - url : document.location.href | ||
251 | - }); | ||
252 | - }, 100); | ||
253 | - return true; | ||
254 | -}; | 954 | + for (i = 0; i < stacktrace.length; i++) { |
955 | + matches = stacktrace[i].match(this.BACKTRACE_MATCHER); | ||
956 | + | ||
957 | + if (matches && this.validBacktraceLine(stacktrace[i])) { | ||
958 | + file = matches[2].replace(this.ROOT, '[PROJECT_ROOT]'); | ||
959 | + | ||
960 | + if (i === 0 && matches[2].match(document.location.href)) { | ||
961 | + // backtrace.push('<line method="" file="internal: " number=""/>'); | ||
962 | + | ||
963 | + backtrace.push({ | ||
964 | + // Updated to fit in with V3 new terms for Backtrace data. | ||
965 | + 'function': '', | ||
966 | + file: 'internal: ', | ||
967 | + line: '' | ||
968 | + }); | ||
969 | + } | ||
970 | + | ||
971 | + // backtrace.push('<line method="' + Util.escape(matches[1]) + '" file="' + Util.escape(file) + | ||
972 | + // '" number="' + matches[3] + '" />'); | ||
973 | + | ||
974 | + backtrace.push({ | ||
975 | + 'function': matches[1], | ||
976 | + file: file, | ||
977 | + line: matches[3] | ||
978 | + }); | ||
979 | + } | ||
980 | + } | ||
981 | + | ||
982 | + return backtrace; | ||
983 | + }, | ||
984 | + | ||
985 | + getStackTrace: function (error) { | ||
986 | + var i, | ||
987 | + stacktrace = printStackTrace({ | ||
988 | + e: error, | ||
989 | + guess: this.options.guessFunctionName | ||
990 | + }); | ||
991 | + | ||
992 | + for (i = 0; i < stacktrace.length; i++) { | ||
993 | + if (stacktrace[i].match(/\:\d+$/)) { | ||
994 | + continue; | ||
995 | + } | ||
996 | + | ||
997 | + if (stacktrace[i].indexOf('@') === -1) { | ||
998 | + stacktrace[i] += '@unsupported.js'; | ||
999 | + } | ||
1000 | + | ||
1001 | + stacktrace[i] += ':0'; | ||
1002 | + } | ||
1003 | + | ||
1004 | + return stacktrace; | ||
1005 | + }, | ||
1006 | + | ||
1007 | + validBacktraceLine: function (line) { | ||
1008 | + for (var i = 0; i < this.backtrace_filters.length; i++) { | ||
1009 | + if (line.match(this.backtrace_filters[i])) { | ||
1010 | + return false; | ||
1011 | + } | ||
1012 | + } | ||
1013 | + | ||
1014 | + return true; | ||
1015 | + } | ||
1016 | + }; | ||
1017 | + | ||
1018 | + window.onerror = function (message, file, line) { | ||
1019 | + setTimeout(function () { | ||
1020 | + new Notifier().notify({ | ||
1021 | + message: message, | ||
1022 | + stack: '()@' + file + ':' + line | ||
1023 | + }); | ||
1024 | + }, 0); | ||
255 | 1025 | ||
1026 | + return true; | ||
1027 | + }; | ||
1028 | +})(); | ||
1029 | +})(window, document); |
spec/controllers/users_controller_spec.rb
@@ -79,6 +79,11 @@ describe UsersController do | @@ -79,6 +79,11 @@ describe UsersController do | ||
79 | @user.reload.time_zone.should == "Warsaw" | 79 | @user.reload.time_zone.should == "Warsaw" |
80 | end | 80 | end |
81 | 81 | ||
82 | + it "should be able to not set github_login option" do | ||
83 | + put :update, :id => @user.to_param, :user => {:github_login => " "} | ||
84 | + @user.reload.github_login.should == nil | ||
85 | + end | ||
86 | + | ||
82 | it "should be able to set github_login option" do | 87 | it "should be able to set github_login option" do |
83 | put :update, :id => @user.to_param, :user => {:github_login => "awesome_name"} | 88 | put :update, :id => @user.to_param, :user => {:github_login => "awesome_name"} |
84 | @user.reload.github_login.should == "awesome_name" | 89 | @user.reload.github_login.should == "awesome_name" |
spec/fabricators/app_fabricator.rb
spec/fabricators/notification_service_fabricator.rb
@@ -12,6 +12,6 @@ Fabricator :gtalk_notification_service, :from => :notification_service, :class_n | @@ -12,6 +12,6 @@ Fabricator :gtalk_notification_service, :from => :notification_service, :class_n | ||
12 | service { sequence :word } | 12 | service { sequence :word } |
13 | end | 13 | end |
14 | 14 | ||
15 | -%w(campfire hipchat hoiio pushover hubot).each do |t| | 15 | +%w(campfire hipchat hoiio pushover hubot webhook).each do |t| |
16 | Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service" | 16 | Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service" |
17 | end | 17 | end |
@@ -0,0 +1,20 @@ | @@ -0,0 +1,20 @@ | ||
1 | +require 'spec_helper' | ||
2 | + | ||
3 | +describe BacktraceLineHelper do | ||
4 | + describe "in app lines" do | ||
5 | + let(:notice) do | ||
6 | + Fabricate.build(:notice, :backtrace => | ||
7 | + Fabricate.build(:backtrace, :lines => [ | ||
8 | + Fabricate.build(:backtrace_line, :file => "[PROJECT_ROOT]/path/to/asset.js") | ||
9 | + ]) | ||
10 | + ) | ||
11 | + end | ||
12 | + | ||
13 | + describe '#link_to_source_file' do | ||
14 | + it 'still returns text for in app file and line number when no repo is configured' do | ||
15 | + result = link_to_source_file(notice.backtrace.lines.first) { haml_concat "link text" } | ||
16 | + result.strip.should == 'link text' | ||
17 | + end | ||
18 | + end | ||
19 | + end | ||
20 | +end |
spec/mailers/mailer_spec.rb
@@ -6,24 +6,39 @@ describe Mailer do | @@ -6,24 +6,39 @@ describe Mailer do | ||
6 | include EmailSpec::Matchers | 6 | include EmailSpec::Matchers |
7 | 7 | ||
8 | let(:notice) { Fabricate(:notice, :message => "class < ActionController::Base") } | 8 | let(:notice) { Fabricate(:notice, :message => "class < ActionController::Base") } |
9 | - let!(:email) { Mailer.err_notification(notice).deliver } | 9 | + |
10 | + before do | ||
11 | + notice.backtrace.lines.last.update_attributes(:file => "[PROJECT_ROOT]/path/to/file.js") | ||
12 | + notice.app.update_attributes :asset_host => "http://example.com" | ||
13 | + notice.problem.update_attributes :notices_count => 3 | ||
14 | + | ||
15 | + @email = Mailer.err_notification(notice).deliver | ||
16 | + end | ||
10 | 17 | ||
11 | it "should send the email" do | 18 | it "should send the email" do |
12 | ActionMailer::Base.deliveries.size.should == 1 | 19 | ActionMailer::Base.deliveries.size.should == 1 |
13 | end | 20 | end |
14 | 21 | ||
15 | it "should html-escape the notice's message for the html part" do | 22 | it "should html-escape the notice's message for the html part" do |
16 | - email.should have_body_text("class < ActionController::Base") | 23 | + @email.should have_body_text("class < ActionController::Base") |
17 | end | 24 | end |
18 | 25 | ||
19 | it "should have inline css" do | 26 | it "should have inline css" do |
20 | - email.should have_body_text('<p class="backtrace" style="') | 27 | + @email.should have_body_text('<p class="backtrace" style="') |
28 | + end | ||
29 | + | ||
30 | + it "should have links to source files" do | ||
31 | + @email.should have_body_text('<a href="http://example.com/path/to/file.js" target="_blank">path/to/file.js') | ||
32 | + end | ||
33 | + | ||
34 | + it "should have the error count in the subject" do | ||
35 | + @email.subject.should =~ /^\(3\) / | ||
21 | end | 36 | end |
22 | 37 | ||
23 | context 'with a very long message' do | 38 | context 'with a very long message' do |
24 | let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) } | 39 | let(:notice) { Fabricate(:notice, :message => 6.times.collect{|a| "0123456789" }.join('')) } |
25 | it "should truncate the long message" do | 40 | it "should truncate the long message" do |
26 | - email.subject.should =~ / \d{47}\.{3}$/ | 41 | + @email.subject.should =~ / \d{47}\.{3}$/ |
27 | end | 42 | end |
28 | end | 43 | end |
29 | end | 44 | end |
spec/models/app_spec.rb
@@ -258,10 +258,10 @@ describe App do | @@ -258,10 +258,10 @@ describe App do | ||
258 | 258 | ||
259 | it 'captures the current_user' do | 259 | it 'captures the current_user' do |
260 | @notice = App.report_error!(@xml) | 260 | @notice = App.report_error!(@xml) |
261 | - @notice.current_user['id'].should == '123' | ||
262 | - @notice.current_user['name'].should == 'Mr. Bean' | ||
263 | - @notice.current_user['email'].should == 'mr.bean@example.com' | ||
264 | - @notice.current_user['username'].should == 'mrbean' | 261 | + @notice.user_attributes['id'].should == '123' |
262 | + @notice.user_attributes['name'].should == 'Mr. Bean' | ||
263 | + @notice.user_attributes['email'].should == 'mr.bean@example.com' | ||
264 | + @notice.user_attributes['username'].should == 'mrbean' | ||
265 | end | 265 | end |
266 | 266 | ||
267 | it 'captures the framework' do | 267 | it 'captures the framework' do |
spec/models/backtrace_line_normalizer_spec.rb
@@ -3,16 +3,25 @@ require 'spec_helper' | @@ -3,16 +3,25 @@ require 'spec_helper' | ||
3 | describe BacktraceLineNormalizer do | 3 | describe BacktraceLineNormalizer do |
4 | subject { described_class.new(raw_line).call } | 4 | subject { described_class.new(raw_line).call } |
5 | 5 | ||
6 | - describe "sanitize file and method" do | ||
7 | - let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => nil } } | 6 | + describe "sanitize" do |
7 | + context "unknown file and method" do | ||
8 | + let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => nil } } | ||
8 | 9 | ||
9 | - it "should replace nil file with [unknown source]" do | ||
10 | - subject['file'].should == "[unknown source]" | ||
11 | - end | 10 | + it "should replace nil file with [unknown source]" do |
11 | + subject['file'].should == "[unknown source]" | ||
12 | + end | ||
12 | 13 | ||
13 | - it "should replace nil method with [unknown method]" do | ||
14 | - subject['method'].should == "[unknown method]" | 14 | + it "should replace nil method with [unknown method]" do |
15 | + subject['method'].should == "[unknown method]" | ||
16 | + end | ||
15 | end | 17 | end |
16 | 18 | ||
19 | + context "in app file" do | ||
20 | + let(:raw_line) { { 'number' => rand(999), 'file' => "[PROJECT_ROOT]/assets/file.js?body=1", 'method' => nil } } | ||
21 | + | ||
22 | + it "should strip query strings from files" do | ||
23 | + subject['file'].should == "[PROJECT_ROOT]/assets/file.js" | ||
24 | + end | ||
25 | + end | ||
17 | end | 26 | end |
18 | end | 27 | end |
spec/models/issue_trackers/redmine_tracker_spec.rb
@@ -8,13 +8,17 @@ describe IssueTrackers::RedmineTracker do | @@ -8,13 +8,17 @@ describe IssueTrackers::RedmineTracker do | ||
8 | number = 5 | 8 | number = 5 |
9 | @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" | 9 | @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}" |
10 | body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" | 10 | body = "<issue><subject>my subject</subject><id>#{number}</id></issue>" |
11 | - stub_request(:post, "#{tracker.account}/issues.xml"). | 11 | + |
12 | + # Build base url with account URL, and username/password basic auth | ||
13 | + base_url = tracker.account.gsub 'http://', "http://#{tracker.username}:#{tracker.password}@" | ||
14 | + | ||
15 | + stub_request(:post, "#{base_url}/issues.xml"). | ||
12 | to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) | 16 | to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body ) |
13 | 17 | ||
14 | problem.app.issue_tracker.create_issue(problem) | 18 | problem.app.issue_tracker.create_issue(problem) |
15 | problem.reload | 19 | problem.reload |
16 | 20 | ||
17 | - requested = have_requested(:post, "#{tracker.account}/issues.xml") | 21 | + requested = have_requested(:post, "#{base_url}/issues.xml") |
18 | WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) | 22 | WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token}) |
19 | WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) | 23 | WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/) |
20 | WebMock.should requested.with(:body => /<subject>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/subject>/) | 24 | WebMock.should requested.with(:body => /<subject>\[#{ problem.environment }\]\[#{problem.where}\] #{problem.message.to_s.truncate(100)}<\/subject>/) |
@@ -26,9 +30,9 @@ describe IssueTrackers::RedmineTracker do | @@ -26,9 +30,9 @@ describe IssueTrackers::RedmineTracker do | ||
26 | it "should generate a url where a file with line number can be viewed" do | 30 | it "should generate a url where a file with line number can be viewed" do |
27 | t = Fabricate(:redmine_tracker, :account => 'http://redmine.example.com', :project_id => "errbit") | 31 | t = Fabricate(:redmine_tracker, :account => 'http://redmine.example.com', :project_id => "errbit") |
28 | t.url_to_file("/example/file").should == | 32 | t.url_to_file("/example/file").should == |
29 | - 'http://redmine.example.com/projects/errbit/repository/annotate/example/file' | 33 | + 'http://redmine.example.com/projects/errbit/repository/revisions/master/changes/example/file' |
30 | t.url_to_file("/example/file", 25).should == | 34 | t.url_to_file("/example/file", 25).should == |
31 | - 'http://redmine.example.com/projects/errbit/repository/annotate/example/file#L25' | 35 | + 'http://redmine.example.com/projects/errbit/repository/revisions/master/changes/example/file#L25' |
32 | end | 36 | end |
33 | 37 | ||
34 | it "should use the alt_project_id to generate a file/linenumber url, if given" do | 38 | it "should use the alt_project_id to generate a file/linenumber url, if given" do |
@@ -36,7 +40,6 @@ describe IssueTrackers::RedmineTracker do | @@ -36,7 +40,6 @@ describe IssueTrackers::RedmineTracker do | ||
36 | :project_id => "errbit", | 40 | :project_id => "errbit", |
37 | :alt_project_id => "actual_project") | 41 | :alt_project_id => "actual_project") |
38 | t.url_to_file("/example/file", 25).should == | 42 | t.url_to_file("/example/file", 25).should == |
39 | - 'http://redmine.example.com/projects/actual_project/repository/annotate/example/file#L25' | 43 | + 'http://redmine.example.com/projects/actual_project/repository/revisions/master/changes/example/file#L25' |
40 | end | 44 | end |
41 | end | 45 | end |
42 | - |
spec/models/notice_spec.rb
@@ -51,7 +51,7 @@ describe Notice do | @@ -51,7 +51,7 @@ describe Notice do | ||
51 | describe "user agent string" do | 51 | describe "user agent string" do |
52 | it "should be parsed and human-readable" do | 52 | it "should be parsed and human-readable" do |
53 | notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) | 53 | notice = Fabricate.build(:notice, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) |
54 | - notice.user_agent_string.should == 'Chrome 10.0.648.204' | 54 | + notice.user_agent_string.should == 'Chrome 10.0.648.204 (Intel Mac OS X 10_6_7)' |
55 | end | 55 | end |
56 | 56 | ||
57 | it "should be nil if HTTP_USER_AGENT is blank" do | 57 | it "should be nil if HTTP_USER_AGENT is blank" do |
spec/models/notification_service/hubot_service_spec.rb
@@ -8,7 +8,7 @@ describe NotificationService::HubotService do | @@ -8,7 +8,7 @@ describe NotificationService::HubotService do | ||
8 | problem = notice.problem | 8 | problem = notice.problem |
9 | 9 | ||
10 | # faraday stubbing | 10 | # faraday stubbing |
11 | - HTTParty.should_receive(:post).with(notification_service.api_token, :body => {:message => '[production][foo#bar] FooError: Too Much Bar', :room => notification_service.room_id}).and_return(true) | 11 | + HTTParty.should_receive(:post).with(notification_service.api_token, :body => {:message => an_instance_of(String), :room => notification_service.room_id}).and_return(true) |
12 | 12 | ||
13 | notification_service.create_notification(problem) | 13 | notification_service.create_notification(problem) |
14 | end | 14 | end |
spec/models/notification_service/webhook_service_spec.rb
0 → 100644
@@ -0,0 +1,13 @@ | @@ -0,0 +1,13 @@ | ||
1 | +require 'spec_helper' | ||
2 | + | ||
3 | +describe NotificationService::WebhookService do | ||
4 | + it "it should send a notification to a user-specified URL" do | ||
5 | + notice = Fabricate :notice | ||
6 | + notification_service = Fabricate :webhook_notification_service, :app => notice.app | ||
7 | + problem = notice.problem | ||
8 | + | ||
9 | + HTTParty.should_receive(:post).with(notification_service.api_token, :body => {:problem => problem.to_json}).and_return(true) | ||
10 | + | ||
11 | + notification_service.create_notification(problem) | ||
12 | + end | ||
13 | +end |
spec/models/problem_spec.rb
@@ -339,7 +339,7 @@ describe Problem do | @@ -339,7 +339,7 @@ describe Problem do | ||
339 | it "adding a notice adds a string to #user_agents" do | 339 | it "adding a notice adds a string to #user_agents" do |
340 | lambda { | 340 | lambda { |
341 | Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) | 341 | Fabricate(:notice, :err => @err, :request => {'cgi-data' => {'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16'}}) |
342 | - }.should change(@problem, :user_agents).from({}).to({Digest::MD5.hexdigest('Chrome 10.0.648.204') => {'value' => 'Chrome 10.0.648.204', 'count' => 1}}) | 342 | + }.should change(@problem, :user_agents).from({}).to({Digest::MD5.hexdigest('Chrome 10.0.648.204 (Intel Mac OS X 10_6_7)') => {'value' => 'Chrome 10.0.648.204 (Intel Mac OS X 10_6_7)', 'count' => 1}}) |
343 | end | 343 | end |
344 | 344 | ||
345 | it "removing a notice removes string from #user_agents" do | 345 | it "removing a notice removes string from #user_agents" do |
@@ -347,7 +347,7 @@ describe Problem do | @@ -347,7 +347,7 @@ describe Problem do | ||
347 | lambda { | 347 | lambda { |
348 | @err.notices.first.destroy | 348 | @err.notices.first.destroy |
349 | @problem.reload | 349 | @problem.reload |
350 | - }.should change(@problem, :user_agents).from({Digest::MD5.hexdigest('Chrome 10.0.648.204') => {'value' => 'Chrome 10.0.648.204', 'count' => 1}}).to({}) | 350 | + }.should change(@problem, :user_agents).from({Digest::MD5.hexdigest('Chrome 10.0.648.204 (Intel Mac OS X 10_6_7)') => {'value' => 'Chrome 10.0.648.204 (Intel Mac OS X 10_6_7)', 'count' => 1}}).to({}) |
351 | end | 351 | end |
352 | end | 352 | end |
353 | 353 |
spec/models/user_spec.rb
@@ -29,6 +29,15 @@ describe User do | @@ -29,6 +29,15 @@ describe User do | ||
29 | user2.should_not be_valid | 29 | user2.should_not be_valid |
30 | user2.errors[:github_login].should include("is already taken") | 30 | user2.errors[:github_login].should include("is already taken") |
31 | end | 31 | end |
32 | + | ||
33 | + it 'allows blank / null github_login' do | ||
34 | + user1 = Fabricate(:user, :github_login => ' ') | ||
35 | + user1.should be_valid | ||
36 | + | ||
37 | + user2 = Fabricate.build(:user, :github_login => ' ') | ||
38 | + user2.save | ||
39 | + user2.should be_valid | ||
40 | + end | ||
32 | end | 41 | end |
33 | 42 | ||
34 | context 'Watchers' do | 43 | context 'Watchers' do |
spec/spec_helper.rb
@@ -28,6 +28,12 @@ RSpec.configure do |config| | @@ -28,6 +28,12 @@ RSpec.configure do |config| | ||
28 | DatabaseCleaner.clean | 28 | DatabaseCleaner.clean |
29 | end | 29 | end |
30 | config.include WebMock::API | 30 | config.include WebMock::API |
31 | + | ||
32 | + config.include Haml, :type => :helper | ||
33 | + config.include Haml::Helpers, :type => :helper | ||
34 | + config.before(:each, :type => :helper) do |config| | ||
35 | + init_haml_helpers | ||
36 | + end | ||
31 | end | 37 | end |
32 | 38 | ||
33 | OmniAuth.config.test_mode = true | 39 | OmniAuth.config.test_mode = true |