Commit 5aa64429386b4698930f9d70d04332a2ff367ef8

Authored by Andrey Subbota
2 parents 9e505cfd 60471bee
Exists in master and in 1 other branch production

Merge upstream

Showing 163 changed files with 3043 additions and 1571 deletions   Show diff stats
.travis.yml
... ... @@ -3,6 +3,7 @@ rvm:
3 3 - 1.9.3
4 4 - 1.9.2
5 5 - 1.8.7
  6 +services: mongodb
6 7  
7 8 # To stop Travis from running tests for a new commit,
8 9 # add the following to your commit message: [ci skip]
... ...
Gemfile
1 1 source 'http://rubygems.org'
2 2  
3   -gem 'rails', '3.2.6'
4   -
5   -gem 'nokogiri'
  3 +gem 'rails', '3.2.8'
6 4 gem 'mongoid', '~> 2.4.10'
7   -
  5 +gem 'mongoid_rails_migrations'
  6 +gem 'devise', '~> 1.5.3'
  7 +gem 'nokogiri'
8 8 gem 'haml'
9 9 gem 'htmlentities', "~> 4.3.0"
  10 +gem 'rack-ssl', :require => 'rack/ssl' # force SSL
10 11  
11   -gem 'devise', '~> 1.5.3'
  12 +gem 'useragent', '~> 0.3.1'
  13 +gem 'inherited_resources'
  14 +gem 'SystemTimer', :platform => :ruby_18
  15 +gem 'actionmailer_inline_css', "~> 1.3.0"
  16 +gem 'kaminari'
  17 +gem 'rack-ssl-enforcer'
  18 +gem 'fabrication', "~> 1.3.0" # Used for both tests and demo data
  19 +gem 'rails_autolink', '~> 1.0.9'
  20 +# Please don't update hoptoad_notifier to airbrake.
  21 +# It's for internal use only, and we monkeypatch certain methods
  22 +gem 'hoptoad_notifier', "~> 2.4"
12 23  
13   -gem 'omniauth-github'
14   -gem 'oa-core'
15 24  
  25 +# Remove / comment out any of the gems below if you want to disable
  26 +# a given issue tracker, notification service, or authentication.
  27 +
  28 +# Issue Trackers
  29 +# ---------------------------------------
  30 +# Lighthouse
16 31 gem 'lighthouse-api'
  32 +# Redmine
17 33 gem 'oruen_redmine_client', :require => 'redmine_client'
18   -gem 'mongoid_rails_migrations'
19   -gem 'useragent', '~> 0.3.1'
  34 +# Pivotal Tracker
20 35 gem 'pivotal-tracker'
  36 +# Fogbugz
21 37 gem 'ruby-fogbugz', :require => 'fogbugz'
22   -
  38 +# Github Issues
23 39 gem 'octokit', '~> 1.0.0'
24 40  
25   -gem 'inherited_resources'
26   -gem 'SystemTimer', :platform => :ruby_18
27   -gem 'hoptoad_notifier', "~> 2.4"
28   -gem 'actionmailer_inline_css', "~> 1.3.0"
29   -gem 'kaminari'
30   -gem 'rack-ssl-enforcer'
31   -gem 'fabrication', "~> 1.3.0" # Both for tests, and loading demo data
32   -gem 'rails_autolink', '~> 1.0.9'
  41 +# Bitbucket Issues
  42 +gem 'bitbucket_rest_api'
  43 +
  44 +# Notification services
  45 +# ---------------------------------------
  46 +# Campfire
  47 +gem 'campy'
  48 +# Hipchat
  49 +gem 'hipchat'
  50 +# Google Talk
  51 +gem 'xmpp4r'
  52 +# Hoiio (SMS)
  53 +gem 'hoi'
  54 +# Pushover (iOS Push notifications)
  55 +gem 'rushover'
  56 +
  57 +# Authentication
  58 +# ---------------------------------------
  59 +# GitHub OAuth
  60 +gem 'omniauth-github'
  61 +
33 62  
34 63 platform :ruby do
35   - gem 'mongo', '= 1.3.1'
36   - gem 'bson', '= 1.3.1'
37   - gem 'bson_ext', '= 1.3.1'
  64 + gem 'mongo', '= 1.6.2'
  65 + gem 'bson', '= 1.6.2'
  66 + gem 'bson_ext', '= 1.6.2'
38 67 end
39 68  
  69 +gem 'omniauth'
  70 +gem 'oa-core'
40 71 gem 'ri_cal'
41   -gem 'yajl-ruby'
  72 +gem 'yajl-ruby', :require => "yajl"
42 73  
43 74 group :development, :test do
44 75 gem 'rspec-rails', '~> 2.6'
... ... @@ -46,8 +77,13 @@ group :development, :test do
46 77 unless ENV["CI"]
47 78 gem 'ruby-debug', :platform => :mri_18
48 79 gem 'debugger', :platform => :mri_19
  80 + gem 'pry'
  81 + gem 'pry-rails'
49 82 end
50   - # gem 'rpm_contrib', :git => "git://github.com/bensymonds/rpm_contrib.git", :branch => "mongo-1.4.0_update"
  83 +# gem 'rpm_contrib'
  84 +# gem 'newrelic_rpm'
  85 + gem 'capistrano'
  86 + gem 'capistrano_colors'
51 87 end
52 88  
53 89 group :test do
... ... @@ -56,6 +92,7 @@ group :test do
56 92 gem 'rspec', '~> 2.6'
57 93 gem 'database_cleaner', '~> 0.6.0'
58 94 gem 'email_spec'
  95 + gem 'timecop'
59 96 end
60 97  
61 98 group :heroku do
... ... @@ -72,3 +109,5 @@ group :assets do
72 109 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
73 110 gem 'uglifier', '>= 1.0.3'
74 111 end
  112 +
  113 +gem 'turbo-sprockets-rails3'
... ...
Gemfile.lock
... ... @@ -2,43 +2,60 @@ GEM
2 2 remote: http://rubygems.org/
3 3 specs:
4 4 SystemTimer (1.2.3)
5   - actionmailer (3.2.6)
6   - actionpack (= 3.2.6)
  5 + actionmailer (3.2.8)
  6 + actionpack (= 3.2.8)
7 7 mail (~> 2.4.4)
8 8 actionmailer_inline_css (1.3.1)
9 9 actionmailer (>= 3.0.0)
10 10 nokogiri (>= 1.4.4)
11 11 premailer (>= 1.7.1)
12   - actionpack (3.2.6)
13   - activemodel (= 3.2.6)
14   - activesupport (= 3.2.6)
  12 + actionpack (3.2.8)
  13 + activemodel (= 3.2.8)
  14 + activesupport (= 3.2.8)
15 15 builder (~> 3.0.0)
16 16 erubis (~> 2.7.0)
17   - journey (~> 1.0.1)
  17 + journey (~> 1.0.4)
18 18 rack (~> 1.4.0)
19 19 rack-cache (~> 1.2)
20 20 rack-test (~> 0.6.1)
21 21 sprockets (~> 2.1.3)
22   - activemodel (3.2.6)
23   - activesupport (= 3.2.6)
  22 + activemodel (3.2.8)
  23 + activesupport (= 3.2.8)
24 24 builder (~> 3.0.0)
25   - activerecord (3.2.6)
26   - activemodel (= 3.2.6)
27   - activesupport (= 3.2.6)
  25 + activerecord (3.2.8)
  26 + activemodel (= 3.2.8)
  27 + activesupport (= 3.2.8)
28 28 arel (~> 3.0.2)
29 29 tzinfo (~> 0.3.29)
30   - activeresource (3.2.6)
31   - activemodel (= 3.2.6)
32   - activesupport (= 3.2.6)
33   - activesupport (3.2.6)
  30 + activeresource (3.2.8)
  31 + activemodel (= 3.2.8)
  32 + activesupport (= 3.2.8)
  33 + activesupport (3.2.8)
34 34 i18n (~> 0.6)
35 35 multi_json (~> 1.0)
36   - addressable (2.2.8)
  36 + addressable (2.3.2)
37 37 arel (3.0.2)
38 38 bcrypt-ruby (3.0.1)
39   - bson (1.3.1)
40   - bson_ext (1.3.1)
41   - builder (3.0.0)
  39 + bitbucket_rest_api (0.1.1)
  40 + faraday (~> 0.8.1)
  41 + faraday_middleware (~> 0.8.1)
  42 + hashie (~> 1.2.0)
  43 + multi_json (~> 1.3)
  44 + nokogiri (~> 1.5.2)
  45 + simple_oauth
  46 + bson (1.6.2)
  47 + bson_ext (1.6.2)
  48 + bson (~> 1.6.2)
  49 + builder (3.0.4)
  50 + campy (0.1.3)
  51 + multi_json (~> 1.0)
  52 + capistrano (2.13.4)
  53 + highline
  54 + net-scp (>= 1.0.0)
  55 + net-sftp (>= 2.0.0)
  56 + net-ssh (>= 2.0.14)
  57 + net-ssh-gateway (>= 1.1.0)
  58 + capistrano_colors (0.5.5)
42 59 capybara (1.1.2)
43 60 mime-types (>= 1.16)
44 61 nokogiri (>= 1.3.3)
... ... @@ -46,8 +63,9 @@ GEM
46 63 rack-test (>= 0.5.4)
47 64 selenium-webdriver (~> 2.0)
48 65 xpath (~> 0.1.4)
49   - childprocess (0.3.2)
50   - ffi (~> 1.0.6)
  66 + childprocess (0.3.5)
  67 + ffi (~> 1.0, >= 1.0.6)
  68 + coderay (1.0.6)
51 69 columnize (0.3.6)
52 70 crack (0.3.1)
53 71 css_parser (1.2.6)
... ... @@ -55,13 +73,13 @@ GEM
55 73 rdoc
56 74 daemons (1.1.8)
57 75 database_cleaner (0.6.7)
58   - debugger (1.1.3)
  76 + debugger (1.2.0)
59 77 columnize (>= 0.3.1)
60 78 debugger-linecache (~> 1.1.1)
61   - debugger-ruby_core_source (~> 1.1.2)
62   - debugger-linecache (1.1.1)
  79 + debugger-ruby_core_source (~> 1.1.3)
  80 + debugger-linecache (1.1.2)
63 81 debugger-ruby_core_source (>= 1.1.1)
64   - debugger-ruby_core_source (1.1.2)
  82 + debugger-ruby_core_source (1.1.3)
65 83 devise (1.5.3)
66 84 bcrypt-ruby (~> 3.0)
67 85 orm_adapter (~> 0.0.3)
... ... @@ -75,38 +93,49 @@ GEM
75 93 execjs (1.4.0)
76 94 multi_json (~> 1.0)
77 95 fabrication (1.3.2)
78   - faraday (0.8.1)
  96 + faraday (0.8.4)
79 97 multipart-post (~> 1.1)
80   - faraday_middleware (0.8.7)
  98 + faraday_middleware (0.8.8)
81 99 faraday (>= 0.7.4, < 0.9)
82   - ffi (1.0.11)
83   - haml (3.1.5)
  100 + ffi (1.1.4)
  101 + haml (3.1.6)
84 102 happymapper (0.4.0)
85 103 libxml-ruby (~> 2.0)
86 104 has_scope (0.5.1)
87 105 hashie (1.2.0)
  106 + highline (1.6.15)
88 107 hike (1.2.1)
  108 + hipchat (0.4.1)
  109 + httparty
  110 + hoi (0.0.6)
  111 + httparty (> 0.6.0)
  112 + json (> 1.4.0)
89 113 hoptoad_notifier (2.4.11)
90 114 activesupport
91 115 builder
92 116 htmlentities (4.3.1)
93   - i18n (0.6.0)
  117 + httparty (0.9.0)
  118 + multi_json (~> 1.0)
  119 + multi_xml
  120 + httpauth (0.1)
  121 + i18n (0.6.1)
94 122 inherited_resources (1.3.1)
95 123 has_scope (~> 0.5.0)
96 124 responders (~> 0.6)
97 125 journey (1.0.4)
98   - json (1.7.3)
99   - kaminari (0.13.0)
  126 + json (1.7.5)
  127 + jwt (0.1.5)
  128 + multi_json (>= 1.0)
  129 + kaminari (0.14.1)
100 130 actionpack (>= 3.0.0)
101 131 activesupport (>= 3.0.0)
102   - railties (>= 3.0.0)
103 132 kgio (2.7.4)
104   - launchy (2.1.0)
105   - addressable (~> 2.2.6)
  133 + launchy (2.1.2)
  134 + addressable (~> 2.3)
106 135 libv8 (3.3.10.4)
107   - libwebsocket (0.1.3)
  136 + libwebsocket (0.1.5)
108 137 addressable
109   - libxml-ruby (2.3.2)
  138 + libxml-ruby (2.3.3)
110 139 lighthouse-api (2.0)
111 140 activeresource (>= 3.0.0)
112 141 activesupport (>= 3.0.0)
... ... @@ -116,9 +145,10 @@ GEM
116 145 i18n (>= 0.4.0)
117 146 mime-types (~> 1.16)
118 147 treetop (~> 1.4.8)
119   - mime-types (1.18)
120   - mongo (1.3.1)
121   - bson (>= 1.3.1)
  148 + method_source (0.7.1)
  149 + mime-types (1.19)
  150 + mongo (1.6.2)
  151 + bson (~> 1.6.2)
122 152 mongoid (2.4.10)
123 153 activemodel (~> 3.1)
124 154 mongo (~> 1.3)
... ... @@ -129,26 +159,37 @@ GEM
129 159 rails (>= 3.0.0)
130 160 railties (>= 3.0.0)
131 161 multi_json (1.3.6)
  162 + multi_xml (0.5.1)
132 163 multipart-post (1.1.5)
133   - nokogiri (1.5.0)
  164 + net-scp (1.0.4)
  165 + net-ssh (>= 1.99.1)
  166 + net-sftp (2.0.5)
  167 + net-ssh (>= 2.0.9)
  168 + net-ssh (2.6.1)
  169 + net-ssh-gateway (1.1.0)
  170 + net-ssh (>= 1.99.1)
  171 + nokogiri (1.5.5)
134 172 oa-core (0.3.2)
135   - oauth2 (0.5.2)
136   - faraday (~> 0.7)
  173 + oauth2 (0.8.0)
  174 + faraday (~> 0.8)
  175 + httpauth (~> 0.1)
  176 + jwt (~> 0.1.4)
137 177 multi_json (~> 1.0)
  178 + rack (~> 1.2)
138 179 octokit (1.0.7)
139 180 addressable (~> 2.2)
140 181 faraday (~> 0.8)
141 182 faraday_middleware (~> 0.8)
142 183 hashie (~> 1.2)
143 184 multi_json (~> 1.3)
144   - omniauth (1.0.3)
  185 + omniauth (1.1.1)
145 186 hashie (~> 1.2)
146 187 rack
147   - omniauth-github (1.0.1)
  188 + omniauth-github (1.0.2)
148 189 omniauth (~> 1.0)
149   - omniauth-oauth2 (~> 1.0)
150   - omniauth-oauth2 (1.0.0)
151   - oauth2 (~> 0.5.0)
  190 + omniauth-oauth2 (~> 1.1)
  191 + omniauth-oauth2 (1.1.1)
  192 + oauth2 (~> 0.8.0)
152 193 omniauth (~> 1.0)
153 194 orm_adapter (0.0.7)
154 195 oruen_redmine_client (0.0.1)
... ... @@ -166,54 +207,60 @@ GEM
166 207 premailer (1.7.3)
167 208 css_parser (>= 1.1.9)
168 209 htmlentities (>= 4.0.0)
  210 + pry (0.9.9.6)
  211 + coderay (~> 1.0.5)
  212 + method_source (~> 0.7.1)
  213 + slop (>= 2.4.4, < 3)
  214 + pry-rails (0.2.0)
  215 + pry
169 216 rack (1.4.1)
170 217 rack-cache (1.2)
171 218 rack (>= 0.4)
172 219 rack-ssl (1.3.2)
173 220 rack
174 221 rack-ssl-enforcer (0.2.4)
175   - rack-test (0.6.1)
  222 + rack-test (0.6.2)
176 223 rack (>= 1.0)
177   - rails (3.2.6)
178   - actionmailer (= 3.2.6)
179   - actionpack (= 3.2.6)
180   - activerecord (= 3.2.6)
181   - activeresource (= 3.2.6)
182   - activesupport (= 3.2.6)
  224 + rails (3.2.8)
  225 + actionmailer (= 3.2.8)
  226 + actionpack (= 3.2.8)
  227 + activerecord (= 3.2.8)
  228 + activeresource (= 3.2.8)
  229 + activesupport (= 3.2.8)
183 230 bundler (~> 1.0)
184   - railties (= 3.2.6)
  231 + railties (= 3.2.8)
185 232 rails_autolink (1.0.9)
186 233 rails (~> 3.1)
187   - railties (3.2.6)
188   - actionpack (= 3.2.6)
189   - activesupport (= 3.2.6)
  234 + railties (3.2.8)
  235 + actionpack (= 3.2.8)
  236 + activesupport (= 3.2.8)
190 237 rack-ssl (~> 1.3.2)
191 238 rake (>= 0.8.7)
192 239 rdoc (~> 3.4)
193 240 thor (>= 0.14.6, < 2.0)
194   - raindrops (0.8.0)
  241 + raindrops (0.10.0)
195 242 rake (0.9.2.2)
196 243 rbx-require-relative (0.0.9)
197 244 rdoc (3.12)
198 245 json (~> 1.4)
199   - responders (0.9.1)
  246 + responders (0.9.2)
200 247 railties (~> 3.1)
201 248 rest-client (1.6.7)
202 249 mime-types (>= 1.16)
203 250 ri_cal (0.8.8)
204   - rspec (2.10.0)
205   - rspec-core (~> 2.10.0)
206   - rspec-expectations (~> 2.10.0)
207   - rspec-mocks (~> 2.10.0)
208   - rspec-core (2.10.0)
209   - rspec-expectations (2.10.0)
  251 + rspec (2.11.0)
  252 + rspec-core (~> 2.11.0)
  253 + rspec-expectations (~> 2.11.0)
  254 + rspec-mocks (~> 2.11.0)
  255 + rspec-core (2.11.1)
  256 + rspec-expectations (2.11.2)
210 257 diff-lcs (~> 1.1.3)
211   - rspec-mocks (2.10.1)
212   - rspec-rails (2.10.1)
  258 + rspec-mocks (2.11.1)
  259 + rspec-rails (2.11.0)
213 260 actionpack (>= 3.0)
214 261 activesupport (>= 3.0)
215 262 railties (>= 3.0)
216   - rspec (~> 2.10.0)
  263 + rspec (~> 2.11.0)
217 264 ruby-debug (0.10.4)
218 265 columnize (>= 0.1)
219 266 ruby-debug-base (~> 0.10.4.0)
... ... @@ -221,42 +268,51 @@ GEM
221 268 linecache (>= 0.3)
222 269 ruby-fogbugz (0.1.1)
223 270 crack
224   - rubyzip (0.9.8)
225   - selenium-webdriver (2.21.2)
  271 + rubyzip (0.9.9)
  272 + rushover (0.1.1)
  273 + json
  274 + rest-client
  275 + selenium-webdriver (2.25.0)
226 276 childprocess (>= 0.2.5)
227   - ffi (~> 1.0)
228 277 libwebsocket (~> 0.1.3)
229 278 multi_json (~> 1.0)
230 279 rubyzip
  280 + simple_oauth (0.1.9)
  281 + slop (2.4.4)
231 282 sprockets (2.1.3)
232 283 hike (~> 1.2)
233 284 rack (~> 1.0)
234 285 tilt (~> 1.1, != 1.3.0)
235   - therubyracer (0.10.1)
  286 + therubyracer (0.10.2)
236 287 libv8 (~> 3.3.10)
237   - thin (1.3.1)
  288 + thin (1.4.1)
238 289 daemons (>= 1.0.9)
239 290 eventmachine (>= 0.12.6)
240 291 rack (>= 1.0.0)
241   - thor (0.15.2)
  292 + thor (0.16.0)
242 293 tilt (1.3.3)
  294 + timecop (0.3.5)
243 295 treetop (1.4.10)
244 296 polyglot
245 297 polyglot (>= 0.3.1)
  298 + turbo-sprockets-rails3 (0.1.10)
  299 + railties (>= 3.1.0)
  300 + sprockets (>= 2.0.0)
246 301 tzinfo (0.3.33)
247   - uglifier (1.2.4)
  302 + uglifier (1.2.7)
248 303 execjs (>= 0.3.0)
249   - multi_json (>= 1.0.2)
  304 + multi_json (~> 1.3)
250 305 unicorn (4.3.1)
251 306 kgio (~> 2.6)
252 307 rack
253 308 raindrops (~> 0.7)
254 309 useragent (0.3.2)
255   - warden (1.2.0)
  310 + warden (1.2.1)
256 311 rack (>= 1.0)
257   - webmock (1.8.6)
  312 + webmock (1.8.7)
258 313 addressable (>= 2.2.7)
259 314 crack (>= 0.1.7)
  315 + xmpp4r (0.5)
260 316 xpath (0.1.4)
261 317 nokogiri (~> 1.3)
262 318 yajl-ruby (1.1.0)
... ... @@ -267,8 +323,12 @@ PLATFORMS
267 323 DEPENDENCIES
268 324 SystemTimer
269 325 actionmailer_inline_css (~> 1.3.0)
270   - bson (= 1.3.1)
271   - bson_ext (= 1.3.1)
  326 + bitbucket_rest_api
  327 + bson (= 1.6.2)
  328 + bson_ext (= 1.6.2)
  329 + campy
  330 + capistrano
  331 + capistrano_colors
272 332 capybara
273 333 database_cleaner (~> 0.6.0)
274 334 debugger
... ... @@ -277,33 +337,43 @@ DEPENDENCIES
277 337 execjs
278 338 fabrication (~> 1.3.0)
279 339 haml
  340 + hipchat
  341 + hoi
280 342 hoptoad_notifier (~> 2.4)
281 343 htmlentities (~> 4.3.0)
282 344 inherited_resources
283 345 kaminari
284 346 launchy
285 347 lighthouse-api
286   - mongo (= 1.3.1)
  348 + mongo (= 1.6.2)
287 349 mongoid (~> 2.4.10)
288 350 mongoid_rails_migrations
289 351 nokogiri
290 352 oa-core
291 353 octokit (~> 1.0.0)
  354 + omniauth
292 355 omniauth-github
293 356 oruen_redmine_client
294 357 pivotal-tracker
  358 + pry
  359 + pry-rails
  360 + rack-ssl
295 361 rack-ssl-enforcer
296   - rails (= 3.2.6)
  362 + rails (= 3.2.8)
297 363 rails_autolink (~> 1.0.9)
298 364 ri_cal
299 365 rspec (~> 2.6)
300 366 rspec-rails (~> 2.6)
301 367 ruby-debug
302 368 ruby-fogbugz
  369 + rushover
303 370 therubyracer
304 371 thin
  372 + timecop
  373 + turbo-sprockets-rails3
305 374 uglifier (>= 1.0.3)
306 375 unicorn
307 376 useragent (~> 0.3.1)
308 377 webmock
  378 + xmpp4r
309 379 yajl-ruby
... ...
README.md
1   -# Errbit [![TravisCI][travis-img-url]][travis-ci-url]
  1 +# Errbit [![TravisCI][travis-img-url]][travis-ci-url] [![Code Climate][codeclimate-img-url]][codeclimate-url]
2 2  
3 3 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master
4 4 [travis-ci-url]: http://travis-ci.org/errbit/errbit
  5 +[codeclimate-img-url]: https://codeclimate.com/badge.png
  6 +[codeclimate-url]: https://codeclimate.com/github/errbit/errbit
  7 +
5 8  
6 9 ### The open source, self-hosted error catcher
7 10  
... ... @@ -57,12 +60,17 @@ If this doesn&#39;t sound like you, you should probably stick with [Airbrake](http:/
57 60 The [Thoughtbot](http://thoughtbot.com) guys offer great support for it and it is much more worry-free.
58 61 They have a free package and even offer a *"Airbrake behind your firewall"* solution.
59 62  
  63 +Mailing List
  64 +------------
  65 +
  66 +Join the Google Group at https://groups.google.com/group/errbit to receive updates and notifications.
  67 +
60 68 Demo
61 69 ----
62 70  
63 71 There is a demo available at [http://errbit-demo.herokuapp.com/](http://errbit-demo.herokuapp.com/)
64 72  
65   -Email: demo@errbit-demo.herokuapp.com
  73 +Email: demo@errbit-demo.herokuapp.com<br/>
66 74 Password: password
67 75  
68 76 Installation
... ... @@ -145,9 +153,6 @@ git clone http://github.com/errbit/errbit.git
145 153 gem install heroku
146 154 heroku create example-errbit --stack cedar
147 155 heroku addons:add mongolab:starter
148   -cp -f config/mongoid.mongolab.yml config/mongoid.yml
149   -git add -f config/mongoid.yml
150   -git commit -m "Added mongoid config for Mongolab"
151 156 heroku addons:add sendgrid:starter
152 157 heroku config:add HEROKU=true
153 158 heroku config:add ERRBIT_HOST=some-hostname.example.com
... ... @@ -168,7 +173,7 @@ heroku run rake db:seed
168 173 ```bash
169 174 # Install the heroku scheduler add-on
170 175 heroku addons:add scheduler:standard
171   -
  176 +
172 177 # Go open the dashboard to schedule the job. You should use
173 178 # 'rake errbit:db:clear_resolved' as the task command, and schedule it
174 179 # at whatever frequency you like (once/day should work great).
... ... @@ -185,7 +190,7 @@ heroku run rake db:seed
185 190 * Or clear resolved errors manually:
186 191  
187 192 ```bash
188   - heroku rake errbit:db:clear_resolved
  193 + heroku run rake errbit:db:clear_resolved
189 194 ```
190 195  
191 196 * You may want to enable the deployment hook for heroku :
... ... @@ -275,6 +280,11 @@ GITHUB_ACCESS_SCOPE=repo,public_repo
275 280 * In `config/config.yml`, set `user_has_username` to `true`
276 281 * Follow the instructions at https://github.com/cschiewek/devise_ldap_authenticatable
277 282 to set up the devise_ldap_authenticatable gem.
  283 + * Ensure to set ```config.ldap_create_user = true``` in ```config/initializers/devise.rb```, this enables creating the users from LDAP, otherwhise login will not work.
  284 + * Create a new initializer (e.g. ```config/initializers/devise_ldap.rb```) and add the following code to enable ldap authentication in the User-model:
  285 +```ruby
  286 +Errbit::Config.devise_modules << :ldap_authenticatable
  287 +```
278 288  
279 289 * If you are authenticating by `username`, you will need to set the user's email manually
280 290 before authentication. You must add the following lines to `app/models/user.rb`:
... ... @@ -286,6 +296,15 @@ GITHUB_ACCESS_SCOPE=repo,public_repo
286 296 end
287 297 ```
288 298  
  299 + * Now login with your user from LDAP, this will create a user in the database
  300 + * Open a rails console and set the admin flag for your user:
  301 +
  302 +```ruby
  303 +user = User.first
  304 +user.admin = true
  305 +user.save!
  306 +```
  307 +
289 308 Upgrading
290 309 ---------
291 310 When upgrading Errbit, please run:
... ... @@ -357,6 +376,12 @@ card_type = Defect, status = Open, priority = Essential
357 376 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit**
358 377 * You will also need to provide your username and password for your GitHub account.
359 378 * (We'd really appreciate it if you wanted to help us implement OAuth instead!)
  379 +
  380 +**Bitbucket Issues Integration**
  381 +
  382 +* For 'BITBUCKET REPO' field, the account will either be a username or organization. i.e. **errbit/errbit**
  383 +* You will also need to provide your username and password for your Bitbucket account.
  384 +
360 385  
361 386  
362 387 What if Errbit has an error?
... ... @@ -384,6 +409,23 @@ or you can set up the GitHub Issues tracker for your **Self.Errbit** app:
384 409 * You can now easily post bug reports to GitHub Issues by clicking the **Create Issue** button on a **Self.Errbit** error.
385 410  
386 411  
  412 +Use Errbit with applications written in other languages
  413 +-------------------------------------------------------
  414 +
  415 +In theory, any Airbrake-compatible error catcher for other languages should work with Errbit.
  416 +Solutions known to work are listed below:
  417 +
  418 +<table>
  419 + <tr>
  420 + <th>PHP (&gt;= 5.3)</th>
  421 + <td>https://github.com/flippa/errbit-php</td>
  422 + </tr>
  423 + <tr>
  424 + <th>Python</th>
  425 + <td>https://github.com/mkorenkov/errbit.py , https://github.com/pulseenergy/airbrakepy</td>
  426 + </tr>
  427 +</table>
  428 +
387 429 TODO
388 430 ----
389 431  
... ...
app/assets/images/bitbucket_create.png 0 → 100644

2.54 KB

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

3.61 KB

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

1.95 KB

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

3.19 KB

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

3.19 KB

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

2.8 KB

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

4.62 KB

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

4.62 KB

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

4.07 KB

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

2.05 KB

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

2.05 KB

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

1.16 KB

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

1.72 KB

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

1.72 KB

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

874 Bytes

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

1.91 KB

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

1.91 KB

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

1.02 KB

app/assets/javascripts/errbit.js
... ... @@ -130,5 +130,10 @@ $(function() {
130 130 // Hide external backtrace on page load
131 131 hide_external_backtrace();
132 132  
  133 + $('.head a.show_tail').click(function(e) {
  134 + $(this).hide().closest('.head_and_tail').find('.tail').show();
  135 + e.preventDefault();
  136 + });
  137 +
133 138 init();
134 139 });
... ...
app/assets/javascripts/form.js
... ... @@ -8,6 +8,9 @@ $(function(){
8 8 if($('div.issue_tracker.nested').length)
9 9 activateTypeSelector('issue_tracker', 'tracker_params');
10 10  
  11 + if($('div.notification_service.nested').length)
  12 + activateTypeSelector('notification_service', 'notification_params');
  13 +
11 14 $('body').addClass('has-js');
12 15 $('.label_radio').click(function(){
13 16 activateLabelIcons();
... ...
app/assets/stylesheets/application.css.erb
... ... @@ -3,6 +3,7 @@
3 3 *= require jquery.alerts
4 4 *= require errbit
5 5 *= require issue_tracker_icons
  6 + *= require notification_service_icons
6 7 *= require_self
7 8 */
8 9  
... ...
app/assets/stylesheets/errbit.css
... ... @@ -535,10 +535,11 @@ a.button.active {
535 535 display: inline-block;
536 536 }
537 537  
538   -/* Watchers and Issue Tracker Forms */
539   -div.watcher.nested .watcher_params, div.issue_tracker.nested .tracker_params {
  538 +/* Watchers / Issue Tracker / Notification Forms */
  539 +div.watcher.nested .watcher_params, div.issue_tracker.nested .tracker_params, div.notification_service.nested .notification_params {
540 540 display: none;
541 541 }
  542 +
542 543 div.nested .chosen {
543 544 display: block !important;
544 545 }
... ... @@ -546,35 +547,35 @@ div.nested .choose {
546 547 margin-bottom: 0.5em;
547 548 }
548 549  
549   -div.issue_tracker.nested .choose {
  550 +div.issue_tracker.nested .choose, div.notification_service.nested .choose {
550 551 background-color: #ebebeb;
551 552 border: 1px solid #dddddd;
552 553 margin: 0 0 15px;
553 554 padding: 12px;
554 555 }
555   -div.issue_tracker.nested img {
  556 +div.issue_tracker.nested img, div.notification_service.nested img {
556 557 vertical-align: middle;
557 558 }
558 559  
559 560 /* Icons for Issue Tracker Radio Buttons */
560   -div.issue_tracker.nested label.label_radio {
  561 +div.issue_tracker.nested label.label_radio, div.notification_service.nested label.label_radio {
561 562 color: #929292;
562 563 padding-left: 33px;
563 564 margin-bottom: 6px;
564 565 margin-right: 8px;
565 566 line-height: 30px;
566 567 }
567   -div.issue_tracker.nested .choose {
  568 +div.issue_tracker.nested .choose, div.notification_service.nested .choose {
568 569 padding-bottom: 6px;
569 570 }
570   -div.issue_tracker.nested label.label_radio:hover {
  571 +div.issue_tracker.nested label.label_radio:hover, div.notification_service.nested label.label_radio:hover {
571 572 color: #696969;
572 573 }
573   -div.issue_tracker.nested .label_radio input {
  574 +div.issue_tracker.nested .label_radio input, div.notification_service.nested .label_radio input {
574 575 position: absolute; left: -9999px;
575 576 }
576 577  
577   -div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover {
  578 +div.issue_tracker.nested label.r_on, div.issue_tracker.nested label.r_on:hover, div.notification_service.nested label.r_on, div.notification_service.nested label.r_on:hover {
578 579 color: #191919;
579 580 }
580 581  
... ... @@ -648,6 +649,13 @@ table.errs td.app .environment {
648 649 table.errs td.message a {
649 650 display: block;
650 651 word-wrap: break-word;
  652 + /* PjpG - configuration in WHAT & WHERE table's columns using ellipsis to avoid oversizing table's width */
  653 + width: 300px;
  654 + overflow: hidden;
  655 + text-overflow: ellipsis;
  656 + -o-text-overflow: ellipsis;
  657 + white-space: nowrap;
  658 + /* ------ */
651 659 }
652 660 table.errs td.message em {
653 661 color: #727272;
... ... @@ -834,15 +842,32 @@ table.comment tbody th {
834 842 height: 20px;
835 843 line-height: 0.5em;
836 844 }
  845 +table.comment th span, table.comment th img {
  846 + vertical-align: middle;
  847 +}
  848 +table.comment th span.comment-info {
  849 + line-height: 21px;
  850 + float: left;
  851 +}
  852 +table.comment img.gravatar {
  853 + margin-right: 7px;
  854 + float: left;
  855 +}
  856 +
837 857 table.comment tbody td {
838 858 background-color: #F9F9F9;
839 859 }
840 860 #content-comments a.destroy-comment {
841 861 color: #EE0000;
842 862 margin-right: 5px;
  863 + margin-top: 2px;
  864 + font-size: 21px;
  865 + line-height: 1;
  866 + float: right;
843 867 }
844 868 #content-comments a.destroy-comment:hover {
845 869 text-decoration: none;
  870 + color: #AA0000;
846 871 }
847 872 #content-comments #comment_submit {
848 873 margin-top: 15px;
... ... @@ -871,3 +896,12 @@ table.errs tr td.message .inline_comment em.commenter {
871 896  
872 897 .current.asc:after { content: ' ↑'; }
873 898 .current.desc:after { content: ' ↓'; }
  899 +
  900 +
  901 +table.users td {
  902 + vertical-align: middle;
  903 +}
  904 +table.users td img.gravatar {
  905 + vertical-align: middle;
  906 + margin-left: 3px;
  907 +}
... ...
app/assets/stylesheets/issue_tracker_icons.css.erb
1 1 /* Issue Tracker inactive, select, create and goto icons */
2 2 <% trackers = IssueTracker.subclasses.map{|t| t.label } << 'none' %>
  3 +
3 4 <% trackers.each do |tracker| %>
4 5 div.issue_tracker.nested label.<%= tracker %> {
5 6 background: url(/assets/<%= tracker %>_inactive.png) no-repeat;
... ... @@ -14,3 +15,4 @@ div.issue_tracker.nested label.r_on.&lt;%= tracker %&gt; {
14 15 background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat;
15 16 }
16 17 <% end %>
  18 +
... ...
app/assets/stylesheets/notification_service_icons.css.erb 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 + /* Notification Service inactive, select, create and goto icons */
  2 +<% notification_services = NotificationService.subclasses.map{|t| t.label } << 'none' %>
  3 +
  4 +<% notification_services.each do |notification_service| %>
  5 +div.notification_service.nested label.<%= notification_service %> {
  6 + background: url(/assets/<%= notification_service %>_inactive.png) no-repeat;
  7 +}
  8 +div.notification_service.nested label.r_on.<%= notification_service %> {
  9 + background: url(/assets/<%= notification_service %>_create.png) no-repeat;
  10 +}
  11 +#action-bar a.<%= notification_service %>_create {
  12 + background: transparent url(/assets/<%= notification_service %>_create.png) 6px 5px no-repeat;
  13 +}
  14 +#action-bar a.<%= notification_service %>_goto {
  15 + background: transparent url(/assets/<%= notification_service %>_goto.png) 6px 5px no-repeat;
  16 +}
  17 +<% end %>
  18 +
... ...
app/controllers/api/v1/notices_controller.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +class Api::V1::NoticesController < ApplicationController
  2 + respond_to :json, :xml
  3 +
  4 + def index
  5 + query = {}
  6 + fields = %w{created_at message error_class}
  7 +
  8 + if params.key?(:start_date) && params.key?(:end_date)
  9 + start_date = Time.parse(params[:start_date]).utc
  10 + end_date = Time.parse(params[:end_date]).utc
  11 + query = {:created_at => {"$lte" => end_date, "$gte" => start_date}}
  12 + end
  13 +
  14 + results = benchmark("[api/v1/notices_controller] query time") { Mongoid.master["notices"].find(query, :fields => fields).to_a }
  15 +
  16 + respond_to do |format|
  17 + format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
  18 + format.json { render :json => Yajl.dump(results) }
  19 + format.xml { render :xml => results }
  20 + end
  21 + end
  22 +
  23 +end
... ...
app/controllers/api/v1/problems_controller.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +class Api::V1::ProblemsController < ApplicationController
  2 + respond_to :json, :xml
  3 +
  4 + def index
  5 + query = {}
  6 + fields = %w{app_id app_name environment message where first_notice_at last_notice_at resolved resolved_at notices_count}
  7 +
  8 + if params.key?(:start_date) && params.key?(:end_date)
  9 + start_date = Time.parse(params[:start_date]).utc
  10 + end_date = Time.parse(params[:end_date]).utc
  11 + query = {:first_notice_at=>{"$lte"=>end_date}, "$or"=>[{:resolved_at=>nil}, {:resolved_at=>{"$gte"=>start_date}}]}
  12 + end
  13 +
  14 + results = benchmark("[api/v1/problems_controller] query time") { Mongoid.master["problems"].find(query, :fields => fields).to_a }
  15 +
  16 + respond_to do |format|
  17 + format.html { render :json => Yajl.dump(results) } # render JSON if no extension specified on path
  18 + format.json { render :json => Yajl.dump(results) }
  19 + format.xml { render :xml => results }
  20 + end
  21 + end
  22 +
  23 +end
... ...
app/controllers/apps_controller.rb
... ... @@ -29,12 +29,14 @@ class AppsController &lt; InheritedResources::Base
29 29 def create
30 30 @app = App.new(params[:app])
31 31 initialize_subclassed_issue_tracker
  32 + initialize_subclassed_notification_service
32 33 create!
33 34 end
34 35  
35 36 def update
36 37 @app = resource
37 38 initialize_subclassed_issue_tracker
  39 + initialize_subclassed_notification_service
38 40 update!
39 41 end
40 42  
... ... @@ -70,6 +72,7 @@ class AppsController &lt; InheritedResources::Base
70 72 end
71 73  
72 74 def initialize_subclassed_issue_tracker
  75 + # set the app's issue tracker
73 76 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
74 77 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type)
75 78 @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
... ... @@ -77,6 +80,15 @@ class AppsController &lt; InheritedResources::Base
77 80 end
78 81 end
79 82  
  83 + def initialize_subclassed_notification_service
  84 + # set the app's notification service
  85 + if params[:app][:notification_service_attributes] && notification_type = params[:app][:notification_service_attributes][:type]
  86 + if NotificationService.subclasses.map(&:name).concat(["NotificationService"]).include?(notification_type)
  87 + @app.notification_service = notification_type.constantize.new(params[:app][:notification_service_attributes])
  88 + end
  89 + end
  90 + end
  91 +
80 92 def begin_of_association_chain
81 93 # Filter the @apps collection to apps watched by the current user, unless user is an admin.
82 94 # If user is an admin, then no filter is applied, and all apps are shown.
... ... @@ -90,6 +102,7 @@ class AppsController &lt; InheritedResources::Base
90 102 def plug_params app
91 103 app.watchers.build if app.watchers.none?
92 104 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
  105 + app.notification_service = NotificationService.new unless app.notification_service_configured?
93 106 app.copy_attributes_from(params[:copy_attributes_from]) if params[:copy_attributes_from]
94 107 end
95 108  
... ...
app/controllers/comments_controller.rb
... ... @@ -11,7 +11,7 @@ class CommentsController &lt; ApplicationController
11 11 else
12 12 flash[:error] = "I'm sorry, your comment was blank! Try again?"
13 13 end
14   - redirect_to app_err_path(@app, @problem)
  14 + redirect_to app_problem_path(@app, @problem)
15 15 end
16 16  
17 17 def destroy
... ... @@ -21,7 +21,7 @@ class CommentsController &lt; ApplicationController
21 21 else
22 22 flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!"
23 23 end
24   - redirect_to app_err_path(@app, @problem)
  24 + redirect_to app_problem_path(@app, @problem)
25 25 end
26 26  
27 27 protected
... ... @@ -34,7 +34,7 @@ class CommentsController &lt; ApplicationController
34 34 end
35 35  
36 36 def find_problem
37   - @problem = @app.problems.find(params[:err_id])
  37 + @problem = @app.problems.find(params[:problem_id])
38 38 end
39 39 end
40 40  
... ...
app/controllers/errs_controller.rb
... ... @@ -1,157 +0,0 @@
1   -class ErrsController < ApplicationController
2   - include ActionView::Helpers::TextHelper
3   -
4   - before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
5   - before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
6   - before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
7   - before_filter :set_sorting_params, :only => [:index, :all]
8   - before_filter :set_tracker_params, :only => [:create_issue]
9   -
10   - def index
11   - app_scope = current_user.admin? ? App.all : current_user.apps
12   -
13   - @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered_by(@sort, @order)
14   - @selected_problems = params[:problems] || []
15   - respond_to do |format|
16   - format.html do
17   - @problems = @problems.page(params[:page]).per(current_user.per_page)
18   - end
19   - format.atom
20   - end
21   - end
22   -
23   - def all
24   - app_scope = current_user.admin? ? App.all : current_user.apps
25   - @problems = Problem.for_apps(app_scope).ordered_by(@sort, @order).page(params[:page]).per(current_user.per_page)
26   - @selected_problems = params[:problems] || []
27   - end
28   -
29   - def show
30   - @notices = @problem.notices.reverse_ordered.page(params[:notice]).per(1)
31   - @notice = @notices.first
32   - @comment = Comment.new
33   - if request.headers['X-PJAX']
34   - params["_pjax"] = nil
35   - render :layout => false
36   - end
37   - end
38   -
39   - def create_issue
40   - # Create an issue on GitHub using user's github token
41   - if params[:tracker] == 'user_github'
42   - if !@app.github_repo?
43   - flash[:error] = "This app doesn't have a GitHub repo set up."
44   - elsif !current_user.github_account?
45   - flash[:error] = "You haven't linked your Github account."
46   - else
47   - @tracker = GithubIssuesTracker.new(
48   - :app => @app,
49   - :username => current_user.github_login,
50   - :oauth_token => current_user.github_oauth_token
51   - )
52   - end
53   -
54   - # Or, create an issue using the App's issue tracker
55   - elsif @app.issue_tracker_configured?
56   - @tracker = @app.issue_tracker
57   -
58   - # Otherwise, display error about missing tracker configuration.
59   - else
60   - flash[:error] = "This app has no issue tracker setup."
61   - end
62   -
63   - if flash[:error].blank? && @tracker
64   - begin
65   - @tracker.create_issue @problem, current_user
66   - rescue Exception => ex
67   - Rails.logger.error "Error during issue creation: " << ex.message
68   - flash[:error] = "There was an error during issue creation: #{ex.message}"
69   - end
70   - end
71   -
72   - redirect_to app_err_path(@app, @problem)
73   - end
74   -
75   - def unlink_issue
76   - @problem.update_attribute :issue_link, nil
77   - redirect_to app_err_path(@app, @problem)
78   - end
79   -
80   - def resolve
81   - @problem.resolve!
82   - flash[:success] = 'Great news everyone! The err has been resolved.'
83   - redirect_to :back
84   - rescue ActionController::RedirectBackError
85   - redirect_to app_path(@app)
86   - end
87   -
88   - def resolve_several
89   - @selected_problems.each(&:resolve!)
90   - flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved."
91   - redirect_to :back
92   - end
93   -
94   - def unresolve_several
95   - @selected_problems.each(&:unresolve!)
96   - flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved."
97   - redirect_to :back
98   - end
99   -
100   - def merge_several
101   - if @selected_problems.length < 2
102   - flash[:notice] = "You must select at least two errors to merge"
103   - else
104   - @merged_problem = Problem.merge!(@selected_problems)
105   - flash[:notice] = "#{@selected_problems.count} errors have been merged."
106   - end
107   - redirect_to :back
108   - end
109   -
110   - def unmerge_several
111   - all = @selected_problems.map(&:unmerge!).flatten
112   - flash[:success] = "#{pluralize(all.length, 'err has', 'errs have')} been unmerged."
113   - redirect_to :back
114   - end
115   -
116   - def destroy_several
117   - nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
118   - flash[:notice] = "#{pluralize(nb_problem_destroy, 'err has', 'errs have')} been deleted."
119   - redirect_to :back
120   - end
121   -
122   - protected
123   - def find_app
124   - @app = App.find(params[:app_id])
125   -
126   - # Mongoid Bug: could not chain: current_user.apps.find_by_id!
127   - # apparently finding by 'watchers.email' and 'id' is broken
128   - raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
129   - end
130   -
131   - def find_problem
132   - @problem = @app.problems.find(params[:id])
133   - end
134   -
135   - def set_tracker_params
136   - IssueTracker.default_url_options[:host] = request.host
137   - IssueTracker.default_url_options[:port] = request.port
138   - IssueTracker.default_url_options[:protocol] = request.scheme
139   - end
140   -
141   - def find_selected_problems
142   - err_ids = (params[:problems] || []).compact
143   - if err_ids.empty?
144   - flash[:notice] = "You have not selected any errors"
145   - redirect_to :back
146   - else
147   - @selected_problems = Array(Problem.find(err_ids))
148   - end
149   - end
150   -
151   - def set_sorting_params
152   - @sort = params[:sort]
153   - @sort = "last_notice_at" unless %w{app message last_notice_at last_deploy_at count}.member?(@sort)
154   - @order = params[:order] || "desc"
155   - end
156   -end
157   -
app/controllers/notices_controller.rb
... ... @@ -5,9 +5,16 @@ class NoticesController &lt; ApplicationController
5 5  
6 6 def create
7 7 # params[:data] if the notice came from a GET request, raw_post if it came via POST
8   - @notice = App.report_error!(params[:data] || request.raw_post)
9   - respond_with @notice
  8 + notice = App.report_error!(params[:data] || request.raw_post)
  9 + api_xml = notice.to_xml(:only => false, :methods => [:id]) do |xml|
  10 + xml.url locate_url(notice.id, :host => Errbit::Config.host)
  11 + end
  12 + render :xml => api_xml
10 13 end
11 14  
  15 + # Redirects a notice to the problem page. Useful when using User Information at Airbrake gem.
  16 + def locate
  17 + problem = Notice.find(params[:id]).problem
  18 + redirect_to app_problem_path(problem.app, problem)
  19 + end
12 20 end
13   -
... ...
app/controllers/problems_controller.rb 0 → 100644
... ... @@ -0,0 +1,157 @@
  1 +class ProblemsController < ApplicationController
  2 + include ActionView::Helpers::TextHelper
  3 +
  4 + before_filter :find_app, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  5 + before_filter :find_problem, :except => [:index, :all, :destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  6 + before_filter :find_selected_problems, :only => [:destroy_several, :resolve_several, :unresolve_several, :merge_several, :unmerge_several]
  7 + before_filter :set_sorting_params, :only => [:index, :all]
  8 + before_filter :set_tracker_params, :only => [:create_issue]
  9 +
  10 + def index
  11 + app_scope = current_user.admin? ? App.all : current_user.apps
  12 +
  13 + @problems = Problem.for_apps(app_scope).in_env(params[:environment]).unresolved.ordered_by(@sort, @order)
  14 + @selected_problems = params[:problems] || []
  15 + respond_to do |format|
  16 + format.html do
  17 + @problems = @problems.page(params[:page]).per(current_user.per_page)
  18 + end
  19 + format.atom
  20 + end
  21 + end
  22 +
  23 + def all
  24 + app_scope = current_user.admin? ? App.all : current_user.apps
  25 + @problems = Problem.for_apps(app_scope).ordered_by(@sort, @order).page(params[:page]).per(current_user.per_page)
  26 + @selected_problems = params[:problems] || []
  27 + end
  28 +
  29 + def show
  30 + @notices = @problem.notices.reverse_ordered.page(params[:notice]).per(1)
  31 + @notice = @notices.first
  32 + @comment = Comment.new
  33 + if request.headers['X-PJAX']
  34 + params["_pjax"] = nil
  35 + render :layout => false
  36 + end
  37 + end
  38 +
  39 + def create_issue
  40 + # Create an issue on GitHub using user's github token
  41 + if params[:tracker] == 'user_github'
  42 + if !@app.github_repo?
  43 + flash[:error] = "This app doesn't have a GitHub repo set up."
  44 + elsif !current_user.github_account?
  45 + flash[:error] = "You haven't linked your Github account."
  46 + else
  47 + @tracker = GithubIssuesTracker.new(
  48 + :app => @app,
  49 + :username => current_user.github_login,
  50 + :oauth_token => current_user.github_oauth_token
  51 + )
  52 + end
  53 +
  54 + # Or, create an issue using the App's issue tracker
  55 + elsif @app.issue_tracker_configured?
  56 + @tracker = @app.issue_tracker
  57 +
  58 + # Otherwise, display error about missing tracker configuration.
  59 + else
  60 + flash[:error] = "This app has no issue tracker setup."
  61 + end
  62 +
  63 + if flash[:error].blank? && @tracker
  64 + begin
  65 + @tracker.create_issue @problem, current_user
  66 + rescue Exception => ex
  67 + Rails.logger.error "Error during issue creation: " << ex.message
  68 + flash[:error] = "There was an error during issue creation: #{ex.message}"
  69 + end
  70 + end
  71 +
  72 + redirect_to app_problem_path(@app, @problem)
  73 + end
  74 +
  75 + def unlink_issue
  76 + @problem.update_attribute :issue_link, nil
  77 + redirect_to app_problem_path(@app, @problem)
  78 + end
  79 +
  80 + def resolve
  81 + @problem.resolve!
  82 + flash[:success] = 'Great news everyone! The err has been resolved.'
  83 + redirect_to :back
  84 + rescue ActionController::RedirectBackError
  85 + redirect_to app_path(@app)
  86 + end
  87 +
  88 + def resolve_several
  89 + @selected_problems.each(&:resolve!)
  90 + flash[:success] = "Great news everyone! #{pluralize(@selected_problems.count, 'err has', 'errs have')} been resolved."
  91 + redirect_to :back
  92 + end
  93 +
  94 + def unresolve_several
  95 + @selected_problems.each(&:unresolve!)
  96 + flash[:success] = "#{pluralize(@selected_problems.count, 'err has', 'errs have')} been unresolved."
  97 + redirect_to :back
  98 + end
  99 +
  100 + def merge_several
  101 + if @selected_problems.length < 2
  102 + flash[:notice] = "You must select at least two errors to merge"
  103 + else
  104 + @merged_problem = Problem.merge!(@selected_problems)
  105 + flash[:notice] = "#{@selected_problems.count} errors have been merged."
  106 + end
  107 + redirect_to :back
  108 + end
  109 +
  110 + def unmerge_several
  111 + all = @selected_problems.map(&:unmerge!).flatten
  112 + flash[:success] = "#{pluralize(all.length, 'err has', 'errs have')} been unmerged."
  113 + redirect_to :back
  114 + end
  115 +
  116 + def destroy_several
  117 + nb_problem_destroy = ProblemDestroy.execute(@selected_problems)
  118 + flash[:notice] = "#{pluralize(nb_problem_destroy, 'err has', 'errs have')} been deleted."
  119 + redirect_to :back
  120 + end
  121 +
  122 + protected
  123 + def find_app
  124 + @app = App.find(params[:app_id])
  125 +
  126 + # Mongoid Bug: could not chain: current_user.apps.find_by_id!
  127 + # apparently finding by 'watchers.email' and 'id' is broken
  128 + raise(Mongoid::Errors::DocumentNotFound.new(App,@app.id)) unless current_user.admin? || current_user.watching?(@app)
  129 + end
  130 +
  131 + def find_problem
  132 + @problem = @app.problems.find(params[:id])
  133 + end
  134 +
  135 + def set_tracker_params
  136 + IssueTracker.default_url_options[:host] = request.host
  137 + IssueTracker.default_url_options[:port] = request.port
  138 + IssueTracker.default_url_options[:protocol] = request.scheme
  139 + end
  140 +
  141 + def find_selected_problems
  142 + err_ids = (params[:problems] || []).compact
  143 + if err_ids.empty?
  144 + flash[:notice] = "You have not selected any errors"
  145 + redirect_to :back
  146 + else
  147 + @selected_problems = Array(Problem.find(err_ids))
  148 + end
  149 + end
  150 +
  151 + def set_sorting_params
  152 + @sort = params[:sort]
  153 + @sort = "last_notice_at" unless %w{app message last_notice_at last_deploy_at count}.member?(@sort)
  154 + @order = params[:order] || "desc"
  155 + end
  156 +end
  157 +
... ...
app/helpers/application_helper.rb
... ... @@ -13,7 +13,7 @@ module ApplicationHelper
13 13 event.dtend = notice.created_at.utc + 60.minutes
14 14 event.organizer = notice.server_environment && notice.server_environment["hostname"]
15 15 event.location = notice.server_environment && notice.server_environment["project-root"]
16   - event.url = app_err_url(:app_id => notice.problem.app.id, :id => notice.problem)
  16 + event.url = app_problem_url(:app_id => notice.problem.app.id, :id => notice.problem)
17 17 end
18 18 end
19 19 end.to_s
... ... @@ -58,12 +58,25 @@ module ApplicationHelper
58 58 percent = 100.0 / total.to_f
59 59 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \
60 60 .sort {|a, b| a[0] <=> b[0]}
61   - render "errs/tally_table", :rows => rows
  61 + render "problems/tally_table", :rows => rows
  62 + end
  63 +
  64 + def head(collection)
  65 + collection.first(head_size)
  66 + end
  67 +
  68 + def tail(collection)
  69 + collection.to_a[head_size..-1].to_a
62 70 end
63 71  
64 72 private
65 73 def total_from_tallies(tallies)
66 74 tallies.values.inject(0) {|sum, n| sum + n}
67 75 end
  76 +
  77 + def head_size
  78 + 4
  79 + end
  80 +
68 81 end
69 82  
... ...
app/helpers/apps_helper.rb
... ... @@ -16,6 +16,16 @@ module AppsHelper
16 16 @any_github_repos
17 17 end
18 18  
  19 + def any_notification_services?
  20 + detect_any_apps_with_attributes unless @any_notification_services
  21 + @any_notification_services
  22 + end
  23 +
  24 + def any_bitbucket_repos?
  25 + detect_any_apps_with_attributes unless @any_bitbucket_repos
  26 + @any_bitbucket_repos
  27 + end
  28 +
19 29 def any_issue_trackers?
20 30 detect_any_apps_with_attributes unless @any_issue_trackers
21 31 @any_issue_trackers
... ... @@ -29,11 +39,14 @@ module AppsHelper
29 39 private
30 40  
31 41 def detect_any_apps_with_attributes
32   - @any_github_repos = @any_issue_trackers = @any_deploys = false
  42 + @any_github_repos = @any_issue_trackers = @any_deploys = @any_bitbucket_repos = @any_notification_services = false
  43 +
33 44 @apps.each do |app|
34 45 @any_github_repos ||= app.github_repo?
  46 + @any_bitbucket_repos ||= app.bitbucket_repo?
35 47 @any_issue_trackers ||= app.issue_tracker_configured?
36 48 @any_deploys ||= !!app.last_deploy_at
  49 + @any_notification_services ||= app.notification_service_configured?
37 50 end
38 51 end
39 52 end
... ...
app/helpers/backtrace_helper.rb 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +module BacktraceHelper
  2 + # Group lines into sections of in-app files and external files
  3 + # (An implementation of Enumerable#chunk so we don't break 1.8.7 support.)
  4 + def grouped_lines(lines)
  5 + line_groups = []
  6 + lines.each do |line|
  7 + in_app = line.in_app?
  8 + if line_groups.last && line_groups.last[0] == in_app
  9 + line_groups.last[1] << line
  10 + else
  11 + line_groups << [in_app, [line]]
  12 + end
  13 + end
  14 + line_groups
  15 + end
  16 +end
... ...
app/helpers/backtrace_line_helper.rb 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +module BacktraceLineHelper
  2 + def link_to_source_file(line, &block)
  3 + text = capture_haml(&block)
  4 + line.in_app? ? link_to_in_app_source_file(line, text) : link_to_external_source_file(text)
  5 + end
  6 +
  7 + private
  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)
  10 + end
  11 +
  12 + def link_to_repo_source_file(line, text)
  13 + link_to_github(line, text) || link_to_bitbucket(line, text)
  14 + end
  15 +
  16 + def link_to_external_source_file(text)
  17 + text
  18 + end
  19 +
  20 + def link_to_github(line, text = nil)
  21 + return unless line.app.github_repo?
  22 + href = "%s#L%s" % [line.app.github_url_to_file(line.file), line.number]
  23 + link_to(text || line.file_name, href, :target => '_blank')
  24 + end
  25 +
  26 + def link_to_bitbucket(line, text = nil)
  27 + return unless line.app.bitbucket_repo?
  28 + href = "%s#cl-%s" % [line.app.bitbucket_url_to_file(line.file), line.number]
  29 + link_to(text || line.file_name, href, :target => '_blank')
  30 + end
  31 +
  32 + 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)
  34 + href = line.app.issue_tracker.url_to_file(line.file, line.number)
  35 + link_to(text || line.file_name, href, :target => '_blank')
  36 + end
  37 +
  38 +end
... ...
app/helpers/errs_helper.rb
... ... @@ -1,17 +0,0 @@
1   -module ErrsHelper
2   - def last_notice_at(problem)
3   - problem.last_notice_at || problem.created_at
4   - end
5   -
6   - def err_confirm
7   - Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?'
8   - end
9   -
10   - def truncated_err_message(problem)
11   - unless (msg = problem.message).blank?
12   - # Truncate & insert invisible chars so that firefox can emulate 'word-wrap: break-word' CSS rule
13   - truncate(msg, :length => 300).scan(/.{1,5}/).map { |s| h(s) }.join("&#8203;").html_safe
14   - end
15   - end
16   -end
17   -
app/helpers/notices_helper.rb
1 1 # encoding: utf-8
2 2 module NoticesHelper
3   - def in_app_backtrace_line?(line)
4   - !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})
5   - end
6   -
7 3 def notice_atom_summary(notice)
8 4 render "notices/atom_entry.html.haml", :notice => notice
9 5 end
10   -
11   - def link_to_source_file(app, line, &block)
12   - text = capture_haml(&block)
13   - if in_app_backtrace_line?(line)
14   - return link_to_github(app, line, text) if app.github_repo?
15   - if app.issue_tracker && app.issue_tracker.respond_to?(:url_to_file)
16   - # Return link to file on tracker if issue tracker supports this
17   - return link_to_issue_tracker_file(app, line, text)
18   - end
19   - end
20   - text
21   - end
22   -
23   - def filepath_parts(file)
24   - [file.split('/').last, file.gsub('[PROJECT_ROOT]', '')]
25   - end
26   -
27   - def link_to_github(app, line, text = nil)
28   - file_name, file_path = filepath_parts(line['file'])
29   - href = "%s#L%s" % [app.github_url_to_file(file_path), line['number']]
30   - link_to(text || file_name, href, :target => '_blank')
31   - end
32   -
33   - def link_to_issue_tracker_file(app, line, text = nil)
34   - file_name, file_path = filepath_parts(line['file'])
35   - href = app.issue_tracker.url_to_file(file_path, line['number'])
36   - link_to(text || file_name, href, :target => '_blank')
37   - end
38   -
39   - # Group lines into sections of in-app files and external files
40   - # (An implementation of Enumerable#chunk so we don't break 1.8.7 support.)
41   - def grouped_lines(lines)
42   - line_groups = []
43   - lines.each do |line|
44   - in_app = in_app_backtrace_line?(line)
45   - if line_groups.last && line_groups.last[0] == in_app
46   - line_groups.last[1] << line
47   - else
48   - line_groups << [in_app, [line]]
49   - end
50   - end
51   - line_groups
52   - end
53   -
54   - def path_for_backtrace_line(line)
55   - path = File.dirname(line['file'])
56   - return '' if path == '.'
57   - # Remove [PROJECT_ROOT]
58   - path.gsub!('[PROJECT_ROOT]/', '')
59   - # Make gem name bold if starts with [GEM_ROOT]/gems
60   - path.gsub!(/\[GEM_ROOT\]\/gems\/([^\/]+)/, "<strong>\\1</strong>")
61   - (path << '/').html_safe
62   - end
63   -
64   - def file_for_backtrace_line(line)
65   - file = File.basename(line['file'])
66   - end
67 6 end
68 7  
... ...
app/helpers/problems_helper.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +module ProblemsHelper
  2 + def problem_confirm
  3 + Errbit::Config.confirm_resolve_err === false ? nil : 'Seriously?'
  4 + end
  5 +
  6 + def truncated_problem_message(problem)
  7 + unless (msg = problem.message).blank?
  8 + # Truncate & insert invisible chars so that firefox can emulate 'word-wrap: break-word' CSS rule
  9 + truncate(msg, :length => 300).scan(/.{1,5}/).map { |s| h(s) }.join("&#8203;").html_safe
  10 + end
  11 + end
  12 +
  13 + def gravatar_tag(email, options = {})
  14 + image_tag gravatar_url(email, options), :alt => email, :class => 'gravatar'
  15 + end
  16 +
  17 + def gravatar_url(email, options = {})
  18 + default_options = {
  19 + :d => Errbit::Config.gravatar_default,
  20 + }
  21 + options.reverse_merge! default_options
  22 + params = options.extract!(:s, :d).delete_if { |k, v| v.blank? }
  23 + email_hash = Digest::MD5.hexdigest(email)
  24 + "http://www.gravatar.com/avatar/#{email_hash}?#{params.to_query}"
  25 + end
  26 +end
  27 +
... ...
app/models/app.rb
... ... @@ -5,6 +5,8 @@ class App
5 5 field :name, :type => String
6 6 field :api_key
7 7 field :github_repo
  8 + field :bitbucket_repo
  9 + field :repository_branch
8 10 field :resolve_errs_on_deploy, :type => Boolean, :default => false
9 11 field :notify_all_users, :type => Boolean, :default => false
10 12 field :notify_on_errs, :type => Boolean, :default => true
... ... @@ -17,6 +19,8 @@ class App
17 19 embeds_many :watchers
18 20 embeds_many :deploys
19 21 embeds_one :issue_tracker
  22 + embeds_one :notification_service
  23 +
20 24 has_many :problems, :inverse_of => :app, :dependent => :destroy
21 25  
22 26 before_validation :generate_api_key, :on => :create
... ... @@ -33,7 +37,8 @@ class App
33 37 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
34 38 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
35 39 :reject_if => proc { |attrs| !IssueTracker.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
36   -
  40 + accepts_nested_attributes_for :notification_service, :allow_destroy => true,
  41 + :reject_if => proc { |attrs| !NotificationService.subclasses.map(&:to_s).include?(attrs[:type].to_s) }
37 42  
38 43 # Processes a new error report.
39 44 #
... ... @@ -74,8 +79,7 @@ class App
74 79 end
75 80  
76 81 def find_or_create_err!(attrs)
77   - Err.any_in(:problem_id => problems.map { |a| a.id }).
78   - where(attrs).first || problems.create!.errs.create!(attrs)
  82 + Err.where(:fingerprint => attrs[:fingerprint]).first || problems.create!.errs.create!(attrs)
79 83 end
80 84  
81 85 # Mongoid Bug: find(id) on association proxies returns an Enumerator
... ... @@ -103,6 +107,9 @@ class App
103 107 end
104 108 alias :notify_on_deploys? :notify_on_deploys
105 109  
  110 + def repo_branch
  111 + self.repository_branch.present? ? self.repository_branch : 'master'
  112 + end
106 113  
107 114 def github_repo?
108 115 self.github_repo.present?
... ... @@ -113,7 +120,19 @@ class App
113 120 end
114 121  
115 122 def github_url_to_file(file)
116   - "#{github_url}/blob/master#{file}"
  123 + "#{github_url}/blob/#{repo_branch + file}"
  124 + end
  125 +
  126 + def bitbucket_repo?
  127 + self.bitbucket_repo.present?
  128 + end
  129 +
  130 + def bitbucket_url
  131 + "https://bitbucket.org/#{bitbucket_repo}" if bitbucket_repo?
  132 + end
  133 +
  134 + def bitbucket_url_to_file(file)
  135 + "#{bitbucket_url}/src/#{repo_branch + file}"
117 136 end
118 137  
119 138  
... ... @@ -121,6 +140,11 @@ class App
121 140 !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?)
122 141 end
123 142  
  143 + def notification_service_configured?
  144 + !!(notification_service && notification_service.class < NotificationService && notification_service.api_token.present?)
  145 + end
  146 +
  147 +
124 148 def notification_recipients
125 149 if notify_all_users
126 150 (User.all.map(&:email).reject(&:blank?) + watchers.map(&:address)).uniq
... ... @@ -137,7 +161,7 @@ class App
137 161 self.send("#{k}=", copy_app.send(k))
138 162 end
139 163 # Clone the embedded objects that can be changed via apps/edit (ignore errs & deploys, etc.)
140   - %w(watchers issue_tracker).each do |relation|
  164 + %w(watchers issue_tracker notification_service).each do |relation|
141 165 if obj = copy_app.send(relation)
142 166 self.send("#{relation}=", obj.is_a?(Array) ? obj.map(&:clone) : obj.clone)
143 167 end
... ...
app/models/backtrace.rb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +class Backtrace
  2 + include Mongoid::Document
  3 + include Mongoid::Timestamps
  4 +
  5 + field :fingerprint
  6 + index :fingerprint
  7 +
  8 + has_many :notices
  9 + has_one :notice
  10 +
  11 + embeds_many :lines, :class_name => "BacktraceLine"
  12 +
  13 + after_initialize :generate_fingerprint
  14 +
  15 + delegate :app, :to => :notice
  16 +
  17 + def self.find_or_create(attributes = {})
  18 + new(attributes).similar || create(attributes)
  19 + end
  20 +
  21 + def similar
  22 + Backtrace.first(:conditions => { :fingerprint => fingerprint } )
  23 + end
  24 +
  25 + def raw=(raw)
  26 + raw.each do |raw_line|
  27 + lines << BacktraceLine.new(BacktraceLineNormalizer.new(raw_line).call)
  28 + end
  29 + end
  30 +
  31 + private
  32 + def generate_fingerprint
  33 + self.fingerprint = Digest::SHA1.hexdigest(lines.map(&:to_s).join)
  34 + end
  35 +
  36 +end
... ...
app/models/backtrace_line.rb 0 → 100644
... ... @@ -0,0 +1,42 @@
  1 +class BacktraceLine
  2 + include Mongoid::Document
  3 + IN_APP_PATH = %r{^\[PROJECT_ROOT\]\/(?!(vendor))}
  4 + GEMS_PATH = %r{\[GEM_ROOT\]\/gems\/([^\/]+)}
  5 +
  6 + field :number, :type => Integer
  7 + field :file
  8 + field :method
  9 +
  10 + embedded_in :backtrace
  11 +
  12 + scope :in_app, where(:file => IN_APP_PATH)
  13 +
  14 + delegate :app, :to => :backtrace
  15 +
  16 + def to_s
  17 + "#{file}:#{number}"
  18 + end
  19 +
  20 + def in_app?
  21 + !!(file =~ IN_APP_PATH)
  22 + end
  23 +
  24 + def path
  25 + File.dirname(file).gsub(/^\.$/, '') + "/"
  26 + end
  27 +
  28 + def file_relative
  29 + file.to_s.sub(IN_APP_PATH, '')
  30 + end
  31 +
  32 + def file_name
  33 + File.basename file
  34 + end
  35 +
  36 + def decorated_path
  37 + path.sub(BacktraceLine::IN_APP_PATH, '').
  38 + sub(BacktraceLine::GEMS_PATH, "<strong>\\1</strong>")
  39 + end
  40 +
  41 +end
  42 +
... ...
app/models/backtrace_line_normalizer.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class BacktraceLineNormalizer
  2 + def initialize(raw_line)
  3 + @raw_line = raw_line
  4 + end
  5 +
  6 + def call
  7 + @raw_line.merge 'file' => normalized_file, 'method' => normalized_method
  8 + end
  9 +
  10 + private
  11 + def normalized_file
  12 + @raw_line['file'].blank? ? "[unknown source]" : @raw_line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems')
  13 + end
  14 +
  15 + def normalized_method
  16 + @raw_line['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__")
  17 + end
  18 +
  19 +end
... ...
app/models/err.rb
... ... @@ -6,20 +6,19 @@ class Err
6 6 include Mongoid::Document
7 7 include Mongoid::Timestamps
8 8  
9   - field :error_class
  9 + field :error_class, :default => "UnknownError"
10 10 field :component
11 11 field :action
12   - field :environment
  12 + field :environment, :default => "unknown"
13 13 field :fingerprint
14 14  
15 15 belongs_to :problem
16 16 index :problem_id
17 17 index :error_class
  18 + index :fingerprint
18 19  
19 20 has_many :notices, :inverse_of => :err, :dependent => :destroy
20 21  
21   - validates_presence_of :error_class, :environment
22   -
23 22 delegate :app, :resolved?, :to => :problem
24 23  
25 24 end
... ...
app/models/error_report.rb
1   -require 'digest/md5'
  1 +require 'digest/sha1'
2 2 require 'hoptoad_notifier'
3 3  
4 4 class ErrorReport
5   - attr_reader :error_class, :message, :backtrace, :request, :server_environment, :api_key, :notifier, :user_attributes
  5 + attr_reader :error_class, :message, :request, :server_environment, :api_key, :notifier, :user_attributes, :current_user
6 6  
7 7 def initialize(xml_or_attributes)
8 8 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
... ... @@ -10,11 +10,7 @@ class ErrorReport
10 10 end
11 11  
12 12 def fingerprint
13   - normalized_backtrace = backtrace[0...3].map do |trace|
14   - trace.merge 'method' => trace['method'].gsub(/[0-9_]{10,}+/, "__FRAGMENT__")
15   - end
16   -
17   - @fingerprint ||= Digest::MD5.hexdigest(normalized_backtrace.to_s)
  13 + @fingerprint ||= Digest::SHA1.hexdigest(fingerprint_source.to_s)
18 14 end
19 15  
20 16 def rails_env
... ... @@ -33,15 +29,21 @@ class ErrorReport
33 29 @app ||= App.find_by_api_key!(api_key)
34 30 end
35 31  
  32 + def backtrace
  33 + @normalized_backtrace ||= Backtrace.find_or_create(:raw => @backtrace)
  34 + end
  35 +
36 36 def generate_notice!
37 37 notice = Notice.new(
38 38 :message => message,
39 39 :error_class => error_class,
40   - :backtrace => backtrace,
  40 + :backtrace_id => backtrace.id,
41 41 :request => request,
42 42 :server_environment => server_environment,
43 43 :notifier => notifier,
44   - :user_attributes => user_attributes)
  44 + :user_attributes => user_attributes,
  45 + :current_user => current_user
  46 + )
45 47  
46 48 err = app.find_or_create_err!(
47 49 :error_class => error_class,
... ... @@ -53,5 +55,18 @@ class ErrorReport
53 55 err.notices << notice
54 56 notice
55 57 end
  58 +
  59 + private
  60 + def fingerprint_source
  61 + {
  62 + :backtrace => backtrace.id,
  63 + :error_class => error_class,
  64 + :component => component,
  65 + :action => action,
  66 + :environment => rails_env,
  67 + :api_key => api_key
  68 + }
  69 + end
  70 +
56 71 end
57 72  
... ...
app/models/issue_tracker.rb
... ... @@ -14,6 +14,7 @@ class IssueTracker
14 14 field :username, :type => String
15 15 field :password, :type => String
16 16 field :ticket_properties, :type => String
  17 + field :subdomain, :type => String
17 18  
18 19 validate :check_params
19 20  
... ...
app/models/issue_trackers/bitbucket_issues_tracker.rb 0 → 100644
... ... @@ -0,0 +1,47 @@
  1 +class IssueTrackers::BitbucketIssuesTracker < IssueTracker
  2 + Label = "bitbucket"
  3 + Note = 'Please configure your Bitbucket repository in the <strong>BITBUCKET REPO</strong> field above.'
  4 + Fields = [
  5 + [:api_token, {
  6 + :placeholder => "Your username on Bitbucket account",
  7 + :label => "Username"
  8 + }],
  9 + [:project_id, {
  10 + :placeholder => "Password for your Bitbucket account",
  11 + :label => "Password"
  12 + }]
  13 + ]
  14 +
  15 + def check_params
  16 + if Fields.detect {|f| self[f[0]].blank? }
  17 + errors.add :base, 'You must specify your Bitbucket username and password'
  18 + end
  19 + end
  20 +
  21 + def repo_name
  22 + app.bitbucket_repo
  23 + end
  24 +
  25 + def create_issue(problem, reported_by = nil)
  26 + bitbucket = BitBucket.new :basic_auth => "#{api_token}:#{project_id}"
  27 +
  28 + begin
  29 + issue = bitbucket.issues.create api_token, repo_name.split('/')[1], :title => issue_title(problem), :content => body_template.result(binding), :priority => 'critical'
  30 + problem.update_attributes(
  31 + :issue_link => "https://bitbucket.org/#{repo_name}/issue/#{issue.local_id}/",
  32 + :issue_type => Label
  33 + )
  34 + rescue BitBucket::Error::Unauthorized
  35 + raise IssueTrackers::AuthenticationError, "Could not authenticate with BitBucket. Please check your username and password."
  36 + end
  37 + end
  38 +
  39 + def body_template
  40 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/bitbucket_issues_body.txt.erb"))
  41 + end
  42 +
  43 + def url
  44 + "https://www.bitbucket.org/#{repo_name}/issues"
  45 + end
  46 +end
  47 +
... ...
app/models/issue_trackers/fogbugz_tracker.rb
1   -class IssueTrackers::FogbugzTracker < IssueTracker
2   - Label = "fogbugz"
3   - Fields = [
4   - [:project_id, {
5   - :label => "Area Name"
6   - }],
7   - [:account, {
8   - :label => "FogBugz URL",
9   - :placeholder => "abc from http://abc.fogbugz.com/"
10   - }],
11   - [:username, {
12   - :placeholder => "Username/Email for your account"
13   - }],
14   - [:password, {
15   - :placeholder => "Password for your account"
16   - }]
17   - ]
18   -
19   - def check_params
20   - if Fields.detect {|f| self[f[0]].blank? }
21   - errors.add :base, 'You must specify your FogBugz Area Name, FogBugz URL, Username, and Password'
  1 +if defined? Fogbugz
  2 + class IssueTrackers::FogbugzTracker < IssueTracker
  3 + Label = "fogbugz"
  4 + Fields = [
  5 + [:project_id, {
  6 + :label => "Area Name"
  7 + }],
  8 + [:account, {
  9 + :label => "FogBugz URL",
  10 + :placeholder => "abc from http://abc.fogbugz.com/"
  11 + }],
  12 + [:username, {
  13 + :placeholder => "Username/Email for your account"
  14 + }],
  15 + [:password, {
  16 + :placeholder => "Password for your account"
  17 + }]
  18 + ]
  19 +
  20 + def check_params
  21 + if Fields.detect {|f| self[f[0]].blank? }
  22 + errors.add :base, 'You must specify your FogBugz Area Name, FogBugz URL, Username, and Password'
  23 + end
22 24 end
23   - end
24 25  
25   - def create_issue(problem, reported_by = nil)
26   - fogbugz = Fogbugz::Interface.new(:email => username, :password => password, :uri => "https://#{account}.fogbugz.com")
27   - fogbugz.authenticate
  26 + def create_issue(problem, reported_by = nil)
  27 + fogbugz = Fogbugz::Interface.new(:email => username, :password => password, :uri => "https://#{account}.fogbugz.com")
  28 + fogbugz.authenticate
28 29  
29   - issue = {}
30   - issue['sTitle'] = issue_title problem
31   - issue['sArea'] = project_id
32   - issue['sEvent'] = body_template.result(binding)
33   - issue['sTags'] = ['errbit'].join(',')
34   - issue['cols'] = ['ixBug'].join(',')
  30 + issue = {}
  31 + issue['sTitle'] = issue_title problem
  32 + issue['sArea'] = project_id
  33 + issue['sEvent'] = body_template.result(binding)
  34 + issue['sTags'] = ['errbit'].join(',')
  35 + issue['cols'] = ['ixBug'].join(',')
35 36  
36   - fb_resp = fogbugz.command(:new, issue)
37   - problem.update_attributes(
38   - :issue_link => "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}",
39   - :issue_type => Label
40   - )
  37 + fb_resp = fogbugz.command(:new, issue)
  38 + problem.update_attributes(
  39 + :issue_link => "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}",
  40 + :issue_type => Label
  41 + )
41 42  
42   - end
  43 + end
43 44  
44   - def body_template
45   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/fogbugz_body.txt.erb"))
46   - end
  45 + def body_template
  46 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/fogbugz_body.txt.erb"))
  47 + end
47 48  
48   - def url
49   - "http://#{account}.fogbugz.com/"
  49 + def url
  50 + "http://#{account}.fogbugz.com/"
  51 + end
50 52 end
51 53 end
52   -
... ...
app/models/issue_trackers/github_issues_tracker.rb
1   -class IssueTrackers::GithubIssuesTracker < IssueTracker
2   - Label = "github"
3   - Note = 'Please configure your github repository in the <strong>GITHUB REPO</strong> field above.<br/>' <<
4   - 'Instead of providing your username & password, you can link your Github account ' <<
5   - 'to your user profile, and allow Errbit to create issues using your OAuth token.'
6   -
7   - Fields = [
8   - [:username, {
9   - :placeholder => "Your username on GitHub"
10   - }],
11   - [:password, {
12   - :placeholder => "Password for your account"
13   - }]
14   - ]
15   -
16   - attr_accessor :oauth_token
17   -
18   - def project_id
19   - app.github_repo
20   - end
21   -
22   - def check_params
23   - if Fields.detect {|f| self[f[0]].blank? }
24   - errors.add :base, 'You must specify your GitHub username and password'
  1 +if defined? Octokit
  2 + class IssueTrackers::GithubIssuesTracker < IssueTracker
  3 + Label = "github"
  4 + Note = 'Please configure your github repository in the <strong>GITHUB REPO</strong> field above.<br/>' <<
  5 + 'Instead of providing your username & password, you can link your Github account ' <<
  6 + 'to your user profile, and allow Errbit to create issues using your OAuth token.'
  7 +
  8 + Fields = [
  9 + [:username, {
  10 + :placeholder => "Your username on GitHub"
  11 + }],
  12 + [:password, {
  13 + :placeholder => "Password for your account"
  14 + }]
  15 + ]
  16 +
  17 + attr_accessor :oauth_token
  18 +
  19 + def project_id
  20 + app.github_repo
25 21 end
26   - end
27 22  
28   - def create_issue(problem, reported_by = nil)
29   - # Login using OAuth token, if given.
30   - if oauth_token
31   - client = Octokit::Client.new(:login => username, :oauth_token => oauth_token)
32   - else
33   - client = Octokit::Client.new(:login => username, :password => password)
  23 + def check_params
  24 + if Fields.detect {|f| self[f[0]].blank? }
  25 + errors.add :base, 'You must specify your GitHub username and password'
  26 + end
34 27 end
35 28  
36   - begin
37   - issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'), options = {})
38   - problem.update_attributes(
39   - :issue_link => issue.html_url,
40   - :issue_type => Label
41   - )
42   -
43   - rescue Octokit::Unauthorized
44   - raise IssueTrackers::AuthenticationError, "Could not authenticate with GitHub. Please check your username and password."
  29 + def create_issue(problem, reported_by = nil)
  30 + # Login using OAuth token, if given.
  31 + if oauth_token
  32 + client = Octokit::Client.new(:login => username, :oauth_token => oauth_token)
  33 + else
  34 + client = Octokit::Client.new(:login => username, :password => password)
  35 + end
  36 +
  37 + begin
  38 + issue = client.create_issue(project_id, issue_title(problem), body_template.result(binding).unpack('C*').pack('U*'), options = {})
  39 + problem.update_attributes(
  40 + :issue_link => issue.html_url,
  41 + :issue_type => Label
  42 + )
  43 +
  44 + rescue Octokit::Unauthorized
  45 + raise IssueTrackers::AuthenticationError, "Could not authenticate with GitHub. Please check your username and password."
  46 + end
45 47 end
46   - end
47 48  
48   - def body_template
49   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/github_issues_body.txt.erb").gsub(/^\s*/, ''))
50   - end
  49 + def body_template
  50 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/github_issues_body.txt.erb").gsub(/^\s*/, ''))
  51 + end
51 52  
52   - def url
53   - "https://github.com/#{project_id}/issues"
  53 + def url
  54 + "https://github.com/#{project_id}/issues"
  55 + end
54 56 end
55 57 -end
  58 +end
56 59 \ No newline at end of file
... ...
app/models/issue_trackers/lighthouse_tracker.rb
1   -class IssueTrackers::LighthouseTracker < IssueTracker
2   - Label = "lighthouseapp"
3   - Fields = [
4   - [:account, {
5   - :placeholder => "abc from http://abc.lighthouseapp.com"
6   - }],
7   - [:api_token, {
8   - :placeholder => "API Token for your account"
9   - }],
10   - [:project_id, {
11   - :placeholder => "Lighthouse project"
12   - }]
13   - ]
14   -
15   - def check_params
16   - if Fields.detect {|f| self[f[0]].blank? }
17   - errors.add :base, 'You must specify your Lighthouseapp account, API token and Project ID'
  1 +if defined? Lighthouse
  2 + class IssueTrackers::LighthouseTracker < IssueTracker
  3 + Label = "lighthouseapp"
  4 + Fields = [
  5 + [:account, {
  6 + :placeholder => "abc from http://abc.lighthouseapp.com"
  7 + }],
  8 + [:api_token, {
  9 + :placeholder => "API Token for your account"
  10 + }],
  11 + [:project_id, {
  12 + :placeholder => "Lighthouse project"
  13 + }]
  14 + ]
  15 +
  16 + def check_params
  17 + if Fields.detect {|f| self[f[0]].blank? }
  18 + errors.add :base, 'You must specify your Lighthouseapp account, API token and Project ID'
  19 + end
18 20 end
19   - end
20 21  
21   - def create_issue(problem, reported_by = nil)
22   - Lighthouse.account = account
23   - Lighthouse.token = api_token
24   - # updating lighthouse account
25   - Lighthouse::Ticket.site
26   - Lighthouse::Ticket.format = :xml
27   - ticket = Lighthouse::Ticket.new(:project_id => project_id)
28   - ticket.title = issue_title problem
  22 + def create_issue(problem, reported_by = nil)
  23 + Lighthouse.account = account
  24 + Lighthouse.token = api_token
  25 + # updating lighthouse account
  26 + Lighthouse::Ticket.site
  27 + Lighthouse::Ticket.format = :xml
  28 + ticket = Lighthouse::Ticket.new(:project_id => project_id)
  29 + ticket.title = issue_title problem
29 30  
30   - ticket.body = body_template.result(binding)
  31 + ticket.body = body_template.result(binding)
31 32  
32   - ticket.tags << "errbit"
33   - ticket.save!
34   - problem.update_attributes(
35   - :issue_link => "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, ''),
36   - :issue_type => Label
37   - )
  33 + ticket.tags << "errbit"
  34 + ticket.save!
  35 + problem.update_attributes(
  36 + :issue_link => "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, ''),
  37 + :issue_type => Label
  38 + )
38 39  
39   - end
  40 + end
40 41  
41   - def body_template
42   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/lighthouseapp_body.txt.erb").gsub(/^\s*/, ''))
43   - end
  42 + def body_template
  43 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/lighthouseapp_body.txt.erb").gsub(/^\s*/, ''))
  44 + end
44 45  
45   - def url
46   - "http://#{account}.lighthouseapp.com"
  46 + def url
  47 + "http://#{account}.lighthouseapp.com"
  48 + end
47 49 end
48   -end
49   -
  50 +end
50 51 \ No newline at end of file
... ...
app/models/issue_trackers/pivotal_labs_tracker.rb
1   -class IssueTrackers::PivotalLabsTracker < IssueTracker
2   - Label = "pivotal"
3   - Fields = [
4   - [:api_token, {
5   - :placeholder => "API Token for your account"
6   - }],
7   - [:project_id, {}]
8   - ]
  1 +if defined? PivotalTracker
  2 + class IssueTrackers::PivotalLabsTracker < IssueTracker
  3 + Label = "pivotal"
  4 + Fields = [
  5 + [:api_token, {
  6 + :placeholder => "API Token for your account"
  7 + }],
  8 + [:project_id, {}]
  9 + ]
9 10  
10   - def check_params
11   - if Fields.detect {|f| self[f[0]].blank? }
12   - errors.add :base, 'You must specify your Pivotal Tracker API token and Project ID'
  11 + def check_params
  12 + if Fields.detect {|f| self[f[0]].blank? }
  13 + errors.add :base, 'You must specify your Pivotal Tracker API token and Project ID'
  14 + end
13 15 end
14   - end
15 16  
16   - def create_issue(problem, reported_by = nil)
17   - PivotalTracker::Client.token = api_token
18   - PivotalTracker::Client.use_ssl = true
19   - project = PivotalTracker::Project.find project_id.to_i
20   - story = project.stories.create :name => issue_title(problem),
21   - :story_type => 'bug', :description => body_template.result(binding),
22   - :requested_by => reported_by.name
  17 + def create_issue(problem, reported_by = nil)
  18 + PivotalTracker::Client.token = api_token
  19 + PivotalTracker::Client.use_ssl = true
  20 + project = PivotalTracker::Project.find project_id.to_i
  21 + story = project.stories.create :name => issue_title(problem),
  22 + :story_type => 'bug', :description => body_template.result(binding),
  23 + :requested_by => reported_by.name
23 24  
24   - if story.errors.present?
25   - raise IssueTrackers::IssueTrackerError, story.errors.first
26   - else
27   - problem.update_attributes(
28   - :issue_link => "https://www.pivotaltracker.com/story/show/#{story.id}",
29   - :issue_type => Label
30   - )
  25 + if story.errors.present?
  26 + raise IssueTrackers::IssueTrackerError, story.errors.first
  27 + else
  28 + problem.update_attributes(
  29 + :issue_link => "https://www.pivotaltracker.com/story/show/#{story.id}",
  30 + :issue_type => Label
  31 + )
  32 + end
31 33 end
32   - end
33 34  
34   - def body_template
35   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/pivotal_body.txt.erb"))
36   - end
  35 + def body_template
  36 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/pivotal_body.txt.erb"))
  37 + end
37 38  
38   - def url
39   - "https://www.pivotaltracker.com/"
  39 + def url
  40 + "https://www.pivotaltracker.com/"
  41 + end
40 42 end
41   -end
42   -
  43 +end
43 44 \ No newline at end of file
... ...
app/models/issue_trackers/redmine_tracker.rb
1   -class IssueTrackers::RedmineTracker < IssueTracker
2   - Label = "redmine"
3   - Fields = [
4   - [:account, {
5   - :label => "Redmine URL",
6   - :placeholder => "e.g. http://www.redmine.org/"
7   - }],
8   - [:api_token, {
9   - :placeholder => "API Token for your account"
10   - }],
11   - [:project_id, {
12   - :label => "Ticket Project",
13   - :placeholder => "Redmine Project where tickets will be created"
14   - }],
15   - [:alt_project_id, {
16   - :optional => true,
17   - :label => "App Project",
18   - :placeholder => "Where app's files & revisions can be viewed. (Leave blank to use the above project by default)"
19   - }]
20   - ]
  1 +if defined? RedmineClient
  2 + class IssueTrackers::RedmineTracker < IssueTracker
  3 + Label = "redmine"
  4 + Fields = [
  5 + [:account, {
  6 + :label => "Redmine URL",
  7 + :placeholder => "e.g. http://www.redmine.org/"
  8 + }],
  9 + [:api_token, {
  10 + :placeholder => "API Token for your account"
  11 + }],
  12 + [:project_id, {
  13 + :label => "Ticket Project",
  14 + :placeholder => "Redmine Project where tickets will be created"
  15 + }],
  16 + [:alt_project_id, {
  17 + :optional => true,
  18 + :label => "App Project",
  19 + :placeholder => "Where app's files & revisions can be viewed. (Leave blank to use the above project by default)"
  20 + }]
  21 + ]
21 22  
22   - def check_params
23   - if Fields.detect {|f| self[f[0]].blank? && !f[1][:optional]}
24   - errors.add :base, 'You must specify your Redmine URL, API token and Project ID'
  23 + def check_params
  24 + 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'
  26 + end
25 27 end
26   - end
27 28  
28   - def create_issue(problem, reported_by = nil)
29   - token = api_token
30   - acc = account
31   - RedmineClient::Base.configure do
32   - self.token = token
33   - self.site = acc
34   - self.format = :xml
  29 + def create_issue(problem, reported_by = nil)
  30 + token = api_token
  31 + acc = account
  32 + RedmineClient::Base.configure do
  33 + self.token = token
  34 + self.site = acc
  35 + self.format = :xml
  36 + end
  37 + issue = RedmineClient::Issue.new(:project_id => project_id)
  38 + issue.subject = issue_title problem
  39 + issue.description = body_template.result(binding)
  40 + issue.save!
  41 + problem.update_attributes(
  42 + :issue_link => "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}"),
  43 + :issue_type => Label
  44 + )
35 45 end
36   - issue = RedmineClient::Issue.new(:project_id => project_id)
37   - issue.subject = issue_title problem
38   - issue.description = body_template.result(binding)
39   - issue.save!
40   - problem.update_attributes(
41   - :issue_link => "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}"),
42   - :issue_type => Label
43   - )
44   - end
45 46  
46   - def url_to_file(file_path, line_number = nil)
47   - # alt_project_id let's users specify a different project for tickets / app files.
48   - project = self.alt_project_id.present? ? self.alt_project_id : self.project_id
49   - url = "#{self.account}/projects/#{project}/repository/annotate/#{file_path.sub(/^\//,'')}"
50   - line_number ? url << "#L#{line_number}" : url
51   - end
  47 + def url_to_file(file_path, line_number = nil)
  48 + # 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
  50 + url = "#{self.account}/projects/#{project}/repository/annotate/#{file_path.sub(/^\//,'')}"
  51 + line_number ? url << "#L#{line_number}" : url
  52 + end
52 53  
53   - def body_template
54   - @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/textile_body.txt.erb"))
55   - end
  54 + def body_template
  55 + @@body_template ||= ERB.new(File.read(Rails.root + "app/views/issue_trackers/textile_body.txt.erb"))
  56 + end
56 57  
57   - def url
58   - acc_url = account.start_with?('http') ? account : "http://#{account}"
59   - URI.parse("#{acc_url}?project_id=#{project_id}").to_s
60   - rescue URI::InvalidURIError
  58 + def url
  59 + acc_url = account.start_with?('http') ? account : "http://#{account}"
  60 + URI.parse("#{acc_url}?project_id=#{project_id}").to_s
  61 + rescue URI::InvalidURIError
  62 + end
61 63 end
62 64 end
63   -
... ...
app/models/notice.rb
... ... @@ -6,14 +6,17 @@ class Notice
6 6 include Mongoid::Timestamps
7 7  
8 8 field :message
9   - field :backtrace, :type => Array
10 9 field :server_environment, :type => Hash
11 10 field :request, :type => Hash
12 11 field :notifier, :type => Hash
13 12 field :user_attributes, :type => Hash
  13 + field :current_user, :type => Hash
14 14 field :error_class
  15 + delegate :lines, :to => :backtrace, :prefix => true
  16 + delegate :app, :to => :err
15 17  
16 18 belongs_to :err
  19 + belongs_to :backtrace, :index => true
17 20 index :created_at
18 21 index(
19 22 [
... ... @@ -89,17 +92,8 @@ class Notice
89 92 request['session'] || {}
90 93 end
91 94  
92   - # Backtrace containing only files from the app itself (ignore gems)
93   - def app_backtrace
94   - backtrace.select { |l| l && l['file'] && l['file'].include?("[PROJECT_ROOT]") }
95   - end
96   -
97   - def backtrace
98   - # If gems are vendored into project, treat vendored gem dir as [GEM_ROOT]
99   - (read_attribute(:backtrace) || []).map do |line|
100   - # Changes "[PROJECT_ROOT]/rubygems/ruby/1.9.1/gems" to "[GEM_ROOT]/gems"
101   - line.merge 'file' => line['file'].to_s.gsub(/\[PROJECT_ROOT\]\/.*\/ruby\/[0-9.]+\/gems/, '[GEM_ROOT]/gems')
102   - end
  95 + def in_app_backtrace_lines
  96 + backtrace_lines.in_app
103 97 end
104 98  
105 99 protected
... ... @@ -113,11 +107,11 @@ class Notice
113 107 end
114 108  
115 109 def remove_cached_attributes_from_problem
116   - problem.remove_cached_notice_attribures(self) if err
  110 + problem.remove_cached_notice_attributes(self) if err
117 111 end
118 112  
119 113 def unresolve_problem
120   - problem.update_attribute(:resolved, false) if problem.resolved?
  114 + problem.update_attributes!(:resolved => false, :resolved_at => nil, :notices_count => 1) if problem.resolved?
121 115 end
122 116  
123 117 def cache_attributes_on_problem
... ... @@ -128,8 +122,6 @@ class Notice
128 122 [:server_environment, :request, :notifier].each do |h|
129 123 send("#{h}=",sanitize_hash(send(h)))
130 124 end
131   - # Set unknown backtrace files
132   - read_attribute(:backtrace).each{|line| line['file'] = "[unknown source]" if line['file'].blank? }
133 125 end
134 126  
135 127 def sanitize_hash(h)
... ...
app/models/notice_observer.rb
... ... @@ -2,18 +2,21 @@ class NoticeObserver &lt; Mongoid::Observer
2 2 observe :notice
3 3  
4 4 def after_create notice
5   - return unless should_notify? notice
  5 + # if the app has a notification service, fire it off
  6 + if notice.app.notification_service_configured?
  7 + notice.app.notification_service.create_notification(notice.problem)
  8 + end
6 9  
7   - Mailer.err_notification(notice).deliver
  10 + if notice.app.notification_recipients.any?
  11 + Mailer.err_notification(notice).deliver
  12 + end
8 13 end
9 14  
10 15 private
11 16  
12 17 def should_notify? notice
13 18 app = notice.app
14   - app.notify_on_errs? &&
15   - (Errbit::Config.per_app_email_at_notices && app.email_at_notices || Errbit::Config.email_at_notices).include?(notice.problem.notices_count) &&
16   - app.notification_recipients.any?
  19 + app.notify_on_errs? and (app.notification_recipients.any? or !app.notification_service.nil?) and
  20 + (app.email_at_notices or Errbit::Config.email_at_notices).include?(notice.problem.notices_count)
17 21 end
18   -
19 22 end
... ...
app/models/notification_service.rb 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +class NotificationService
  2 + include Mongoid::Document
  3 +
  4 + include Rails.application.routes.url_helpers
  5 + default_url_options[:host] = ActionMailer::Base.default_url_options[:host]
  6 +
  7 + field :room_id, :type => String
  8 + field :api_token, :type => String
  9 + field :subdomain, :type => String
  10 + field :sender_name, :type => String
  11 +
  12 + embedded_in :app, :inverse_of => :notification_service
  13 +
  14 + validate :check_params
  15 +
  16 + # Subclasses are responsible for overwriting this method.
  17 + def check_params; true; end
  18 +
  19 + def notification_description(problem)
  20 + "[#{ problem.environment }][#{ problem.where }] #{problem.message.to_s.truncate(100)}"
  21 + end
  22 +
  23 + # Allows us to set the issue tracker class from a single form.
  24 + def type; self._type; end
  25 + def type=(t); self._type=t; end
  26 +
  27 + def url; nil; end
  28 +
  29 + # Retrieve tracker label from either class or instance.
  30 + Label = ''
  31 + def self.label; self::Label; end
  32 + def label; self.class.label; end
  33 +end
... ...
app/models/notification_services/campfire_service.rb 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +if defined? Campy
  2 + class NotificationServices::CampfireService < NotificationService
  3 + Label = "campfire"
  4 + Fields = [
  5 + [:subdomain, {
  6 + :placeholder => "Campfire Subdomain"
  7 + }],
  8 + [:api_token, {
  9 + :placeholder => "API Token"
  10 + }],
  11 + [:room_id, {
  12 + :placeholder => "Room ID",
  13 + :label => "Room ID"
  14 + }],
  15 + ]
  16 +
  17 + def check_params
  18 + if Fields.detect {|f| self[f[0]].blank? }
  19 + errors.add :base, 'You must specify your Campfire Subdomain, API token and Room ID'
  20 + end
  21 + end
  22 +
  23 + def url
  24 + "http://campfirenow.com/"
  25 + end
  26 +
  27 + def create_notification(problem)
  28 + # build the campfire client
  29 + campy = Campy::Room.new(:account => subdomain, :token => api_token, :room_id => room_id)
  30 + # post the issue to the campfire room
  31 + campy.speak "[errbit] #{problem.app.name} #{notification_description problem} - http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}/problems/#{problem.id.to_s}"
  32 + end
  33 + end
  34 +end
... ...
app/models/notification_services/gtalk_service.rb 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +class NotificationServices::GtalkService < NotificationService
  2 + Label = "gtalk"
  3 + Fields = [
  4 + [:subdomain, {
  5 + :placeholder => "username@example.com",
  6 + :label => "Username"
  7 + }],
  8 + [:api_token, {
  9 + :placeholder => "password",
  10 + :label => "Password"
  11 + }],
  12 + [:room_id, {
  13 + :placeholder => "touser@example.com",
  14 + :label => "Send To User"
  15 + }],
  16 + ]
  17 +
  18 + def check_params
  19 + if Fields.detect {|f| self[f[0]].blank? }
  20 + errors.add :base, 'You must specify your Username, Password and To User'
  21 + end
  22 + end
  23 +
  24 + def url
  25 + "http://www.google.com/talk/"
  26 + end
  27 +
  28 + def create_notification(problem)
  29 + # build the xmpp client
  30 + client = Jabber::Client.new(Jabber::JID.new(subdomain))
  31 + client.connect("talk.google.com")
  32 + client.auth(api_token)
  33 +
  34 + # post the issue to the xmpp room
  35 + client.send(Jabber::Message.new(room_id, "[errbit] http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_description problem}"))
  36 + end
  37 +end
0 38 \ No newline at end of file
... ...
app/models/notification_services/hipchat_service.rb 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +if defined? HipChat
  2 + class NotificationServices::HipchatService < NotificationService
  3 + Label = 'hipchat'
  4 + Fields = [
  5 + [:api_token, {
  6 + :placeholder => "API Token"
  7 + }],
  8 + [:room_id, {
  9 + :placeholder => "Room ID",
  10 + :label => "Room ID"
  11 + }],
  12 + ]
  13 +
  14 + def check_params
  15 + if Fields.any? { |f, _| self[f].blank? }
  16 + errors.add :base, 'You must specify your Hipchat API token and Room ID'
  17 + end
  18 + end
  19 +
  20 + def create_notification(problem)
  21 + url = app_problem_url problem.app, problem
  22 + message = <<-MSG.strip_heredoc
  23 + [#{ERB::Util.html_escape problem.app.name}]#{ERB::Util.html_escape notification_description(problem)}<br>
  24 + <a href="#{url}">#{url}</a>
  25 + MSG
  26 +
  27 + client = HipChat::Client.new(api_token)
  28 + client[room_id].send('Errbit', message, :color => 'red')
  29 + end
  30 + end
  31 +end
0 32 \ No newline at end of file
... ...
app/models/notification_services/hoiio_service.rb 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +class NotificationServices::HoiioService < NotificationService
  2 + Label = "hoiio"
  3 + Fields = [
  4 + [:api_token, {
  5 + :placeholder => "App ID",
  6 + :label => "App ID"
  7 + }],
  8 + [:subdomain, {
  9 + :placeholder => "Access Token",
  10 + :label => "Access Token"
  11 + }],
  12 + [:room_id, {
  13 + :placeholder => "+6511111111, +6511111111",
  14 + :label => "Recipient's phone numbers seperated by comma. Phone numbers should start with a \"+\" and country code."
  15 + }]
  16 + ]
  17 +
  18 + def check_params
  19 + if Fields.detect {|f| self[f[0]].blank? }
  20 + errors.add :base, 'You must specify your App ID, Access Token and Recipient\'s phone numbers'
  21 + end
  22 + end
  23 +
  24 + def notification_description(problem)
  25 + "[#{ problem.environment }]#{problem.message.to_s.truncate(50)}"
  26 + end
  27 +
  28 + def create_notification(problem)
  29 + # build the hoi client
  30 + sms = Hoi::SMS.new(api_token, subdomain)
  31 +
  32 + # send sms
  33 + room_id.split(',').each do |number|
  34 + sms.send :dest => number, :msg => "http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_description problem}"
  35 + end
  36 +
  37 + end
  38 +end
0 39 \ No newline at end of file
... ...
app/models/notification_services/pushover_service.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +class NotificationServices::PushoverService < NotificationService
  2 + Label = "pushover"
  3 + Fields = [
  4 + [:api_token, {
  5 + :placeholder => "User Key",
  6 + :label => "User Key"
  7 + }],
  8 + [:subdomain, {
  9 + :placeholder => "Application API Token",
  10 + :label => "Application API Token"
  11 + }]
  12 + ]
  13 +
  14 + def check_params
  15 + if Fields.detect {|f| self[f[0]].blank? }
  16 + errors.add :base, 'You must specify your User Key and Application API Token.'
  17 + end
  18 + end
  19 +
  20 + def create_notification(problem)
  21 + # build the hoi client
  22 + notification = Rushover::Client.new(subdomain)
  23 +
  24 + # send push notification to pushover
  25 + notification.notify(api_token, "#{notification_description problem}", :priority => 1, :title => "Errbit Notification", :url => "http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s}", :url_title => "Link to error")
  26 +
  27 + end
  28 +end
0 29 \ No newline at end of file
... ...
app/models/problem.rb
... ... @@ -6,9 +6,11 @@ class Problem
6 6 include Mongoid::Document
7 7 include Mongoid::Timestamps
8 8  
9   - field :last_notice_at, :type => DateTime
  9 + field :last_notice_at, :type => DateTime, :default => Proc.new { Time.now }
  10 + field :first_notice_at, :type => DateTime, :default => Proc.new { Time.now }
10 11 field :last_deploy_at, :type => Time
11 12 field :resolved, :type => Boolean, :default => false
  13 + field :resolved_at, :type => Time
12 14 field :issue_link, :type => String
13 15 field :issue_type, :type => String
14 16  
... ... @@ -28,7 +30,9 @@ class Problem
28 30 index :app_name
29 31 index :message
30 32 index :last_notice_at
  33 + index :first_notice_at
31 34 index :last_deploy_at
  35 + index :resolved_at
32 36 index :notices_count
33 37  
34 38 belongs_to :app
... ... @@ -41,6 +45,8 @@ class Problem
41 45 scope :unresolved, where(:resolved => false)
42 46 scope :ordered, order_by(:last_notice_at.desc)
43 47 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))}
  48 +
  49 + validates_presence_of :last_notice_at, :first_notice_at
44 50  
45 51  
46 52 def self.in_env(env)
... ... @@ -52,11 +58,11 @@ class Problem
52 58 end
53 59  
54 60 def resolve!
55   - self.update_attributes!(:resolved => true, :notices_count => 0)
  61 + self.update_attributes!(:resolved => true, :resolved_at => Time.now)
56 62 end
57 63  
58 64 def unresolve!
59   - self.update_attributes!(:resolved => false)
  65 + self.update_attributes!(:resolved => false, :resolved_at => nil)
60 66 end
61 67  
62 68 def unresolved?
... ... @@ -103,6 +109,10 @@ class Problem
103 109 else raise("\"#{sort}\" is not a recognized sort")
104 110 end
105 111 end
  112 +
  113 + def self.in_date_range(date_range)
  114 + where(:first_notice_at.lte => date_range.end).where("$or" => [{:resolved_at => nil}, {:resolved_at.gte => date_range.begin}])
  115 + end
106 116  
107 117  
108 118 def reset_cached_attributes
... ... @@ -124,21 +134,26 @@ class Problem
124 134 end
125 135  
126 136 def cache_notice_attributes(notice=nil)
127   - notice ||= notices.first
128   - attrs = {:last_notice_at => notices.order_by([:created_at, :asc]).last.try(:created_at)}
  137 + first_notice = notices.order_by([:created_at, :asc]).first
  138 + last_notice = notices.order_by([:created_at, :asc]).last
  139 + notice ||= first_notice
  140 +
  141 + attrs = {}
  142 + attrs[:first_notice_at] = first_notice.created_at if first_notice
  143 + attrs[:last_notice_at] = last_notice.created_at if last_notice
129 144 attrs.merge!(
130   - :message => notice.message,
  145 + :message => notice.message,
131 146 :environment => notice.environment_name,
132 147 :error_class => notice.error_class,
133   - :where => notice.where,
  148 + :where => notice.where,
134 149 :messages => attribute_count_increase(:messages, notice.message),
135 150 :hosts => attribute_count_increase(:hosts, notice.host),
136 151 :user_agents => attribute_count_increase(:user_agents, notice.user_agent_string)
137   - ) if notice
  152 + ) if notice
138 153 update_attributes!(attrs)
139 154 end
140 155  
141   - def remove_cached_notice_attribures(notice)
  156 + def remove_cached_notice_attributes(notice)
142 157 update_attributes!(
143 158 :messages => attribute_count_descrease(:messages, notice.message),
144 159 :hosts => attribute_count_descrease(:hosts, notice.host),
... ...
app/views/apps/_configuration_instructions.html.haml
1 1 %pre
2   - %code
3   - :preserve
4   - # Require the hoptoad_notifier gem in your App.
5   - # ---------------------------------------------
6   - #
7   - # Rails 3 - In your Gemfile
8   - # gem 'airbrake'
9   - #
10   - # Rails 2 - In environment.rb
11   - # config.gem 'airbrake'
12   - #
13   - # Then add the following to config/initializers/errbit.rb
14   - # -------------------------------------------------------
  2 + %code
  3 + :preserve
  4 + # Require the hoptoad_notifier gem in your App.
  5 + # ---------------------------------------------
  6 + #
  7 + # Rails 3 - In your Gemfile
  8 + # gem 'airbrake'
  9 + #
  10 + # Rails 2 - In environment.rb
  11 + # config.gem 'airbrake'
  12 + #
  13 + # Then add the following to config/initializers/errbit.rb
  14 + # -------------------------------------------------------
15 15  
16   - Airbrake.configure do |config|
17   - config.api_key = '#{app.api_key}'
18   - config.host = '#{request.host}'
19   - config.port = #{request.port}
20   - config.secure = config.port == 443
21   - end
  16 + Airbrake.configure do |config|
  17 + config.api_key = '#{app.api_key}'
  18 + config.host = '#{request.host}'
  19 + config.port = #{request.port}
  20 + config.secure = config.port == 443
  21 + end
22 22  
23   - # Set up Javascript notifications
24   - # -------------------------------
25   - #
26   - # To receive notifications for javascript errors,
27   - # you should add <%= airbrake_javascript_notifier %> to the top of your layouts.
28   - #
29   - # Testing
30   - # -------
31   - #
32   - # Rails 2 - you'll need to vendor airbrake to get the rake tasks
33   - # rake gems:unpack GEM=airbrake
34   - #
35   - # Run:
36   - # rake airbrake:test
37   - # refresh this page
  23 + # Set up Javascript notifications
  24 + # -------------------------------
  25 + #
  26 + # To receive notifications for javascript errors,
  27 + # you should add <%= airbrake_javascript_notifier %> to the top of your layouts.
  28 + #
  29 + # Testing
  30 + # -------
  31 + #
  32 + # Rails 2 - you'll need to vendor airbrake to get the rake tasks
  33 + # rake gems:unpack GEM=airbrake
  34 + #
  35 + # Run:
  36 + # rake airbrake:test
  37 + # refresh this page
38 38  
... ...
app/views/apps/_fields.html.haml
... ... @@ -5,8 +5,14 @@
5 5 = f.text_field :name
6 6  
7 7 %div
  8 + = f.label :repository_branch
  9 + = f.text_field :repository_branch, :placeholder => "master"
  10 +%div
8 11 = f.label :github_repo
9 12 = f.text_field :github_repo, :placeholder => "errbit/errbit from https://github.com/errbit/errbit"
  13 +%div
  14 + = f.label :bitbucket_repo
  15 + = f.text_field :bitbucket_repo, :placeholder => "errbit/errbit from https://bitbucket.org/errbit/errbit"
10 16  
11 17 %fieldset
12 18 %legend Notifications
... ... @@ -46,4 +52,5 @@
46 52 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy'
47 53  
48 54 = render "issue_tracker_fields", :f => f
  55 += render "service_notification_fields", :f => f
49 56  
... ...
app/views/apps/_service_notification_fields.html.haml 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +%fieldset
  2 + %legend Notification Service
  3 + = f.fields_for :notification_service do |w|
  4 + %div.notification_service.nested
  5 + %div.choose
  6 + = label_tag :type_none, :for => label_for_attr(w, 'type_notificationservice'), :class => "label_radio none" do
  7 + = w.radio_button :type, "NotificationService", 'data-section' => 'none'
  8 + (None)
  9 + - NotificationService.subclasses.each do |notification_service|
  10 + = label_tag "type_#{notification_service.label}:", :for => label_for_attr(w, "type_#{notification_service.name.downcase.gsub(':','')}"), :class => "label_radio #{notification_service.label}" do
  11 + = w.radio_button :type, notification_service.name, 'data-section' => notification_service.label
  12 + = notification_service.name[/::(.*)Service/,1].titleize
  13 +
  14 + %div.notification_params.none{:class => (w.object && !(w.object.class < NotificationService)) ? 'chosen' : nil}
  15 + - NotificationService.subclasses.each do |notification_service|
  16 + %div.notification_params{:class => (w.object.is_a?(notification_service) ? 'chosen ' : '') << notification_service.label}
  17 + - notification_service::Fields.each do |field, field_info|
  18 + = w.label field, field_info[:label] || field.to_s.titleize
  19 + - field_type = field == :password ? :password_field : :text_field
  20 + = w.send field_type, field, :placeholder => field_info[:placeholder], :value => w.object.send(field)
  21 +
  22 + .image_preloader
  23 + - (NotificationService.subclasses.map{|t| t.label } << 'none').each do |notification_service|
  24 + = image_tag "#{notification_service}_inactive.png"
  25 + = image_tag "#{notification_service}_create.png"
... ...
app/views/apps/index.html.haml
... ... @@ -6,8 +6,10 @@
6 6 %thead
7 7 %tr
8 8 %th Name
9   - - if any_github_repos?
10   - %th GitHub Repo
  9 + - if any_github_repos? || any_bitbucket_repos?
  10 + %th Repository
  11 + - if any_notification_services?
  12 + %th Notification Service
11 13 - if any_issue_trackers?
12 14 %th Tracker
13 15 - if any_deploys?
... ... @@ -17,10 +19,20 @@
17 19 - @apps.each do |app|
18 20 %tr
19 21 %td.name= link_to app.name, app_path(app)
20   - - if any_github_repos?
  22 + - if any_github_repos? or any_bitbucket_repos?
21 23 %td.github_repo
22 24 - if app.github_repo?
23 25 = link_to(app.github_repo, app.github_url, :target => '_blank')
  26 + - if app.bitbucket_repo?
  27 + = link_to(app.bitbucket_repo, app.bitbucket_url, :target => '_blank')
  28 + - if any_notification_services?
  29 + %td.notification_service
  30 + - if app.notification_service_configured?
  31 + - notification_service_img = image_tag("#{app.notification_service.label}_goto.png")
  32 + - if app.notification_service.url
  33 + = link_to( notification_service_img, app.notification_service.url, :target => "_blank" )
  34 + - else
  35 + = notification_service_img
24 36 - if any_issue_trackers?
25 37 %td.issue_tracker
26 38 - if app.issue_tracker_configured?
... ...
app/views/apps/show.atom.builder
1 1 atom_feed do |feed|
2 2 feed.title("Errbit notices for #{h @app.name} at #{root_url}")
3   - render "errs/list", :feed => feed
  3 + render "problems/list", :feed => feed
4 4 end
... ...
app/views/apps/show.html.haml
... ... @@ -12,10 +12,10 @@
12 12 - if current_user.admin?
13 13 = link_to 'edit', edit_app_path(@app), :class => 'button'
14 14 = link_to 'destroy', app_path(@app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
15   - - if @all_errs
16   - = link_to 'unresolved errs', app_path(@app), :class => 'button'
17   - - else
18   - = link_to 'all errs', app_path(@app, :all_errs => true), :class => 'button'
  15 + - if @all_errs
  16 + = link_to 'unresolved errs', app_path(@app), :class => 'button'
  17 + - else
  18 + = link_to 'all errs', app_path(@app, :all_errs => true), :class => 'button'
19 19 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?'
20 20 %h3#watchers_toggle
21 21 Watchers
... ... @@ -83,7 +83,7 @@
83 83  
84 84 - if @app.problems.any?
85 85 %h3.clear Errors
86   - = render 'errs/table', :errs => @problems
  86 + = render 'problems/table', :problems => @problems
87 87 - else
88 88 %h3.clear No errs have been caught yet, make sure you setup your app
89 89 = render 'configuration_instructions', :app => @app
... ...
app/views/errs/_issue_tracker_links.html.haml
... ... @@ -1,15 +0,0 @@
1   -- if @app.issue_tracker_configured? || current_user.github_account?
2   - - if @problem.issue_link.present?
3   - %span= link_to 'go to issue', @problem.issue_link, :class => "#{@problem.issue_type}_goto goto-issue"
4   - = link_to 'unlink issue', unlink_issue_app_err_path(@app, @problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue"
5   - - elsif @problem.issue_link == "pending"
6   - %span.disabled= link_to 'creating...', '#', :class => "#{@problem.issue_type}_inactive create-issue"
7   - = link_to 'retry', create_issue_app_err_path(@app, @problem), :method => :post
8   - - else
9   - - if @app.github_repo?
10   - - if current_user.can_create_github_issues?
11   - %span= link_to 'create issue', create_issue_app_err_path(@app, @problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue"
12   - - elsif @app.issue_tracker_configured? && @app.issue_tracker.is_a?(GithubIssuesTracker)
13   - %span= link_to 'create issue', create_issue_app_err_path(@app, @problem), :method => :post, :class => "github_create create-issue"
14   - - if @app.issue_tracker_configured? && !@app.issue_tracker.is_a?(GithubIssuesTracker)
15   - %span= link_to 'create issue', create_issue_app_err_path(@app, @problem), :method => :post, :class => "#{@app.issue_tracker.label}_create create-issue"
app/views/errs/_list.atom.builder
... ... @@ -1,15 +0,0 @@
1   -feed.updated(@problems.first.try(:created_at) || Time.now)
2   -
3   -for problem in @problems
4   - notice = problem.notices.first
5   -
6   - feed.entry(problem, :url => app_err_url(problem.app, problem)) do |entry|
7   - entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}"
8   - entry.author do |author|
9   - author.name "#{ problem.app.name } [#{ problem.environment }]"
10   - end
11   - if notice
12   - entry.summary(notice_atom_summary(notice), :type => "html")
13   - end
14   - end
15   -end
app/views/errs/_table.html.haml
... ... @@ -1,56 +0,0 @@
1   -- any_issue_links = errs.any?{|e| e.issue_link.present? && e.issue_link != 'pending' }
2   -=form_tag do
3   - %table.errs.selectable
4   - %thead
5   - %tr
6   - %th= check_box_tag "toggle_problems_checkboxes"
7   - %th= link_for_sort "App"
8   - %th= link_for_sort "What &amp; Where".html_safe, "message"
9   - %th= link_for_sort "Latest", "last_notice_at"
10   - %th= link_for_sort "Deploy", "last_deploy_at"
11   - %th= link_for_sort "Count"
12   - - if any_issue_links
13   - %th Issue
14   - %th Resolve
15   - %tbody
16   - - errs.each do |problem|
17   - %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'}
18   - %td.select
19   - = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s)
20   - %td.app
21   - = link_to problem.app.name, app_path(problem.app)
22   - - if current_page?(:controller => 'errs')
23   - %span.environment= link_to problem.environment, errs_path(:environment => problem.environment)
24   - - else
25   - %span.environment= link_to problem.environment, app_path(problem.app, :environment => problem.environment)
26   - %td.message
27   - = link_to truncated_err_message(problem), app_err_path(problem.app, problem)
28   - %em= problem.where
29   - - if problem.comments_count > 0
30   - - comment = problem.comments.last
31   - %br
32   - .inline_comment
33   - - if comment.user
34   - %em.commenter= (Errbit::Config.user_has_username ? comment.user.username : comment.user.email).to_s << ":"
35   - %em= truncate(comment.body, :length => 100, :separator => ' ')
36   - %td.latest #{time_ago_in_words(last_notice_at problem)} ago
37   - %td.deploy= problem.last_deploy_at ? problem.last_deploy_at.to_s(:micro) : 'n/a'
38   - %td.count= link_to problem.notices_count, app_err_path(problem.app, problem)
39   - - if any_issue_links
40   - %td.issue_link
41   - - if problem.app.issue_tracker_configured? && problem.issue_link.present? && problem.issue_link != 'pending'
42   - = link_to image_tag("#{problem.issue_type}_goto.png"), problem.issue_link, :target => "_blank"
43   - %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_err_path(problem.app, problem), :title => "Resolve", :method => :put, :data => { :confirm => err_confirm }, :class => 'resolve' if problem.unresolved?
44   - - if errs.none?
45   - %tr
46   - %td{:colspan => (any_issue_links ? 8 : 7)}
47   - %em No errs here
48   - = paginate errs
49   - .tab-bar
50   - %ul
51   - %li= submit_tag 'Merge', :id => 'merge_errs', :class => 'button', 'data-action' => merge_several_errs_path
52   - %li= submit_tag 'Unmerge', :id => 'unmerge_errs', :class => 'button', 'data-action' => unmerge_several_errs_path
53   - %li= submit_tag 'Resolve', :id => 'resolve_errs', :class => 'button', 'data-action' => resolve_several_errs_path
54   - %li= submit_tag 'Unresolve', :id => 'unresolve_errs', :class => 'button', 'data-action' => unresolve_several_errs_path
55   - %li= submit_tag 'Delete', :id => 'delete_errs', :class => 'button', 'data-action' => destroy_several_errs_path
56   -
app/views/errs/_tally_table.html.haml
... ... @@ -1,5 +0,0 @@
1   -%table.tally
2   - - rows.each do |row|
3   - %tr
4   - %td.percent= number_to_percentage(row[0], :precision => 1)
5   - %th.value= row[1]
app/views/errs/all.html.haml
... ... @@ -1,4 +0,0 @@
1   -- content_for :title, 'All Errors'
2   -- content_for :action_bar do
3   - = link_to 'hide resolved', errs_path, :class => 'button'
4   -= render 'table', :errs => @problems
app/views/errs/index.atom.builder
... ... @@ -1,4 +0,0 @@
1   -atom_feed do |feed|
2   - feed.title("Errbit notices at #{root_url}")
3   - render "errs/list", :feed => feed
4   -end
app/views/errs/index.html.haml
... ... @@ -1,6 +0,0 @@
1   -- content_for :title, 'Unresolved Errors'
2   -- content_for :head do
3   - = auto_discovery_link_tag :atom, errs_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}"
4   -- content_for :action_bar do
5   - = link_to 'show resolved', all_errs_path, :class => 'button'
6   -= render 'table', :errs => @problems
app/views/errs/show.html.haml
... ... @@ -1,80 +0,0 @@
1   -- content_for :page_title, @problem.message
2   -- content_for :title_css_class, 'err_show'
3   -- content_for :title, @problem.error_class || truncate(@problem.message, :length => 32)
4   -- content_for :meta do
5   - %strong App:
6   - = link_to @app.name, app_path(@app)
7   - %strong Where:
8   - = @problem.where
9   - %br
10   - %strong Environment:
11   - = @problem.environment
12   - %strong Last Notice:
13   - = last_notice_at(@problem).to_s(:precise)
14   -- content_for :action_bar do
15   - - if @problem.unresolved?
16   - %span= link_to 'resolve', resolve_app_err_path(@app, @problem), :method => :put, :data => { :confirm => err_confirm }, :class => 'resolve'
17   - - if current_user.authentication_token
18   - %span= link_to 'iCal', app_err_path(:app_id => @app.id, :id => @problem.id, :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
19   - %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_errs_path(@app)), :class => 'up'
20   - %br
21   - = render "issue_tracker_links"
22   -
23   -- if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @problem.comments.any?
24   - - content_for :comments do
25   - %h3 Comments
26   - - @problem.comments.each do |comment|
27   - .window
28   - %table.comment
29   - %tr
30   - %th
31   - %span= link_to '&#10008;'.html_safe, app_err_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
32   - = time_ago_in_words(comment.created_at, true) << " ago by "
33   - = link_to comment.user.email, user_path(comment.user)
34   - %tr
35   - %td= comment.body.gsub("\n", "<br>").html_safe
36   - - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured?
37   - = form_for @comment, :url => app_err_comments_path(@app, @problem) do |comment_form|
38   - %p Add a comment
39   - = comment_form.text_area :body, :style => "width: 420px; height: 80px;"
40   - = comment_form.submit "Save Comment"
41   -
42   -%h4= @notice.try(:message)
43   -
44   -= paginate @notices, :param_name => :notice, :theme => :notices
45   -
46   -.tab-bar
47   - %ul
48   - %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button'
49   - %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button'
50   - - if @notice && @notice.user_attributes.present?
51   - %li= link_to 'User Details', '#user_attributes', :rel => 'user_attributes', :class => 'button'
52   - %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button'
53   - %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button'
54   - %li= link_to 'Session', '#session', :rel => 'session', :class => 'button'
55   -
56   -- if @notice
57   - #summary
58   - %h3 Summary
59   - = render 'notices/summary', :notice => @notice, :problem => @problem
60   -
61   - #backtrace
62   - %h3 Backtrace
63   - = render 'notices/backtrace', :lines => @notice.backtrace
64   -
65   - - if @notice.user_attributes.present?
66   - #user_attributes
67   - %h3 User Details
68   - = render 'notices/user_attributes', :user => @notice.user_attributes
69   -
70   - #environment
71   - %h3 Environment
72   - = render 'notices/environment', :notice => @notice
73   -
74   - #params
75   - %h3 Parameters
76   - = render 'notices/params', :notice => @notice
77   -
78   - #session
79   - %h3 Session
80   - = render 'notices/session', :notice => @notice
app/views/errs/show.ics.haml
... ... @@ -1 +0,0 @@
1   -= generate_problem_ical(@problem.notices.order_by(:created_at.asc))
app/views/issue_trackers/bitbucket_issues_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +[[<%= app_problem_url problem.app, problem %>| [See this exception on Errbit]]]
  2 +
  3 +----
  4 +
  5 +<% if notice = problem.notices.first %>
  6 + <%= notice.message %>
  7 +
  8 +----
  9 +
  10 + == Summary ==
  11 + <% if notice.request['url'].present? %>
  12 + === URL ===
  13 + [[<%= notice.request['url'] %>]]
  14 + <% end %>
  15 +
  16 +----
  17 +
  18 + === Where ===
  19 + <%= notice.where %>
  20 +
  21 +----
  22 +
  23 + === Occured ===
  24 + <%= notice.created_at.to_s(:micro) %>
  25 +
  26 +----
  27 +
  28 + === Similar ===
  29 + <%= (notice.problem.notices_count - 1).to_s %>
  30 +
  31 +----
  32 +
  33 + == Params ==
  34 +{{{
  35 +<%= pretty_hash(notice.params) %>
  36 +}}}
  37 +
  38 +----
  39 +
  40 + == Session ==
  41 +{{{
  42 +<%= pretty_hash(notice.session) %>
  43 +}}}
  44 +
  45 +----
  46 +
  47 + == Backtrace ==
  48 + <% notice.backtrace_lines.each do |line| %>| <%= line['number'] %>: | <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>** |
  49 + <% end %>
  50 +
  51 +----
  52 +
  53 + == Environment ==
  54 + <% for key, val in notice.env_vars %>
  55 + | <%= key %>: | <%= val %> |
  56 + <% end %>
  57 +<% end %>
  58 +
... ...
app/views/issue_trackers/fogbugz_body.txt.erb
1   -"See this exception on Errbit": <%= app_err_url(problem.app, problem) %>
  1 +"See this exception on Errbit": <%= app_problem_url(problem.app, problem) %>
2 2 <% if notice = problem.notices.first %>
3 3 <%= notice.message %>
4 4  
... ... @@ -19,8 +19,8 @@
19 19 <%= pretty_hash(notice.session) %>
20 20  
21 21 Backtrace
22   - <% for line in notice.backtrace %>
23   - <%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %>
  22 + <% notice.backtrace_lines.each do |line| %>
  23 + <%= line.number %>: <%= line.file_relative %>
24 24 <% end %>
25 25  
26 26 Environment
... ...
app/views/issue_trackers/github_issues_body.txt.erb
1   -[See this exception on Errbit](<%= app_err_url problem.app, problem %> "See this exception on Errbit")
  1 +[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2 2 <% if notice = problem.notices.first %>
3 3 # <%= notice.message %> #
4 4 ## Summary ##
... ... @@ -27,7 +27,7 @@
27 27  
28 28 ## Backtrace ##
29 29 ```
30   -<% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>**
  30 +<% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
31 31 <% end %>
32 32 ```
33 33  
... ...
app/views/issue_trackers/lighthouseapp_body.txt.erb
1   -[See this exception on Errbit](<%= app_err_url problem.app, problem %> "See this exception on Errbit")
  1 +[See this exception on Errbit](<%= app_problem_url problem.app, problem %> "See this exception on Errbit")
2 2 <% if notice = problem.notices.first %>
3 3 # <%= notice.message %> #
4 4 ## Summary ##
... ... @@ -23,7 +23,7 @@
23 23  
24 24 ## Backtrace ##
25 25 <code>
26   - <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>**
  26 + <% notice.backtrace_lines.each do |line| %><%= line.number %>: <%= line.file_relative %> -> **<%= line.method %>**
27 27 <% end %>
28 28 </code>
29 29  
... ...
app/views/issue_trackers/pivotal_body.txt.erb
1   -See this exception on Errbit: <%= app_err_url problem.app, problem %>
  1 +See this exception on Errbit: <%= app_problem_url problem.app, problem %>
2 2 <% if notice = problem.notices.first %>
3 3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
4 4 Where: <%= notice.where %>
... ... @@ -12,6 +12,6 @@ See this exception on Errbit: &lt;%= app_err_url problem.app, problem %&gt;
12 12 <%= pretty_hash notice.session %>
13 13  
14 14 Backtrace:
15   - <%= notice.backtrace[0..4].map { |line| "#{line['number']}: #{line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %>
  15 + <%= notice.backtrace_lines[0..4].map { |line| "#{line.number}: #{line.file_relative} -> *#{line.method}*" }.join "\n" %>
16 16 <% end %>
17 17  
... ...
app/views/issue_trackers/textile_body.txt.erb
1 1 <% if notice = problem.notices.first %>
2 2 h1. <%= notice.message %>
3 3  
4   -h3. "See this exception on Errbit":<%= app_err_url problem.app, problem %>
  4 +h3. "See this exception on Errbit":<%= app_problem_url problem.app, problem %>
5 5  
6 6 h2. Summary
7 7 <% if notice.request['url'].present? %>
... ... @@ -32,7 +32,7 @@ h2. Session
32 32 h2. Backtrace
33 33  
34 34 | Line | File | Method |
35   -<% for line in notice.backtrace %>| <%= line['number'] %> | <%= line['file'].to_s.sub(/^\[PROJECT_ROOT\]/, '') %> | *<%= line['method'] %>* |
  35 +<% notice.backtrace_lines.each do |line| %>| <%= line.number %> | <%= line.file_relative %> | *<%= line.method %>* |
36 36 <% end %>
37 37  
38 38 h2. Environment
... ...
app/views/kaminari/_first_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the first page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.first
... ...
app/views/kaminari/_gap.html.haml
1 1 -# Non-link tag that stands for skipped pages...
2 2 -# available local variables
3 3 -# current_page: a page object for the currently displayed page
4   --# num_pages: total number of pages
  4 +-# total_pages: total number of pages
5 5 -# per_page: number of items to fetch per page
6 6 -# remote: data-remote
7 7 %span.page.gap
... ...
app/views/kaminari/_last_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the last page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.last
... ...
app/views/kaminari/_next_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the next page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.next
... ...
app/views/kaminari/_page.html.haml
... ... @@ -3,7 +3,7 @@
3 3 -# page: a page object for "this" page
4 4 -# url: url to this page
5 5 -# current_page: a page object for the currently displayed page
6   --# num_pages: total number of pages
  6 +-# total_pages: total number of pages
7 7 -# per_page: number of items to fetch per page
8 8 -# remote: data-remote
9 9 %span{:class => "page#{' current' if page.current?}"}
... ...
app/views/kaminari/_paginator.html.haml
1 1 -# The container tag
2 2 -# available local variables
3 3 -# current_page: a page object for the currently displayed page
4   --# num_pages: total number of pages
  4 +-# total_pages: total number of pages
5 5 -# per_page: number of items to fetch per page
6 6 -# remote: data-remote
7 7 -# paginator: the paginator that renders the pagination tags inside
... ...
app/views/kaminari/_prev_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the previous page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.prev
... ...
app/views/kaminari/notices/_first_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the first page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.first
... ...
app/views/kaminari/notices/_gap.html.haml
1 1 -# Non-link tag that stands for skipped pages...
2 2 -# available local variables
3 3 -# current_page: a page object for the currently displayed page
4   --# num_pages: total number of pages
  4 +-# total_pages: total number of pages
5 5 -# per_page: number of items to fetch per page
6 6 -# remote: data-remote
7 7 %span.page.gap
... ...
app/views/kaminari/notices/_last_page.html.haml
... ... @@ -2,7 +2,7 @@
2 2 -# available local variables
3 3 -# url: url to the last page
4 4 -# current_page: a page object for the currently displayed page
5   --# num_pages: total number of pages
  5 +-# total_pages: total number of pages
6 6 -# per_page: number of items to fetch per page
7 7 -# remote: data-remote
8 8 %span.last
... ...
app/views/kaminari/notices/_page.html.haml
... ... @@ -3,7 +3,7 @@
3 3 -# page: a page object for "this" page
4 4 -# url: url to this page
5 5 -# current_page: a page object for the currently displayed page
6   --# num_pages: total number of pages
  6 +-# total_pages: total number of pages
7 7 -# per_page: number of items to fetch per page
8 8 -# remote: data-remote
9 9 %span{:class => "page#{' current' if page.current?}"}
... ...
app/views/kaminari/notices/_paginator.html.haml
1 1 -# The container tag
2 2 -# available local variables
3 3 -# current_page: a page object for the currently displayed page
4   --# num_pages: total number of pages
  4 +-# total_pages: total number of pages
5 5 -# per_page: number of items to fetch per page
6 6 -# remote: data-remote
7 7 -# paginator: the paginator that renders the pagination tags inside
... ... @@ -11,4 +11,4 @@
11 11 |&nbsp;
12 12 = prev_page_tag
13 13 .notice-pagination-loader= image_tag 'loader.gif'
14   -viewing occurrence #{page_count_from_end(current_page, num_pages)} of #{num_pages}
  14 +viewing occurrence #{page_count_from_end(current_page, total_pages)} of #{total_pages}
... ...
app/views/layouts/application.html.haml
... ... @@ -18,7 +18,7 @@
18 18 = render 'shared/navigation' if current_user
19 19 = render 'shared/session'
20 20 #content-wrapper
21   - #content-title{ :class => (yield :title_css_class).to_s }
  21 + #content-title{ :class => (yield :title_css_class).to_s, :style => (yield :title_style) }
22 22 %h1= yield :title
23 23 %span.meta= yield :meta
24 24 - if (action_bar = yield(:action_bar)).present?
... ... @@ -33,3 +33,4 @@
33 33 #footer= "Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.".html_safe
34 34 = yield :scripts
35 35  
  36 += yield :before_title
36 37 \ No newline at end of file
... ...
app/views/mailer/err_notification.html.haml
... ... @@ -14,7 +14,7 @@
14 14 %br
15 15 This err has occurred #{pluralize @notice.problem.notices_count, 'time'}.
16 16 %p
17   - = link_to("Click here to view the error on Errbit", app_err_url(@app, @notice.problem), :class => "bold") << "."
  17 + = link_to("Click here to view the error on Errbit", app_problem_url(@app, @notice.problem), :class => "bold") << "."
18 18 %tr
19 19 %td.section
20 20 %table(cellpadding="0" cellspacing="0" border="0" align="left")
... ... @@ -27,16 +27,15 @@
27 27 %p.heading WHERE:
28 28 %p.monospace
29 29 = @notice.where
30   - - if (app_backtrace = @notice.app_backtrace).any?
31   - - app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line|
32   - %p.backtrace= line
  30 + - @notice.in_app_backtrace_lines.each do |line|
  31 + %p.backtrace= line
33 32 %br
34 33 %p.heading URL:
35 34 %p.monospace
36 35 - if @notice.request['url'].present?
37 36 = link_to @notice.request['url'], @notice.request['url']
38 37 %p.heading BACKTRACE:
39   - - @notice.backtrace.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line|
  38 + - @notice.backtrace_lines.each do |line|
40 39 %p.backtrace= line
41 40 %br
42 41  
... ...
app/views/mailer/err_notification.text.erb
... ... @@ -2,7 +2,7 @@ An err has just occurred in &lt;%= @notice.environment_name %&gt;: &lt;%= raw(@notice.mes
2 2  
3 3 This err has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here:
4 4  
5   - <%= app_err_url(@app, @notice.problem) %>
  5 + <%= app_problem_url(@app, @notice.problem) %>
6 6  
7 7  
8 8 ERROR MESSAGE:
... ... @@ -14,7 +14,7 @@ WHERE:
14 14  
15 15 <%= @notice.where %>
16 16  
17   -<% @notice.app_backtrace.map {|l| "#{l['file']}:#{l['number']}" }.each do |line| %>
  17 +<% @notice.in_app_backtrace_lines.each do |line| %>
18 18 <%= line %>
19 19 <% end %>
20 20  
... ... @@ -26,7 +26,7 @@ URL:
26 26  
27 27 BACKTRACE:
28 28  
29   -<% @notice.backtrace.map {|l| l ? "#{l['file']}:#{l['number']}" : nil }.compact.each do |line| %>
  29 +<% @notice.backtrace_lines.each do |line| %>
30 30 <%= line %>
31 31 <% end %>
32 32  
... ...
app/views/notices/_atom_entry.html.haml
... ... @@ -22,13 +22,13 @@
22 22  
23 23 %h3 Backtrace
24 24 %table
25   - - for line in notice.backtrace
  25 + - for line in notice.backtrace_lines
26 26 %tr
27 27 %td
28   - = "#{line['number']}:"
  28 + = "#{line.number}:"
29 29 &nbsp;&nbsp;
30 30 %td
31   - = raw "#{h line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> #{content_tag :strong, h(line['method'])}"
  31 + = raw "#{h line.file_relative} -> #{content_tag :strong, h(line.method)}"
32 32  
33 33 %h3 Environment
34 34 %table
... ...
app/views/notices/_backtrace_line.html.haml
1   -%tr{:class => defined?(row_class) ? row_class : nil}
2   - %td.line{:class => (in_app_backtrace_line?(line) ? 'in-app' : nil)}
3   - = link_to_source_file(@app, line) do
4   - %span.path>= path_for_backtrace_line(line)
5   - %span.file>= file_for_backtrace_line(line)
6   - - if line['number'].present?
7   - %span.number>= ":#{line['number']}"
  1 +%tr{:class => defined?(row_class) && row_class}
  2 + %td.line{:class => line.in_app? && 'in-app' }
  3 + = link_to_source_file(line) do
  4 + %span.path>=raw line.decorated_path
  5 + %span.file>= line.file_name
  6 + - if line.number.present?
  7 + %span.number>= ":#{line.number}"
8 8 &rarr;
9   - %span.method= line['method']
  9 + %span.method= line.method
... ...
app/views/problems/_issue_tracker_links.html.haml 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +- if @app.issue_tracker_configured? || current_user.github_account?
  2 + - if @problem.issue_link.present?
  3 + %span= link_to 'go to issue', @problem.issue_link, :class => "#{@problem.issue_type}_goto goto-issue"
  4 + = link_to 'unlink issue', unlink_issue_app_problem_path(@app, @problem), :method => :delete, :data => { :confirm => "Unlink err issues?" }, :class => "unlink-issue"
  5 + - elsif @problem.issue_link == "pending"
  6 + %span.disabled= link_to 'creating...', '#', :class => "#{@problem.issue_type}_inactive create-issue"
  7 + = link_to 'retry', create_issue_app_problem_path(@app, @problem), :method => :post
  8 + - else
  9 + - if @app.github_repo?
  10 + - if current_user.can_create_github_issues?
  11 + %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem, :tracker => 'user_github'), :method => :post, :class => "github_create create-issue"
  12 + - elsif @app.issue_tracker_configured? && @app.issue_tracker.is_a?(GithubIssuesTracker)
  13 + %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "github_create create-issue"
  14 + - if @app.issue_tracker_configured? && !@app.issue_tracker.is_a?(GithubIssuesTracker)
  15 + %span= link_to 'create issue', create_issue_app_problem_path(@app, @problem), :method => :post, :class => "#{@app.issue_tracker.label}_create create-issue"
... ...
app/views/problems/_list.atom.builder 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +feed.updated(@problems.first.try(:created_at) || Time.now)
  2 +
  3 +for problem in @problems
  4 + notice = problem.notices.first
  5 +
  6 + feed.entry(problem, :url => app_problem_url(problem.app, problem)) do |entry|
  7 + entry.title "[#{ problem.where }] #{problem.message.to_s.truncate(27)}"
  8 + entry.author do |author|
  9 + author.name "#{ problem.app.name } [#{ problem.environment }]"
  10 + end
  11 + if notice
  12 + entry.summary(notice_atom_summary(notice), :type => "html")
  13 + end
  14 + end
  15 +end
... ...
app/views/problems/_table.html.haml 0 → 100644
... ... @@ -0,0 +1,56 @@
  1 +- any_issue_links = problems.any?{|e| e.issue_link.present? && e.issue_link != 'pending' }
  2 +=form_tag do
  3 + %table.errs.selectable
  4 + %thead
  5 + %tr
  6 + %th= check_box_tag "toggle_problems_checkboxes"
  7 + %th= link_for_sort "App"
  8 + %th= link_for_sort "What &amp; Where".html_safe, "message"
  9 + %th= link_for_sort "Latest", "last_notice_at"
  10 + %th= link_for_sort "Deploy", "last_deploy_at"
  11 + %th= link_for_sort "Count"
  12 + - if any_issue_links
  13 + %th Issue
  14 + %th Resolve
  15 + %tbody
  16 + - problems.each do |problem|
  17 + %tr{:class => problem.resolved? ? 'resolved' : 'unresolved'}
  18 + %td.select
  19 + = check_box_tag "problems[]", problem.id, @selected_problems.member?(problem.id.to_s)
  20 + %td.app
  21 + = link_to problem.app.name, app_path(problem.app)
  22 + - if current_page?(:controller => 'problems')
  23 + %span.environment= link_to problem.environment, problems_path(:environment => problem.environment)
  24 + - else
  25 + %span.environment= link_to problem.environment, app_path(problem.app, :environment => problem.environment)
  26 + %td.message
  27 + = link_to truncated_problem_message(problem), app_problem_path(problem.app, problem)
  28 + %em= problem.where
  29 + - if problem.comments_count > 0
  30 + - comment = problem.comments.last
  31 + %br
  32 + .inline_comment
  33 + - if comment.user
  34 + %em.commenter= (Errbit::Config.user_has_username ? comment.user.username : comment.user.email).to_s << ":"
  35 + %em= truncate(comment.body, :length => 100, :separator => ' ')
  36 + %td.latest #{time_ago_in_words(problem.last_notice_at)} ago
  37 + %td.deploy= problem.last_deploy_at ? problem.last_deploy_at.to_s(:micro) : 'n/a'
  38 + %td.count= link_to problem.notices_count, app_problem_path(problem.app, problem)
  39 + - if any_issue_links
  40 + %td.issue_link
  41 + - if problem.app.issue_tracker_configured? && problem.issue_link.present? && problem.issue_link != 'pending'
  42 + = link_to image_tag("#{problem.issue_type}_goto.png"), problem.issue_link, :target => "_blank"
  43 + %td.resolve= link_to image_tag("thumbs-up.png"), resolve_app_problem_path(problem.app, problem), :title => "Resolve", :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve' if problem.unresolved?
  44 + - if problems.none?
  45 + %tr
  46 + %td{:colspan => (any_issue_links ? 8 : 7)}
  47 + %em No errs here
  48 + = paginate problems
  49 + .tab-bar
  50 + %ul
  51 + %li= submit_tag 'Merge', :id => 'merge_problems', :class => 'button', 'data-action' => merge_several_problems_path
  52 + %li= submit_tag 'Unmerge', :id => 'unmerge_problems', :class => 'button', 'data-action' => unmerge_several_problems_path
  53 + %li= submit_tag 'Resolve', :id => 'resolve_problems', :class => 'button', 'data-action' => resolve_several_problems_path
  54 + %li= submit_tag 'Unresolve', :id => 'unresolve_problems', :class => 'button', 'data-action' => unresolve_several_problems_path
  55 + %li= submit_tag 'Delete', :id => 'delete_problems', :class => 'button', 'data-action' => destroy_several_problems_path
  56 +
... ...
app/views/problems/_tally_table.html.haml 0 → 100644
... ... @@ -0,0 +1,18 @@
  1 +.head_and_tail
  2 + %table.tally.head
  3 + %tbody
  4 + - head(rows).each do |row|
  5 + %tr
  6 + %td.percent= number_to_percentage(row[0], :precision => 1)
  7 + %th.value= row[1]
  8 + - if rows.size > head_size
  9 + %tfoot
  10 + %tr
  11 + %td{:colspan => 2}
  12 + = link_to 'Show more...', '#', :class => :show_tail
  13 + %table.tally.tail{:style => "display: none"}
  14 + %tbody
  15 + - tail(rows).each do |row|
  16 + %tr
  17 + %td.percent= number_to_percentage(row[0], :precision => 1)
  18 + %th.value= row[1]
... ...
app/views/problems/all.html.haml 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +- content_for :title, 'All Errors'
  2 +- content_for :action_bar do
  3 + = link_to 'hide resolved', problems_path, :class => 'button'
  4 += render 'table', :problems => @problems
... ...
app/views/problems/index.atom.builder 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +atom_feed do |feed|
  2 + feed.title("Errbit notices at #{root_url}")
  3 + render "problems/list", :feed => feed
  4 +end
... ...
app/views/problems/index.html.haml 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +- content_for :title, 'Unresolved Errors'
  2 +- content_for :head do
  3 + = auto_discovery_link_tag :atom, problems_path(User.token_authentication_key => current_user.authentication_token, :format => "atom"), :title => "Errbit notices at #{request.host}"
  4 +- content_for :action_bar do
  5 + = link_to 'show resolved', all_problems_path, :class => 'button'
  6 += render 'table', :problems => @problems
... ...
app/views/problems/show.html.haml 0 → 100644
... ... @@ -0,0 +1,88 @@
  1 +- content_for :page_title, @problem.message
  2 +- content_for :title_css_class, 'err_show'
  3 +- content_for :title, @problem.error_class || truncate(@problem.message, :length => 32)
  4 +- content_for :meta do
  5 + %strong App:
  6 + = link_to @app.name, app_path(@app)
  7 + %strong Where:
  8 + = @problem.where
  9 + %br
  10 + %strong Environment:
  11 + = @problem.environment
  12 + %strong Last Notice:
  13 + = @problem.last_notice_at.to_s(:precise)
  14 +- content_for :action_bar do
  15 + - if @problem.unresolved?
  16 + %span= link_to 'resolve', resolve_app_problem_path(@app, @problem), :method => :put, :data => { :confirm => problem_confirm }, :class => 'resolve'
  17 + - if current_user.authentication_token
  18 + %span= link_to 'iCal', app_problem_path(:app_id => @app.id, :id => @problem.id, :format => "ics", :auth_token => current_user.authentication_token), :class => "calendar_link"
  19 + %span>= link_to 'up', (request.env['HTTP_REFERER'] ? :back : app_problems_path(@app)), :class => 'up'
  20 + %br
  21 + = render "issue_tracker_links"
  22 +
  23 +- if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured? || @problem.comments.any?
  24 + - content_for :comments do
  25 + %h3 Comments
  26 + - @problem.comments.each do |comment|
  27 + .window
  28 + %table.comment
  29 + %tr
  30 + %th
  31 + - if comment.user
  32 + - if Errbit::Config.use_gravatar
  33 + = gravatar_tag comment.user.email, :s => 24
  34 + %span.comment-info
  35 + = time_ago_in_words(comment.created_at, true) << " ago by "
  36 + = link_to comment.user.email, user_path(comment.user)
  37 + %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
  38 + - else
  39 + %span.comment-info
  40 + = time_ago_in_words(comment.created_at, true) << " ago by [Unknown User]"
  41 + %span.delete= link_to '&#10008;'.html_safe, app_problem_comment_path(@app, @problem, comment), :method => :delete, :data => { :confirm => "Are sure you don't need this comment?" }, :class => "destroy-comment"
  42 + %tr
  43 + %td= comment.body.gsub("\n", "<br>").html_safe
  44 + - if Errbit::Config.allow_comments_with_issue_tracker || !@app.issue_tracker_configured?
  45 + = form_for @comment, :url => app_problem_comments_path(@app, @problem) do |comment_form|
  46 + %p Add a comment
  47 + = comment_form.text_area :body, :style => "width: 420px; height: 80px;"
  48 + = comment_form.submit "Save Comment"
  49 +
  50 +%h4= @notice.try(:message)
  51 +
  52 += paginate @notices, :param_name => :notice, :theme => :notices
  53 +
  54 +.tab-bar
  55 + %ul
  56 + %li= link_to 'Summary', '#summary', :rel => 'summary', :class => 'button'
  57 + %li= link_to 'Backtrace', '#backtrace', :rel => 'backtrace', :class => 'button'
  58 + - if @notice && @notice.user_attributes.present?
  59 + %li= link_to 'User Details', '#user_attributes', :rel => 'user_attributes', :class => 'button'
  60 + %li= link_to 'Environment', '#environment', :rel => 'environment', :class => 'button'
  61 + %li= link_to 'Parameters', '#params', :rel => 'params', :class => 'button'
  62 + %li= link_to 'Session', '#session', :rel => 'session', :class => 'button'
  63 +
  64 +- if @notice
  65 + #summary
  66 + %h3 Summary
  67 + = render 'notices/summary', :notice => @notice, :problem => @problem
  68 +
  69 + #backtrace
  70 + %h3 Backtrace
  71 + = render 'notices/backtrace', :lines => @notice.backtrace_lines
  72 +
  73 + - if @notice.user_attributes.present?
  74 + #user_attributes
  75 + %h3 User Details
  76 + = render 'notices/user_attributes', :user => @notice.user_attributes
  77 +
  78 + #environment
  79 + %h3 Environment
  80 + = render 'notices/environment', :notice => @notice
  81 +
  82 + #params
  83 + %h3 Parameters
  84 + = render 'notices/params', :notice => @notice
  85 +
  86 + #session
  87 + %h3 Session
  88 + = render 'notices/session', :notice => @notice
... ...
app/views/problems/show.ics.haml 0 → 100644
... ... @@ -0,0 +1 @@
  1 += generate_problem_ical(@problem.notices.order_by(:created_at.asc))
... ...
app/views/shared/_navigation.html.haml
... ... @@ -2,7 +2,7 @@
2 2 %ul
3 3 //%li= link_to 'Dashboard', admin_dashboard_path, :class => active_if_here(:dashboards)
4 4 %li.apps{:class => active_if_here(:apps)}= link_to 'Apps', apps_path
5   - %li.errs{:class => active_if_here(:errs)}= link_to 'Errors', errs_path
  5 + %li.errs{:class => active_if_here(:problems)}= link_to 'Errors', problems_path
6 6 - if user_signed_in? && current_user.admin?
7 7 %li.users{:class => active_if_here(:users)}= link_to 'Users', users_path
8   - %div.clear
9 8 \ No newline at end of file
  9 + %div.clear
... ...
app/views/users/index.html.haml
... ... @@ -2,9 +2,11 @@
2 2 - content_for :action_bar do
3 3 %span= link_to('Add a New User', new_user_path, :class => 'add')
4 4  
5   -%table
  5 +%table.users
6 6 %thead
7 7 %tr
  8 + - if Errbit::Config.use_gravatar
  9 + %th
8 10 %th Name
9 11 - if Errbit::Config.user_has_username
10 12 %th Username
... ... @@ -13,6 +15,8 @@
13 15 %tbody
14 16 - @users.each do |user|
15 17 %tr
  18 + - if Errbit::Config.use_gravatar
  19 + %td= gravatar_tag user.email, :s => 24
16 20 %td.nowrap= link_to user.name, user_path(user)
17 21 - if Errbit::Config.user_has_username
18 22 %td= user.username
... ...
app/views/users/show.html.haml
1 1 - content_for :title, @user.name
  2 +- if Errbit::Config.use_gravatar
  3 + - content_for :title_style do
  4 + background: url('#{gravatar_url @user.email, :s => 86}') no-repeat;
  5 + padding-left: 106px;
  6 +
2 7 - content_for :action_bar do
3 8 = render 'shared/link_github_account', :user => @user
4 9 %span= link_to('Add a New User', new_user_path, :class => 'add')
5 10 = link_to 'edit', edit_user_path(@user), :class => 'button'
6 11 = link_to 'destroy', user_path(@user), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button'
7 12  
8   -
9 13 %table.single_user
10 14 %tr
11 15 %th Email
... ...
config/config.example.yml
... ... @@ -39,6 +39,11 @@ user_has_username: false
39 39 # but you want to leave a short comment.
40 40 allow_comments_with_issue_tracker: true
41 41  
  42 +# Enable Gravatar.
  43 +use_gravatar: true
  44 +# Default Gravatar image, can be: mm, identicon, monsterid, wavatar, retro.
  45 +gravatar_default: identicon
  46 +
42 47 # Setup your deploy options for capistrano.
43 48 deployment:
44 49 hosts:
... ...
config/initializers/_load_config.rb
... ... @@ -13,6 +13,10 @@ unless defined?(Errbit::Config)
13 13 Errbit::Config.confirm_resolve_err = ENV['ERRBIT_CONFIRM_RESOLVE_ERR']
14 14 Errbit::Config.user_has_username = ENV['ERRBIT_USER_HAS_USERNAME']
15 15 Errbit::Config.allow_comments_with_issue_tracker = ENV['ERRBIT_ALLOW_COMMENTS_WITH_ISSUE_TRACKER']
  16 + Errbit::Config.enforce_ssl = ENV['ERRBIT_ENFORCE_SSL']
  17 +
  18 + Errbit::Config.use_gravatar = ENV['ERRBIT_USE_GRAVATAR']
  19 + Errbit::Config.gravatar_default = ENV['ERRBIT_GRAVATAR_DEFAULT']
16 20  
17 21 Errbit::Config.github_authentication = ENV['GITHUB_AUTHENTICATION']
18 22 Errbit::Config.github_client_id = ENV['GITHUB_CLIENT_ID']
... ... @@ -56,6 +60,9 @@ default_config.each do |k,v|
56 60 Errbit::Config.send("#{k}=", v) if Errbit::Config.send(k) === nil
57 61 end
58 62  
  63 +# Disable GitHub oauth if gem is missing
  64 +Errbit::Config.github_authentication = false unless defined?(OmniAuth::Strategies::GitHub)
  65 +
59 66 # Set SMTP settings if given.
60 67 if smtp = Errbit::Config.smtp_settings
61 68 ActionMailer::Base.delivery_method = :smtp
... ...
config/initializers/devise.rb
... ... @@ -122,7 +122,8 @@ Devise.setup do |config|
122 122 config.omniauth :github,
123 123 Errbit::Config.github_client_id,
124 124 Errbit::Config.github_secret,
125   - :scope => Errbit::Config.github_access_scope.join(",")
  125 + :scope => Errbit::Config.github_access_scope.join(","),
  126 + :skip_info => true
126 127 end
127 128  
128 129 # ==> Navigation configuration
... ...
config/initializers/notification_services.rb 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +# Include nested notification services models
  2 +include NotificationServices
... ...
config/initializers/omniauth.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +OmniAuth.config.logger = Rails.logger
... ...
config/mongoid.mongohq.yml
... ... @@ -5,4 +5,4 @@
5 5 # commit it to your repo, then push to heroku.
6 6  
7 7 production:
8   - uri: <%= ENV['MONGOHQ_URL'] %>
9 8 \ No newline at end of file
  9 + uri: <%= ENV['MONGOHQ_URL'] %>
... ...
config/routes.rb
... ... @@ -4,6 +4,7 @@ Errbit::Application.routes.draw do
4 4  
5 5 # Hoptoad Notifier Routes
6 6 match '/notifier_api/v2/notices' => 'notices#create'
  7 + match '/locate/:id' => 'notices#locate', :as => :locate
7 8 match '/deploys.txt' => 'deploys#create'
8 9  
9 10 resources :notices, :only => [:show]
... ... @@ -13,7 +14,7 @@ Errbit::Application.routes.draw do
13 14 delete :unlink_github
14 15 end
15 16 end
16   - resources :errs, :only => [:index] do
  17 + resources :problems, :only => [:index] do
17 18 collection do
18 19 post :destroy_several
19 20 post :resolve_several
... ... @@ -25,7 +26,7 @@ Errbit::Application.routes.draw do
25 26 end
26 27  
27 28 resources :apps do
28   - resources :errs do
  29 + resources :problems do
29 30 resources :notices
30 31 resources :comments, :only => [:create, :destroy]
31 32  
... ... @@ -40,6 +41,13 @@ Errbit::Application.routes.draw do
40 41 resources :watchers, :only => [:destroy]
41 42 end
42 43  
  44 + namespace :api do
  45 + namespace :v1 do
  46 + resources :problems, :only => [:index], :defaults => { :format => 'json' }
  47 + resources :notices, :only => [:index], :defaults => { :format => 'json' }
  48 + end
  49 + end
  50 +
43 51 root :to => 'apps#index'
44 52  
45 53 end
... ...
config/unicorn.rb
1 1 # http://michaelvanrooijen.com/articles/2011/06/01-more-concurrency-on-a-single-heroku-dyno-with-the-new-celadon-cedar-stack/
2 2  
3 3 worker_processes 3 # amount of unicorn workers to spin up
4   -timeout 30 # restarts workers that hang for 30 seconds
5 4 \ No newline at end of file
  5 +timeout 30 # restarts workers that hang for 30 seconds
  6 +preload_app true
6 7 \ No newline at end of file
... ...
db/migrate/20110422152027_move_notices_to_separate_collection.rb
... ... @@ -5,6 +5,10 @@ class MoveNoticesToSeparateCollection &lt; Mongoid::Migration
5 5 errs = mongo_db.collection("errs").find({ }, :fields => ["notices"])
6 6 errs.each do |err|
7 7 next unless err['notices']
  8 +
  9 + # This Err was created after the Problem->Err->Notice redesign
  10 + next if err['app_id'].nil? or err['problem_id']
  11 +
8 12 e = Err.find(err['_id'])
9 13 # disable email notifications
10 14 old_notify = e.app.notify_on_errs?
... ...
db/migrate/20120530005915_rename_klass_to_error_class.rb
1 1 class RenameKlassToErrorClass < Mongoid::Migration
2 2 def self.up
3 3 [Problem, Err, Notice].each do |model|
4   - model.collection.update({}, {'$rename' => {'klass' => 'error_class'}}, multi: true, safe: true)
  4 + model.collection.update({}, {'$rename' => {'klass' => 'error_class'}}, :multi => true, :safe => true)
5 5 end
6 6 end
7 7  
8 8 def self.down
9 9 [Problem, Err, Notice].each do |model|
10   - model.collection.update({}, {'$rename' => {'error_class' => 'klass'}}, multi: true, safe: true)
  10 + model.collection.update({}, {'$rename' => {'error_class' => 'klass'}}, :multi => true, :safe => true)
11 11 end
12 12 end
13 13 end
... ...
db/migrate/20120603112130_change_github_url_to_github_repo.rb
1 1 class ChangeGithubUrlToGithubRepo < Mongoid::Migration
2 2 def self.up
3   - App.collection.update({}, {'$rename' => {'github_url' => 'github_repo'}}, multi: true, safe: true)
  3 + App.collection.update({}, {'$rename' => {'github_url' => 'github_repo'}}, :multi => true, :safe => true)
4 4 App.all.each do |app|
5 5 app.send :normalize_github_repo
6 6 app.save
... ... @@ -8,7 +8,7 @@ class ChangeGithubUrlToGithubRepo &lt; Mongoid::Migration
8 8 end
9 9  
10 10 def self.down
11   - App.collection.update({}, {'$rename' => {'github_repo' => 'github_url'}}, multi: true, safe: true)
  11 + App.collection.update({}, {'$rename' => {'github_repo' => 'github_url'}}, :multi => true, :safe => true)
12 12 App.all.each do |app|
13 13 unless app.github_repo.include?("github.com")
14 14 app.update_attribute :github_url, "https://github.com/" << app.github_url
... ...
db/migrate/20120822195841_set_first_notice_at_on_problems.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +class SetFirstNoticeAtOnProblems < Mongoid::Migration
  2 + def self.up
  3 + Problem.all.each do |problem|
  4 + problem.update_attribute :first_notice_at, problem.notices.order_by([:created_at, :asc]).first.try(:created_at)
  5 + end
  6 + end
  7 +
  8 + def self.down
  9 + end
  10 +end
0 11 \ No newline at end of file
... ...
db/migrate/20120829034812_ensure_that_problems_last_notice_at_is_not_nil.rb 0 → 100644
... ... @@ -0,0 +1,23 @@
  1 +class EnsureThatProblemsLastNoticeAtIsNotNil < Mongoid::Migration
  2 + def self.up
  3 + Problem.where("$or" => [{:last_notice_at => nil}, {:first_notice_at => nil}]).each do |problem|
  4 + first_notice = problem.notices.order_by([:created_at, :asc]).first
  5 +
  6 + # Destroy problems with no notices
  7 + if first_notice.nil?
  8 + problem.destroy
  9 + next
  10 + end
  11 +
  12 + last_notice = problem.notices.order_by([:created_at, :asc]).last
  13 +
  14 + problem.update_attributes!({
  15 + :first_notice_at => first_notice.created_at,
  16 + :last_notice_at => last_notice.created_at
  17 + })
  18 + end
  19 + end
  20 +
  21 + def self.down
  22 + end
  23 +end
... ...
db/migrate/20121003223358_extract_backtraces.rb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +class ExtractBacktraces < Mongoid::Migration
  2 + def self.up
  3 + say "It could take long time (hours if you have many Notices)"
  4 + Notice.unscoped.all.each do |notice|
  5 + backtrace = Backtrace.find_or_create(:raw => notice['backtrace'])
  6 + notice.backtrace = backtrace
  7 + notice['backtrace'] = nil
  8 + notice.save!
  9 + end
  10 + say "run `db.repairDatabase()` (in mongodb console) to recover deleted space"
  11 + end
  12 +
  13 + def self.down
  14 + end
  15 +end
... ...
db/migrate/20121005142110_regenerate_err_fingerprints.rb 0 → 100644
... ... @@ -0,0 +1,19 @@
  1 +class RegenerateErrFingerprints < Mongoid::Migration
  2 + def self.up
  3 + Err.all.each do |err|
  4 + fingerprint_source = {
  5 + :backtrace => err.notices.first.backtrace_id,
  6 + :error_class => err.error_class,
  7 + :component => err.component,
  8 + :action => err.action,
  9 + :environment => err.environment,
  10 + :api_key => err.app.api_key
  11 + }
  12 + fingerprint = Digest::SHA1.hexdigest(fingerprint_source.to_s)
  13 + err.update_attribute(:fingerprint, fingerprint)
  14 + end
  15 + end
  16 +
  17 + def self.down
  18 + end
  19 +end
... ...
lib/hoptoad.rb
... ... @@ -3,7 +3,7 @@ require &#39;hoptoad/v2&#39;
3 3 module Hoptoad
4 4 class ApiVersionError < StandardError
5 5 def initialize
6   - super "Wrong API Version: Expecting 2.0, 2.1, or 2.2"
  6 + super "Wrong API Version: Expecting 2.0, 2.1, 2.2 or 2.3"
7 7 end
8 8 end
9 9  
... ... @@ -16,7 +16,7 @@ module Hoptoad
16 16 private
17 17 def self.get_version_processor(version)
18 18 case version
19   - when /2\.[012]/; Hoptoad::V2
  19 + when /2\.[0123]/; Hoptoad::V2
20 20 else; raise ApiVersionError
21 21 end
22 22 end
... ...
lib/hoptoad/v2.rb
... ... @@ -59,7 +59,8 @@ module Hoptoad
59 59  
60 60 :api_key => notice['api-key'],
61 61 :notifier => notice['notifier'],
62   - :user_attributes => notice['user-attributes'] || {}
  62 + :user_attributes => notice['user-attributes'] || {},
  63 + :current_user => notice['current-user'] || {}
63 64 }
64 65 end
65 66 end
... ...
lib/issue_trackers/apis/mingle.rb
  1 +require 'active_resource'
  2 +
1 3 module Mingle
2 4 class Card < ActiveResource::Base
3 5 # site template ~> "https://username:password@mingle.example.com/api/v1/projects/:project_id/"
... ...
lib/tasks/errbit/database.rake
  1 +require 'digest/sha1'
  2 +
1 3 namespace :errbit do
2 4 namespace :db do
3   -
  5 +
4 6 desc "Updates cached attributes on Problem"
5 7 task :update_problem_attrs => :environment do
6 8 puts "Updating problems"
7 9 Problem.all.each(&:cache_notice_attributes)
8 10 end
9   -
  11 +
10 12 desc "Updates Problem#notices_count"
11 13 task :update_notices_count => :environment do
12 14 puts "Updating problem.notices_count"
... ... @@ -14,12 +16,59 @@ namespace :errbit do
14 16 p.update_attributes(:notices_count => p.notices.count)
15 17 end
16 18 end
17   -
  19 +
18 20 desc "Delete resolved errors from the database. (Useful for limited heroku databases)"
19 21 task :clear_resolved => :environment do
20 22 count = Problem.resolved.count
21 23 Problem.resolved.each {|problem| problem.destroy }
22 24 puts "=== Cleared #{count} resolved errors from the database." if count > 0
23 25 end
  26 +
  27 + desc "Regenerate fingerprints"
  28 + task :regenerate_fingerprints => :environment do
  29 +
  30 + def normalize_backtrace(backtrace)
  31 + backtrace[0...3].map do |trace|
  32 + trace.merge 'method' => trace['method'].to_s.gsub(/[0-9_]{10,}+/, "__FRAGMENT__")
  33 + end
  34 + end
  35 +
  36 + def fingerprint(source)
  37 + Digest::SHA1.hexdigest(source.to_s)
  38 + end
  39 +
  40 + puts "Regenerating Err fingerprints"
  41 + Err.create_indexes
  42 + Err.all.each do |err|
  43 + next if err.notices.count == 0
  44 + source = {
  45 + :backtrace => normalize_backtrace(err.notices.first.backtrace).to_s,
  46 + :error_class => err.error_class,
  47 + :component => err.component,
  48 + :action => err.action,
  49 + :environment => err.environment,
  50 + :api_key => err.app.api_key
  51 + }
  52 + err.update_attributes(:fingerprint => fingerprint(source))
  53 + end
  54 + end
  55 +
  56 + desc "Remove notices in batch"
  57 + task :notices_delete, [ :problem_id ] => [ :environment ] do
  58 + BATCH_SIZE = 1000
  59 + if args[:problem_id]
  60 + item_count = Problem.find(args[:problem_id]).notices.count
  61 + removed_count = 0
  62 + puts "Notices to remove: #{item_count}"
  63 + while item_count > 0
  64 + Problem.find(args[:problem_id]).notices.limit(BATCH_SIZE).each do |notice|
  65 + notice.remove
  66 + removed_count += 1
  67 + end
  68 + item_count -= BATCH_SIZE
  69 + puts "Removed #{removed_count} notices"
  70 + end
  71 + end
  72 + end
24 73 end
25 74 end
... ...
public/javascripts/notifier.js
... ... @@ -92,13 +92,13 @@ var Hoptoad = {
92 92  
93 93 var methods = ['params', 'session'];
94 94  
95   - for (var i = 0; i < 2; i++) {
96   - var type = methods[i];
  95 + for (var i = 0; i < methods.length; i++) {
  96 + var method = methods[i];
97 97  
98   - if (error[type]) {
99   - data += '<' + type + '>';
100   - data += Hoptoad.generateVariables(error[type]);
101   - data += '</' + type + '>';
  98 + if (error[method]) {
  99 + data += '<' + method + '>';
  100 + data += Hoptoad.generateVariables(error[method]);
  101 + data += '</' + method + '>';
102 102 }
103 103 }
104 104  
... ...
spec/controllers/api/v1/notices_controller_spec.rb 0 → 100644
... ... @@ -0,0 +1,54 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Api::V1::NoticesController do
  4 +
  5 + context "when logged in" do
  6 + before do
  7 + @user = Fabricate(:user)
  8 + end
  9 +
  10 + describe "GET /api/v1/notices" do
  11 + before do
  12 + Fabricate(:notice, :created_at => DateTime.new(2012, 8, 01))
  13 + Fabricate(:notice, :created_at => DateTime.new(2012, 8, 01))
  14 + Fabricate(:notice, :created_at => DateTime.new(2012, 8, 21))
  15 + Fabricate(:notice, :created_at => DateTime.new(2012, 8, 30))
  16 + end
  17 +
  18 + it "should return JSON if JSON is requested" do
  19 + get :index, :auth_token => @user.authentication_token, :format => "json"
  20 + lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  21 + end
  22 +
  23 + it "should return XML if XML is requested" do
  24 + get :index, :auth_token => @user.authentication_token, :format => "xml"
  25 + lambda { XML::Parser.string(response.body).parse }.should_not raise_error
  26 + end
  27 +
  28 + it "should return JSON by default" do
  29 + get :index, :auth_token => @user.authentication_token
  30 + lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  31 + end
  32 +
  33 + describe "given a date range" do
  34 +
  35 + it "should return only the notices created during the date range" do
  36 + get :index, {:auth_token => @user.authentication_token, :start_date => "2012-08-01", :end_date => "2012-08-27"}
  37 + response.should be_success
  38 + notices = JSON.load response.body
  39 + notices.length.should == 3
  40 + end
  41 +
  42 + end
  43 +
  44 + it "should return all notices" do
  45 + get :index, {:auth_token => @user.authentication_token}
  46 + response.should be_success
  47 + notices = JSON.load response.body
  48 + notices.length.should == 4
  49 + end
  50 +
  51 + end
  52 + end
  53 +
  54 +end
... ...
spec/controllers/api/v1/problems_controller_spec.rb 0 → 100644
... ... @@ -0,0 +1,58 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Api::V1::ProblemsController do
  4 +
  5 + context "when logged in" do
  6 + before do
  7 + @user = Fabricate(:user)
  8 + end
  9 +
  10 + describe "GET /api/v1/problems" do
  11 + before do
  12 + Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 01), :resolved_at => Date.new(2012, 8, 02))
  13 + Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 01), :resolved_at => Date.new(2012, 8, 21))
  14 + Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 21))
  15 + Fabricate(:problem, :first_notice_at => Date.new(2012, 8, 30))
  16 + end
  17 +
  18 +
  19 +
  20 + it "should return JSON if JSON is requested" do
  21 + get :index, :auth_token => @user.authentication_token, :format => "json"
  22 + lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  23 + end
  24 +
  25 + it "should return XML if XML is requested" do
  26 + get :index, :auth_token => @user.authentication_token, :format => "xml"
  27 + lambda { XML::Parser.string(response.body).parse }.should_not raise_error
  28 + end
  29 +
  30 + it "should return JSON by default" do
  31 + get :index, :auth_token => @user.authentication_token
  32 + lambda { JSON.load(response.body) }.should_not raise_error(JSON::ParserError)
  33 + end
  34 +
  35 +
  36 +
  37 + describe "given a date range" do
  38 +
  39 + it "should return only the problems open during the date range" do
  40 + get :index, {:auth_token => @user.authentication_token, :start_date => "2012-08-20", :end_date => "2012-08-27"}
  41 + response.should be_success
  42 + problems = JSON.load response.body
  43 + problems.length.should == 2
  44 + end
  45 +
  46 + end
  47 +
  48 + it "should return all problems" do
  49 + get :index, {:auth_token => @user.authentication_token}
  50 + response.should be_success
  51 + problems = JSON.load response.body
  52 + problems.length.should == 4
  53 + end
  54 +
  55 + end
  56 + end
  57 +
  58 +end
... ...
spec/controllers/comments_controller_spec.rb
... ... @@ -16,7 +16,7 @@ describe CommentsController do
16 16 let(:user) { Fabricate(:user) }
17 17  
18 18 before(:each) do
19   - post :create, :app_id => problem.app.id, :err_id => problem.id,
  19 + post :create, :app_id => problem.app.id, :problem_id => problem.id,
20 20 :comment => { :body => "One test comment", :user_id => user.id }
21 21 problem.reload
22 22 end
... ... @@ -26,7 +26,7 @@ describe CommentsController do
26 26 end
27 27  
28 28 it "should redirect to problem page" do
29   - response.should redirect_to( app_err_path(problem.app, problem) )
  29 + response.should redirect_to( app_problem_path(problem.app, problem) )
30 30 end
31 31 end
32 32 end
... ... @@ -43,7 +43,7 @@ describe CommentsController do
43 43 let(:comment) { problem.reload.comments.first }
44 44  
45 45 before(:each) do
46   - delete :destroy, :app_id => problem.app.id, :err_id => problem.id, :id => comment.id.to_s
  46 + delete :destroy, :app_id => problem.app.id, :problem_id => problem.id, :id => comment.id.to_s
47 47 problem.reload
48 48 end
49 49  
... ... @@ -52,7 +52,7 @@ describe CommentsController do
52 52 end
53 53  
54 54 it "should redirect to problem page" do
55   - response.should redirect_to( app_err_path(problem.app, problem) )
  55 + response.should redirect_to( app_problem_path(problem.app, problem) )
56 56 end
57 57 end
58 58 end
... ...
spec/controllers/errs_controller_spec.rb
... ... @@ -1,441 +0,0 @@
1   -require 'spec_helper'
2   -
3   -describe ErrsController do
4   -
5   - it_requires_authentication :for => {
6   - :index => :get, :all => :get, :show => :get, :resolve => :put
7   - },
8   - :params => {:app_id => 'dummyid', :id => 'dummyid'}
9   -
10   - let(:app) { Fabricate(:app) }
11   - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production")) }
12   -
13   -
14   - describe "GET /errs" do
15   - render_views
16   - context 'when logged in as an admin' do
17   - before(:each) do
18   - @user = Fabricate(:admin)
19   - sign_in @user
20   - @problem = Fabricate(:notice, :err => Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production"))).problem
21   - end
22   -
23   - it "should successfully list errs" do
24   - get :index
25   - response.should be_success
26   - response.body.gsub("&#8203;", "").should match(@problem.message)
27   - end
28   -
29   - it "should list atom feed successfully" do
30   - get :index, :format => "atom"
31   - response.should be_success
32   - response.body.should match(@problem.message)
33   - end
34   -
35   - context "pagination" do
36   - before(:each) do
37   - 35.times { Fabricate :err }
38   - end
39   -
40   - it "should have default per_page value for user" do
41   - get :index
42   - assigns(:problems).to_a.size.should == User::PER_PAGE
43   - end
44   -
45   - it "should be able to override default per_page value" do
46   - @user.update_attribute :per_page, 10
47   - get :index
48   - assigns(:problems).to_a.size.should == 10
49   - end
50   - end
51   -
52   - context 'with environment filters' do
53   - before(:each) do
54   - environments = ['production', 'test', 'development', 'staging']
55   - 20.times do |i|
56   - Fabricate(:problem, :environment => environments[i % environments.length])
57   - end
58   - end
59   -
60   - context 'no params' do
61   - it 'shows errs for all environments' do
62   - get :index
63   - assigns(:problems).size.should == 21
64   - end
65   - end
66   -
67   - context 'environment production' do
68   - it 'shows errs for just production' do
69   - get :index, :environment => 'production'
70   - assigns(:problems).size.should == 6
71   - end
72   - end
73   -
74   - context 'environment staging' do
75   - it 'shows errs for just staging' do
76   - get :index, :environment => 'staging'
77   - assigns(:problems).size.should == 5
78   - end
79   - end
80   -
81   - context 'environment development' do
82   - it 'shows errs for just development' do
83   - get :index, :environment => 'development'
84   - assigns(:problems).size.should == 5
85   - end
86   - end
87   -
88   - context 'environment test' do
89   - it 'shows errs for just test' do
90   - get :index, :environment => 'test'
91   - assigns(:problems).size.should == 5
92   - end
93   - end
94   - end
95   - end
96   -
97   - context 'when logged in as a user' do
98   - it 'gets a paginated list of unresolved errs for the users apps' do
99   - sign_in(user = Fabricate(:user))
100   - unwatched_err = Fabricate(:err)
101   - watched_unresolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false))
102   - watched_resolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true))
103   - get :index
104   - assigns(:problems).should include(watched_unresolved_err.problem)
105   - assigns(:problems).should_not include(unwatched_err.problem, watched_resolved_err.problem)
106   - end
107   - end
108   - end
109   -
110   - describe "GET /errs/all" do
111   - context 'when logged in as an admin' do
112   - it "gets a paginated list of all errs" do
113   - sign_in Fabricate(:admin)
114   - errs = Kaminari.paginate_array((1..30).to_a)
115   - 3.times { errs << Fabricate(:err).problem }
116   - 3.times { errs << Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem }
117   - Problem.should_receive(:ordered_by).and_return(
118   - mock('proxy', :page => mock('other_proxy', :per => errs))
119   - )
120   - get :all
121   - assigns(:problems).should == errs
122   - end
123   - end
124   -
125   - context 'when logged in as a user' do
126   - it 'gets a paginated list of all errs for the users apps' do
127   - sign_in(user = Fabricate(:user))
128   - unwatched_err = Fabricate(:problem)
129   - watched_unresolved_err = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false)
130   - watched_resolved_err = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true)
131   - get :all
132   - assigns(:problems).should include(watched_resolved_err, watched_unresolved_err)
133   - assigns(:problems).should_not include(unwatched_err)
134   - end
135   - end
136   - end
137   -
138   - describe "GET /apps/:app_id/errs/:id" do
139   - render_views
140   -
141   - context 'when logged in as an admin' do
142   - before do
143   - sign_in Fabricate(:admin)
144   - end
145   -
146   - it "finds the app" do
147   - get :show, :app_id => app.id, :id => err.problem.id
148   - assigns(:app).should == app
149   - end
150   -
151   - it "finds the err" do
152   - get :show, :app_id => app.id, :id => err.problem.id
153   - assigns(:problem).should == err.problem
154   - end
155   -
156   - it "successfully render page" do
157   - get :show, :app_id => app.id, :id => err.problem.id
158   - response.should be_success
159   - end
160   -
161   - context 'pagination' do
162   - let!(:notices) do
163   - 3.times.reduce([]) do |coll, i|
164   - coll << Fabricate(:notice, :err => err, :created_at => (Time.now + i))
165   - end
166   - end
167   -
168   - it "paginates the notices 1 at a time, starting with the most recent" do
169   - get :show, :app_id => app.id, :id => err.problem.id
170   - assigns(:notices).entries.count.should == 1
171   - assigns(:notices).should include(notices.last)
172   - end
173   -
174   - it "paginates the notices 1 at a time, based on then notice param" do
175   - get :show, :app_id => app.id, :id => err.problem.id, :notice => 3
176   - assigns(:notices).entries.count.should == 1
177   - assigns(:notices).should include(notices.first)
178   - end
179   - end
180   -
181   - context "create issue button" do
182   - let(:button_matcher) { match(/create issue/) }
183   -
184   - it "should not exist for err's app without issue tracker" do
185   - err = Fabricate :err
186   - get :show, :app_id => err.app.id, :id => err.problem.id
187   -
188   - response.body.should_not button_matcher
189   - end
190   -
191   - it "should exist for err's app with issue tracker" do
192   - tracker = Fabricate(:lighthouse_tracker)
193   - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app))
194   - get :show, :app_id => err.app.id, :id => err.problem.id
195   -
196   - response.body.should button_matcher
197   - end
198   -
199   - it "should not exist for err with issue_link" do
200   - tracker = Fabricate(:lighthouse_tracker)
201   - err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app, :issue_link => "http://some.host"))
202   - get :show, :app_id => err.app.id, :id => err.problem.id
203   -
204   - response.body.should_not button_matcher
205   - end
206   - end
207   - end
208   -
209   - context 'when logged in as a user' do
210   - before do
211   - sign_in(@user = Fabricate(:user))
212   - @unwatched_err = Fabricate(:err)
213   - @watched_app = Fabricate(:app)
214   - @watcher = Fabricate(:user_watcher, :user => @user, :app => @watched_app)
215   - @watched_err = Fabricate(:err, :problem => Fabricate(:problem, :app => @watched_app))
216   - end
217   -
218   - it 'finds the err if the user is watching the app' do
219   - get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id
220   - assigns(:problem).should == @watched_err.problem
221   - end
222   -
223   - it 'raises a DocumentNotFound error if the user is not watching the app' do
224   - lambda {
225   - get :show, :app_id => @unwatched_err.problem.app_id, :id => @unwatched_err.problem.id
226   - }.should raise_error(Mongoid::Errors::DocumentNotFound)
227   - end
228   - end
229   - end
230   -
231   - describe "PUT /apps/:app_id/errs/:id/resolve" do
232   - before do
233   - sign_in Fabricate(:admin)
234   -
235   - @problem = Fabricate(:err)
236   - App.stub(:find).with(@problem.app.id).and_return(@problem.app)
237   - @problem.app.problems.stub(:find).and_return(@problem.problem)
238   - @problem.problem.stub(:resolve!)
239   - end
240   -
241   - it 'finds the app and the err' do
242   - App.should_receive(:find).with(@problem.app.id).and_return(@problem.app)
243   - @problem.app.problems.should_receive(:find).and_return(@problem.problem)
244   - put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
245   - assigns(:app).should == @problem.app
246   - assigns(:problem).should == @problem.problem
247   - end
248   -
249   - it "should resolve the issue" do
250   - @problem.problem.should_receive(:resolve!).and_return(true)
251   - put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
252   - end
253   -
254   - it "should display a message" do
255   - put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
256   - request.flash[:success].should match(/Great news/)
257   - end
258   -
259   - it "should redirect to the app page" do
260   - put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
261   - response.should redirect_to(app_path(@problem.app))
262   - end
263   -
264   - it "should redirect back to errs page" do
265   - request.env["Referer"] = errs_path
266   - put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
267   - response.should redirect_to(errs_path)
268   - end
269   - end
270   -
271   - describe "POST /apps/:app_id/errs/:id/create_issue" do
272   - render_views
273   -
274   - before(:each) do
275   - sign_in Fabricate(:admin)
276   - end
277   -
278   - context "successful issue creation" do
279   - context "lighthouseapp tracker" do
280   - let(:notice) { Fabricate :notice }
281   - let(:tracker) { Fabricate :lighthouse_tracker, :app => notice.app }
282   - let(:problem) { notice.problem }
283   -
284   - before(:each) do
285   - number = 5
286   - @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
287   - body = "<ticket><number type=\"integer\">#{number}</number></ticket>"
288   - stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
289   - to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
290   -
291   - post :create_issue, :app_id => problem.app.id, :id => problem.id
292   - problem.reload
293   - end
294   -
295   - it "should redirect to problem page" do
296   - response.should redirect_to( app_err_path(problem.app, problem) )
297   - end
298   - end
299   - end
300   -
301   - context "absent issue tracker" do
302   - let(:problem) { Fabricate :problem }
303   -
304   - before(:each) do
305   - post :create_issue, :app_id => problem.app.id, :id => problem.id
306   - end
307   -
308   - it "should redirect to problem page" do
309   - response.should redirect_to( app_err_path(problem.app, problem) )
310   - end
311   -
312   - it "should set flash error message telling issue tracker of the app doesn't exist" do
313   - flash[:error].should == "This app has no issue tracker setup."
314   - end
315   - end
316   -
317   - context "error during request to a tracker" do
318   - context "lighthouseapp tracker" do
319   - let(:tracker) { Fabricate :lighthouse_tracker }
320   - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app)) }
321   -
322   - before(:each) do
323   - stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 500)
324   -
325   - post :create_issue, :app_id => err.app.id, :id => err.problem.id
326   - end
327   -
328   - it "should redirect to err page" do
329   - response.should redirect_to( app_err_path(err.app, err.problem) )
330   - end
331   -
332   - it "should notify of connection error" do
333   - flash[:error].should include("There was an error during issue creation:")
334   - end
335   - end
336   - end
337   - end
338   -
339   - describe "DELETE /apps/:app_id/errs/:id/unlink_issue" do
340   - before(:each) do
341   - sign_in Fabricate(:admin)
342   - end
343   -
344   - context "err with issue" do
345   - let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :issue_link => "http://some.host")) }
346   -
347   - before(:each) do
348   - delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
349   - err.problem.reload
350   - end
351   -
352   - it "should redirect to err page" do
353   - response.should redirect_to( app_err_path(err.app, err.problem) )
354   - end
355   -
356   - it "should clear issue link" do
357   - err.problem.issue_link.should be_nil
358   - end
359   - end
360   -
361   - context "err without issue" do
362   - let(:err) { Fabricate :err }
363   -
364   - before(:each) do
365   - delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
366   - err.problem.reload
367   - end
368   -
369   - it "should redirect to err page" do
370   - response.should redirect_to( app_err_path(err.app, err.problem) )
371   - end
372   - end
373   - end
374   -
375   - describe "Bulk Actions" do
376   - before(:each) do
377   - sign_in Fabricate(:admin)
378   - @problem1 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem
379   - @problem2 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => false)).problem
380   - end
381   -
382   - it "should apply to multiple problems" do
383   - post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
384   - assigns(:selected_problems).should == [@problem1, @problem2]
385   - end
386   -
387   - it "should require at least one problem" do
388   - post :resolve_several, :problems => []
389   - request.flash[:notice].should match(/You have not selected any/)
390   - end
391   -
392   - context "POST /errs/merge_several" do
393   - it "should require at least two problems" do
394   - post :merge_several, :problems => [@problem1.id.to_s]
395   - request.flash[:notice].should match(/You must select at least two/)
396   - end
397   -
398   - it "should merge the problems" do
399   - lambda {
400   - post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
401   - assigns(:merged_problem).reload.errs.length.should == 2
402   - }.should change(Problem, :count).by(-1)
403   - end
404   - end
405   -
406   - context "POST /errs/unmerge_several" do
407   - it "should unmerge a merged problem" do
408   - merged_problem = Problem.merge!(@problem1, @problem2)
409   - merged_problem.errs.length.should == 2
410   - lambda {
411   - post :unmerge_several, :problems => [merged_problem.id.to_s]
412   - merged_problem.reload.errs.length.should == 1
413   - }.should change(Problem, :count).by(1)
414   - end
415   - end
416   -
417   - context "POST /errs/resolve_several" do
418   - it "should resolve the issue" do
419   - post :resolve_several, :problems => [@problem2.id.to_s]
420   - @problem2.reload.resolved?.should == true
421   - end
422   - end
423   -
424   - context "POST /errs/unresolve_several" do
425   - it "should unresolve the issue" do
426   - post :unresolve_several, :problems => [@problem1.id.to_s]
427   - @problem1.reload.resolved?.should == false
428   - end
429   - end
430   -
431   - context "POST /errs/destroy_several" do
432   - it "should delete the errs" do
433   - lambda {
434   - post :destroy_several, :problems => [@problem1.id.to_s]
435   - }.should change(Problem, :count).by(-1)
436   - end
437   - end
438   - end
439   -
440   -end
441   -
spec/controllers/notices_controller_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe NoticesController do
  4 + it_requires_authentication :for => { :locate => :get }
  5 +
  6 + let(:app) { Fabricate(:app) }
4 7  
5 8 context 'notices API' do
6 9 before do
... ... @@ -8,25 +11,44 @@ describe NoticesController do
8 11 @app = Fabricate(:app_with_watcher)
9 12 App.stub(:find_by_api_key!).and_return(@app)
10 13 @notice = App.report_error!(@xml)
11   -
12   - request.env['Content-type'] = 'text/xml'
13   - request.env['Accept'] = 'text/xml, application/xml'
14 14 end
15 15  
16   - it "generates a notice from xml [POST]" do
  16 + it "generates a notice from raw xml [POST]" do
17 17 App.should_receive(:report_error!).with(@xml).and_return(@notice)
18 18 request.should_receive(:raw_post).and_return(@xml)
19   - post :create
  19 + post :create, :format => :xml
  20 + response.should be_success
  21 + # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53)
  22 + # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb
  23 + response.body.should match(%r{<id[^>]*>#{@notice.id}</id>})
  24 + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>})
  25 + end
  26 +
  27 + it "generates a notice from xml in a data param [POST]" do
  28 + App.should_receive(:report_error!).with(@xml).and_return(@notice)
  29 + post :create, :data => @xml, :format => :xml
  30 + response.should be_success
  31 + # Same RegExp from Airbrake::Sender#send_to_airbrake (https://github.com/airbrake/airbrake/blob/master/lib/airbrake/sender.rb#L53)
  32 + # Inspired by https://github.com/airbrake/airbrake/blob/master/test/sender_test.rb
  33 + response.body.should match(%r{<id[^>]*>#{@notice.id}</id>})
  34 + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>})
20 35 end
21 36  
22 37 it "generates a notice from xml [GET]" do
23 38 App.should_receive(:report_error!).with(@xml).and_return(@notice)
24   - get :create, {:data => @xml}
  39 + get :create, :data => @xml, :format => :xml
  40 + response.should be_success
  41 + response.body.should match(%r{<id[^>]*>#{@notice.id}</id>})
  42 + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>})
25 43 end
26 44  
27 45 it "sends a notification email" do
  46 + App.should_receive(:report_error!).with(@xml).and_return(@notice)
28 47 request.should_receive(:raw_post).and_return(@xml)
29   - post :create
  48 + post :create, :format => :xml
  49 + response.should be_success
  50 + response.body.should match(%r{<id[^>]*>#{@notice.id}</id>})
  51 + response.body.should match(%r{<url[^>]*>(.+)#{locate_path(@notice.id)}</url>})
30 52 email = ActionMailer::Base.deliveries.last
31 53 email.to.should include(@app.watchers.first.email)
32 54 email.subject.should include(@notice.message)
... ... @@ -35,5 +57,21 @@ describe NoticesController do
35 57 end
36 58 end
37 59  
  60 + describe "GET /locate/:id" do
  61 + context 'when logged in as an admin' do
  62 + before(:each) do
  63 + @user = Fabricate(:admin)
  64 + sign_in @user
  65 + end
  66 +
  67 + it "should locate notice and redirect to problem" do
  68 + problem = Fabricate(:problem, :app => app, :environment => "production")
  69 + notice = Fabricate(:notice, :err => Fabricate(:err, :problem => problem))
  70 + get :locate, :id => notice.id
  71 + response.should redirect_to(app_problem_path(problem.app, problem))
  72 + end
  73 + end
  74 + end
  75 +
38 76 end
39 77  
... ...
spec/controllers/problems_controller_spec.rb 0 → 100644
... ... @@ -0,0 +1,441 @@
  1 +require 'spec_helper'
  2 +
  3 +describe ProblemsController do
  4 +
  5 + it_requires_authentication :for => {
  6 + :index => :get, :all => :get, :show => :get, :resolve => :put
  7 + },
  8 + :params => {:app_id => 'dummyid', :id => 'dummyid'}
  9 +
  10 + let(:app) { Fabricate(:app) }
  11 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production")) }
  12 +
  13 +
  14 + describe "GET /problems" do
  15 + render_views
  16 + context 'when logged in as an admin' do
  17 + before(:each) do
  18 + @user = Fabricate(:admin)
  19 + sign_in @user
  20 + @problem = Fabricate(:notice, :err => Fabricate(:err, :problem => Fabricate(:problem, :app => app, :environment => "production"))).problem
  21 + end
  22 +
  23 + it "should successfully list problems" do
  24 + get :index
  25 + response.should be_success
  26 + response.body.gsub("&#8203;", "").should match(@problem.message)
  27 + end
  28 +
  29 + it "should list atom feed successfully" do
  30 + get :index, :format => "atom"
  31 + response.should be_success
  32 + response.body.should match(@problem.message)
  33 + end
  34 +
  35 + context "pagination" do
  36 + before(:each) do
  37 + 35.times { Fabricate :err }
  38 + end
  39 +
  40 + it "should have default per_page value for user" do
  41 + get :index
  42 + assigns(:problems).to_a.size.should == User::PER_PAGE
  43 + end
  44 +
  45 + it "should be able to override default per_page value" do
  46 + @user.update_attribute :per_page, 10
  47 + get :index
  48 + assigns(:problems).to_a.size.should == 10
  49 + end
  50 + end
  51 +
  52 + context 'with environment filters' do
  53 + before(:each) do
  54 + environments = ['production', 'test', 'development', 'staging']
  55 + 20.times do |i|
  56 + Fabricate(:problem, :environment => environments[i % environments.length])
  57 + end
  58 + end
  59 +
  60 + context 'no params' do
  61 + it 'shows problems for all environments' do
  62 + get :index
  63 + assigns(:problems).size.should == 21
  64 + end
  65 + end
  66 +
  67 + context 'environment production' do
  68 + it 'shows problems for just production' do
  69 + get :index, :environment => 'production'
  70 + assigns(:problems).size.should == 6
  71 + end
  72 + end
  73 +
  74 + context 'environment staging' do
  75 + it 'shows problems for just staging' do
  76 + get :index, :environment => 'staging'
  77 + assigns(:problems).size.should == 5
  78 + end
  79 + end
  80 +
  81 + context 'environment development' do
  82 + it 'shows problems for just development' do
  83 + get :index, :environment => 'development'
  84 + assigns(:problems).size.should == 5
  85 + end
  86 + end
  87 +
  88 + context 'environment test' do
  89 + it 'shows problems for just test' do
  90 + get :index, :environment => 'test'
  91 + assigns(:problems).size.should == 5
  92 + end
  93 + end
  94 + end
  95 + end
  96 +
  97 + context 'when logged in as a user' do
  98 + it 'gets a paginated list of unresolved problems for the users apps' do
  99 + sign_in(user = Fabricate(:user))
  100 + unwatched_err = Fabricate(:err)
  101 + watched_unresolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false))
  102 + watched_resolved_err = Fabricate(:err, :problem => Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true))
  103 + get :index
  104 + assigns(:problems).should include(watched_unresolved_err.problem)
  105 + assigns(:problems).should_not include(unwatched_err.problem, watched_resolved_err.problem)
  106 + end
  107 + end
  108 + end
  109 +
  110 + describe "GET /problems/all" do
  111 + context 'when logged in as an admin' do
  112 + it "gets a paginated list of all problems" do
  113 + sign_in Fabricate(:admin)
  114 + problems = Kaminari.paginate_array((1..30).to_a)
  115 + 3.times { problems << Fabricate(:err).problem }
  116 + 3.times { problems << Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem }
  117 + Problem.should_receive(:ordered_by).and_return(
  118 + mock('proxy', :page => mock('other_proxy', :per => problems))
  119 + )
  120 + get :all
  121 + assigns(:problems).should == problems
  122 + end
  123 + end
  124 +
  125 + context 'when logged in as a user' do
  126 + it 'gets a paginated list of all problems for the users apps' do
  127 + sign_in(user = Fabricate(:user))
  128 + unwatched_problem = Fabricate(:problem)
  129 + watched_unresolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => false)
  130 + watched_resolved_problem = Fabricate(:problem, :app => Fabricate(:user_watcher, :user => user).app, :resolved => true)
  131 + get :all
  132 + assigns(:problems).should include(watched_resolved_problem, watched_unresolved_problem)
  133 + assigns(:problems).should_not include(unwatched_problem)
  134 + end
  135 + end
  136 + end
  137 +
  138 + describe "GET /apps/:app_id/problems/:id" do
  139 + render_views
  140 +
  141 + context 'when logged in as an admin' do
  142 + before do
  143 + sign_in Fabricate(:admin)
  144 + end
  145 +
  146 + it "finds the app" do
  147 + get :show, :app_id => app.id, :id => err.problem.id
  148 + assigns(:app).should == app
  149 + end
  150 +
  151 + it "finds the problem" do
  152 + get :show, :app_id => app.id, :id => err.problem.id
  153 + assigns(:problem).should == err.problem
  154 + end
  155 +
  156 + it "successfully render page" do
  157 + get :show, :app_id => app.id, :id => err.problem.id
  158 + response.should be_success
  159 + end
  160 +
  161 + context 'pagination' do
  162 + let!(:notices) do
  163 + 3.times.reduce([]) do |coll, i|
  164 + coll << Fabricate(:notice, :err => err, :created_at => (Time.now + i))
  165 + end
  166 + end
  167 +
  168 + it "paginates the notices 1 at a time, starting with the most recent" do
  169 + get :show, :app_id => app.id, :id => err.problem.id
  170 + assigns(:notices).entries.count.should == 1
  171 + assigns(:notices).should include(notices.last)
  172 + end
  173 +
  174 + it "paginates the notices 1 at a time, based on then notice param" do
  175 + get :show, :app_id => app.id, :id => err.problem.id, :notice => 3
  176 + assigns(:notices).entries.count.should == 1
  177 + assigns(:notices).should include(notices.first)
  178 + end
  179 + end
  180 +
  181 + context "create issue button" do
  182 + let(:button_matcher) { match(/create issue/) }
  183 +
  184 + it "should not exist for problem's app without issue tracker" do
  185 + err = Fabricate :err
  186 + get :show, :app_id => err.app.id, :id => err.problem.id
  187 +
  188 + response.body.should_not button_matcher
  189 + end
  190 +
  191 + it "should exist for problem's app with issue tracker" do
  192 + tracker = Fabricate(:lighthouse_tracker)
  193 + err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app))
  194 + get :show, :app_id => err.app.id, :id => err.problem.id
  195 +
  196 + response.body.should button_matcher
  197 + end
  198 +
  199 + it "should not exist for problem with issue_link" do
  200 + tracker = Fabricate(:lighthouse_tracker)
  201 + err = Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app, :issue_link => "http://some.host"))
  202 + get :show, :app_id => err.app.id, :id => err.problem.id
  203 +
  204 + response.body.should_not button_matcher
  205 + end
  206 + end
  207 + end
  208 +
  209 + context 'when logged in as a user' do
  210 + before do
  211 + sign_in(@user = Fabricate(:user))
  212 + @unwatched_err = Fabricate(:err)
  213 + @watched_app = Fabricate(:app)
  214 + @watcher = Fabricate(:user_watcher, :user => @user, :app => @watched_app)
  215 + @watched_err = Fabricate(:err, :problem => Fabricate(:problem, :app => @watched_app))
  216 + end
  217 +
  218 + it 'finds the problem if the user is watching the app' do
  219 + get :show, :app_id => @watched_app.to_param, :id => @watched_err.problem.id
  220 + assigns(:problem).should == @watched_err.problem
  221 + end
  222 +
  223 + it 'raises a DocumentNotFound error if the user is not watching the app' do
  224 + lambda {
  225 + get :show, :app_id => @unwatched_err.problem.app_id, :id => @unwatched_err.problem.id
  226 + }.should raise_error(Mongoid::Errors::DocumentNotFound)
  227 + end
  228 + end
  229 + end
  230 +
  231 + describe "PUT /apps/:app_id/problems/:id/resolve" do
  232 + before do
  233 + sign_in Fabricate(:admin)
  234 +
  235 + @problem = Fabricate(:err)
  236 + App.stub(:find).with(@problem.app.id).and_return(@problem.app)
  237 + @problem.app.problems.stub(:find).and_return(@problem.problem)
  238 + @problem.problem.stub(:resolve!)
  239 + end
  240 +
  241 + it 'finds the app and the problem' do
  242 + App.should_receive(:find).with(@problem.app.id).and_return(@problem.app)
  243 + @problem.app.problems.should_receive(:find).and_return(@problem.problem)
  244 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  245 + assigns(:app).should == @problem.app
  246 + assigns(:problem).should == @problem.problem
  247 + end
  248 +
  249 + it "should resolve the issue" do
  250 + @problem.problem.should_receive(:resolve!).and_return(true)
  251 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  252 + end
  253 +
  254 + it "should display a message" do
  255 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  256 + request.flash[:success].should match(/Great news/)
  257 + end
  258 +
  259 + it "should redirect to the app page" do
  260 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  261 + response.should redirect_to(app_path(@problem.app))
  262 + end
  263 +
  264 + it "should redirect back to problems page" do
  265 + request.env["Referer"] = problems_path
  266 + put :resolve, :app_id => @problem.app.id, :id => @problem.problem.id
  267 + response.should redirect_to(problems_path)
  268 + end
  269 + end
  270 +
  271 + describe "POST /apps/:app_id/problems/:id/create_issue" do
  272 + render_views
  273 +
  274 + before(:each) do
  275 + sign_in Fabricate(:admin)
  276 + end
  277 +
  278 + context "successful issue creation" do
  279 + context "lighthouseapp tracker" do
  280 + let(:notice) { Fabricate :notice }
  281 + let(:tracker) { Fabricate :lighthouse_tracker, :app => notice.app }
  282 + let(:problem) { notice.problem }
  283 +
  284 + before(:each) do
  285 + number = 5
  286 + @issue_link = "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets/#{number}.xml"
  287 + body = "<ticket><number type=\"integer\">#{number}</number></ticket>"
  288 + stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").
  289 + to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
  290 +
  291 + post :create_issue, :app_id => problem.app.id, :id => problem.id
  292 + problem.reload
  293 + end
  294 +
  295 + it "should redirect to problem page" do
  296 + response.should redirect_to( app_problem_path(problem.app, problem) )
  297 + end
  298 + end
  299 + end
  300 +
  301 + context "absent issue tracker" do
  302 + let(:problem) { Fabricate :problem }
  303 +
  304 + before(:each) do
  305 + post :create_issue, :app_id => problem.app.id, :id => problem.id
  306 + end
  307 +
  308 + it "should redirect to problem page" do
  309 + response.should redirect_to( app_problem_path(problem.app, problem) )
  310 + end
  311 +
  312 + it "should set flash error message telling issue tracker of the app doesn't exist" do
  313 + flash[:error].should == "This app has no issue tracker setup."
  314 + end
  315 + end
  316 +
  317 + context "error during request to a tracker" do
  318 + context "lighthouseapp tracker" do
  319 + let(:tracker) { Fabricate :lighthouse_tracker }
  320 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => tracker.app)) }
  321 +
  322 + before(:each) do
  323 + stub_request(:post, "http://#{tracker.account}.lighthouseapp.com/projects/#{tracker.project_id}/tickets.xml").to_return(:status => 500)
  324 +
  325 + post :create_issue, :app_id => err.app.id, :id => err.problem.id
  326 + end
  327 +
  328 + it "should redirect to problem page" do
  329 + response.should redirect_to( app_problem_path(err.app, err.problem) )
  330 + end
  331 +
  332 + it "should notify of connection error" do
  333 + flash[:error].should include("There was an error during issue creation:")
  334 + end
  335 + end
  336 + end
  337 + end
  338 +
  339 + describe "DELETE /apps/:app_id/problems/:id/unlink_issue" do
  340 + before(:each) do
  341 + sign_in Fabricate(:admin)
  342 + end
  343 +
  344 + context "problem with issue" do
  345 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :issue_link => "http://some.host")) }
  346 +
  347 + before(:each) do
  348 + delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
  349 + err.problem.reload
  350 + end
  351 +
  352 + it "should redirect to problem page" do
  353 + response.should redirect_to( app_problem_path(err.app, err.problem) )
  354 + end
  355 +
  356 + it "should clear issue link" do
  357 + err.problem.issue_link.should be_nil
  358 + end
  359 + end
  360 +
  361 + context "err without issue" do
  362 + let(:err) { Fabricate :err }
  363 +
  364 + before(:each) do
  365 + delete :unlink_issue, :app_id => err.app.id, :id => err.problem.id
  366 + err.problem.reload
  367 + end
  368 +
  369 + it "should redirect to problem page" do
  370 + response.should redirect_to( app_problem_path(err.app, err.problem) )
  371 + end
  372 + end
  373 + end
  374 +
  375 + describe "Bulk Actions" do
  376 + before(:each) do
  377 + sign_in Fabricate(:admin)
  378 + @problem1 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => true)).problem
  379 + @problem2 = Fabricate(:err, :problem => Fabricate(:problem, :resolved => false)).problem
  380 + end
  381 +
  382 + it "should apply to multiple problems" do
  383 + post :resolve_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  384 + assigns(:selected_problems).should == [@problem1, @problem2]
  385 + end
  386 +
  387 + it "should require at least one problem" do
  388 + post :resolve_several, :problems => []
  389 + request.flash[:notice].should match(/You have not selected any/)
  390 + end
  391 +
  392 + context "POST /problems/merge_several" do
  393 + it "should require at least two problems" do
  394 + post :merge_several, :problems => [@problem1.id.to_s]
  395 + request.flash[:notice].should match(/You must select at least two/)
  396 + end
  397 +
  398 + it "should merge the problems" do
  399 + lambda {
  400 + post :merge_several, :problems => [@problem1.id.to_s, @problem2.id.to_s]
  401 + assigns(:merged_problem).reload.errs.length.should == 2
  402 + }.should change(Problem, :count).by(-1)
  403 + end
  404 + end
  405 +
  406 + context "POST /problems/unmerge_several" do
  407 + it "should unmerge a merged problem" do
  408 + merged_problem = Problem.merge!(@problem1, @problem2)
  409 + merged_problem.errs.length.should == 2
  410 + lambda {
  411 + post :unmerge_several, :problems => [merged_problem.id.to_s]
  412 + merged_problem.reload.errs.length.should == 1
  413 + }.should change(Problem, :count).by(1)
  414 + end
  415 + end
  416 +
  417 + context "POST /problems/resolve_several" do
  418 + it "should resolve the issue" do
  419 + post :resolve_several, :problems => [@problem2.id.to_s]
  420 + @problem2.reload.resolved?.should == true
  421 + end
  422 + end
  423 +
  424 + context "POST /problems/unresolve_several" do
  425 + it "should unresolve the issue" do
  426 + post :unresolve_several, :problems => [@problem1.id.to_s]
  427 + @problem1.reload.resolved?.should == false
  428 + end
  429 + end
  430 +
  431 + context "POST /problems/destroy_several" do
  432 + it "should delete the problems" do
  433 + lambda {
  434 + post :destroy_several, :problems => [@problem1.id.to_s]
  435 + }.should change(Problem, :count).by(-1)
  436 + end
  437 + end
  438 + end
  439 +
  440 +end
  441 +
... ...
spec/fabricators/err_fabricator.rb
... ... @@ -9,19 +9,19 @@ end
9 9 Fabricator :notice do
10 10 err!
11 11 message 'FooError: Too Much Bar'
12   - backtrace { random_backtrace }
  12 + backtrace!
13 13 server_environment { {'environment-name' => 'production'} }
14 14 request {{ 'component' => 'foo', 'action' => 'bar' }}
15 15 notifier {{ 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' }}
16 16 end
17 17  
18   -def random_backtrace
19   - backtrace = []
20   - 99.times {|t| backtrace << {
21   - 'number' => rand(999),
22   - 'file' => "/path/to/file/#{SecureRandom.hex(4)}.rb",
23   - 'method' => ActiveSupport.methods.shuffle.first
24   - }}
25   - backtrace
  18 +Fabricator :backtrace do
  19 + fingerprint "fingerprint"
  20 + lines(:count => 99) { Fabricate.build(:backtrace_line) }
26 21 end
27 22  
  23 +Fabricator :backtrace_line do
  24 + number { rand(999) }
  25 + file { "/path/to/file/#{SecureRandom.hex(4)}.rb" }
  26 + method(:method) { ActiveSupport.methods.shuffle.first }
  27 +end
... ...
spec/fabricators/issue_tracker_fabricator.rb
... ... @@ -25,3 +25,7 @@ Fabricator :github_issues_tracker, :from =&gt; :issue_tracker, :class_name =&gt; &quot;Issu
25 25 username 'test_username'
26 26 end
27 27  
  28 +Fabricator :bitbucket_issues_tracker, :from => :issue_tracker, :class_name => "IssueTrackers::BitbucketIssuesTracker" do
  29 + project_id 'password'
  30 + api_token 'test_username'
  31 +end
... ...
spec/fabricators/notification_service_fabricator.rb 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +Fabricator :notification_service do
  2 + app!
  3 + room_id { sequence :word }
  4 + api_token { sequence :word }
  5 + subdomain { sequence :word }
  6 +end
  7 +
  8 +%w(campfire gtalk hipchat hoiio pushover).each do |t|
  9 + Fabricator "#{t}_notification_service".to_sym, :from => :notification_service, :class_name => "NotificationService::#{t.camelcase}Service"
  10 +end
... ...
spec/fixtures/hoptoad_test_notice.xml
1 1 <?xml version="1.0" encoding="UTF-8"?>
2   -<notice version="2.0">
  2 +<notice version="2.3">
3 3 <api-key>APIKEY</api-key>
4 4 <notifier>
5 5 <name>Hoptoad Notifier</name>
... ... @@ -144,4 +144,10 @@
144 144 <project-root>/path/to/sample/project</project-root>
145 145 <environment-name>development</environment-name>
146 146 </server-environment>
  147 + <current-user>
  148 + <id>123</id>
  149 + <name>Mr. Bean</name>
  150 + <email>mr.bean@example.com</email>
  151 + <username>mrbean</username>
  152 + </current-user>
147 153 </notice>
... ...
spec/helpers/errs_helper_spec.rb
... ... @@ -1,12 +0,0 @@
1   -require 'spec_helper'
2   -
3   -describe ErrsHelper do
4   - describe '#truncated_err_message' do
5   - it 'is html safe' do
6   - problem = double('problem', :message => '#<NoMethodError: ...>')
7   - truncated = helper.truncated_err_message(problem)
8   - truncated.should be_html_safe
9   - truncated.should_not include('<', '>')
10   - end
11   - end
12   -end
spec/helpers/problems_helper_spec.rb 0 → 100644
... ... @@ -0,0 +1,35 @@
  1 +require 'spec_helper'
  2 +
  3 +describe ProblemsHelper do
  4 + describe '#truncated_problem_message' do
  5 + it 'is html safe' do
  6 + problem = double('problem', :message => '#<NoMethodError: ...>')
  7 + truncated = helper.truncated_problem_message(problem)
  8 + truncated.should be_html_safe
  9 + truncated.should_not include('<', '>')
  10 + end
  11 + end
  12 +
  13 + describe "#gravatar_tag" do
  14 + let(:email) { "gravatar@example.com" }
  15 + let(:email_hash) { Digest::MD5.hexdigest email }
  16 + let(:base_url) { "http://www.gravatar.com/avatar/#{email_hash}" }
  17 +
  18 + context "default config" do
  19 + before do
  20 + Errbit::Config.stub(:use_gravatar).and_return(true)
  21 + Errbit::Config.stub(:gravatar_default).and_return('identicon')
  22 + end
  23 +
  24 + it "should render image_tag with correct alt and src" do
  25 + expected = "<img alt=\"#{email}\" class=\"gravatar\" src=\"#{base_url}?d=identicon&amp;s=48\" />"
  26 + helper.gravatar_tag(email, :s => 48).should eq(expected)
  27 + end
  28 +
  29 + it "should override :d" do
  30 + expected = "<img alt=\"#{email}\" class=\"gravatar\" src=\"#{base_url}?d=retro&amp;s=48\" />"
  31 + helper.gravatar_tag(email, :d => 'retro', :s => 48).should eq(expected)
  32 + end
  33 + end
  34 + end
  35 +end
... ...
spec/models/app_spec.rb
... ... @@ -200,8 +200,8 @@ describe App do
200 200  
201 201 it 'captures the backtrace' do
202 202 @notice = App.report_error!(@xml)
203   - @notice.backtrace.size.should == 73
204   - @notice.backtrace.last['file'].should == '[GEM_ROOT]/bin/rake'
  203 + @notice.backtrace_lines.size.should == 73
  204 + @notice.backtrace_lines.last['file'].should == '[GEM_ROOT]/bin/rake'
205 205 end
206 206  
207 207 it 'captures the server_environment' do
... ... @@ -228,8 +228,17 @@ describe App do
228 228 it "should handle params with only a single line of backtrace" do
229 229 xml = Rails.root.join('spec','fixtures','hoptoad_test_notice_with_one_line_of_backtrace.xml').read
230 230 lambda { @notice = App.report_error!(xml) }.should_not raise_error
231   - @notice.backtrace.length.should == 1
  231 + @notice.backtrace_lines.length.should == 1
232 232 end
  233 +
  234 + it 'captures the current_user' do
  235 + @notice = App.report_error!(@xml)
  236 + @notice.current_user['id'].should == '123'
  237 + @notice.current_user['name'].should == 'Mr. Bean'
  238 + @notice.current_user['email'].should == 'mr.bean@example.com'
  239 + @notice.current_user['username'].should == 'mrbean'
  240 + end
  241 +
233 242 end
234 243  
235 244  
... ...
spec/models/backtrace_line_normalizer_spec.rb 0 → 100644
... ... @@ -0,0 +1,14 @@
  1 +require 'spec_helper'
  2 +
  3 +describe BacktraceLineNormalizer do
  4 + subject { described_class.new(raw_line).call }
  5 +
  6 + describe "sanitize file" do
  7 + let(:raw_line) { { 'number' => rand(999), 'file' => nil, 'method' => ActiveSupport.methods.shuffle.first.to_s } }
  8 +
  9 + it "should replace nil file with [unknown source]" do
  10 + subject['file'].should == "[unknown source]"
  11 + end
  12 +
  13 + end
  14 +end
... ...
spec/models/backtrace_spec.rb 0 → 100644
... ... @@ -0,0 +1,46 @@
  1 +require 'spec_helper'
  2 +
  3 +describe Backtrace do
  4 + subject { described_class.new }
  5 +
  6 + its(:fingerprint) { should be_present }
  7 +
  8 + describe "#similar" do
  9 + context "no similar backtrace" do
  10 + its(:similar) { should be_nil }
  11 + end
  12 +
  13 + context "similar backtrace exist" do
  14 + let!(:similar_backtrace) { Fabricate(:backtrace, :fingerprint => fingerprint) }
  15 + let(:fingerprint) { "fingerprint" }
  16 +
  17 + before { subject.stub(:fingerprint => fingerprint) }
  18 +
  19 + its(:similar) { should == similar_backtrace }
  20 + end
  21 + end
  22 +
  23 + describe "find_or_create" do
  24 + subject { described_class.find_or_create(attributes) }
  25 + let(:attributes) { mock :attributes }
  26 + let(:backtrace) { mock :backtrace }
  27 +
  28 + before { described_class.stub(:new => backtrace) }
  29 +
  30 + context "no similar backtrace" do
  31 + before { backtrace.stub(:similar => nil) }
  32 + it "create new backtrace" do
  33 + described_class.should_receive(:create).with(attributes)
  34 +
  35 + described_class.find_or_create(attributes)
  36 + end
  37 + end
  38 +
  39 + context "similar backtrace exist" do
  40 + let(:similar_backtrace) { mock :similar_backtrace }
  41 + before { backtrace.stub(:similar => similar_backtrace) }
  42 +
  43 + it { should == similar_backtrace }
  44 + end
  45 + end
  46 +end
... ...
spec/models/err_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe Err do
4   -
5   - context 'validations' do
6   - it 'requires a error_class' do
7   - err = Fabricate.build(:err, :error_class => nil)
8   - err.should_not be_valid
9   - err.errors[:error_class].should include("can't be blank")
10   - end
11   -
12   - it 'requires an environment' do
13   - err = Fabricate.build(:err, :environment => nil)
14   - err.should_not be_valid
15   - err.errors[:environment].should include("can't be blank")
16   - end
  4 + it 'sets a default error_class and environment' do
  5 + err = Err.new
  6 + err.error_class.should == "UnknownError"
  7 + err.environment.should == "unknown"
17 8 end
18   -
19 9 end
20 10  
... ...
spec/models/issue_trackers/bitbucket_issues_tracker_spec.rb 0 → 100644
... ... @@ -0,0 +1,41 @@
  1 +require 'spec_helper'
  2 +
  3 +describe IssueTrackers::BitbucketIssuesTracker do
  4 + it "should create an issue on BitBucket Issues with problem params, and set issue link for problem" do
  5 + repo = "test_user/test_repo"
  6 + notice = Fabricate :notice
  7 + notice.app.bitbucket_repo = repo
  8 + tracker = Fabricate :bitbucket_issues_tracker, :app => notice.app
  9 + problem = notice.problem
  10 +
  11 + number = 123
  12 + @issue_link = "https://bitbucket.org/#{repo}/issue/#{number}/"
  13 + body = <<EOF
  14 +{
  15 + "status": "new",
  16 + "priority": "critical",
  17 + "title": "[production][foo#bar] FooError: Too Much Bar",
  18 + "comment_count": 0,
  19 + "content": "This is the content",
  20 + "created_on": "2012-07-29 04:35:38",
  21 + "local_id": 123,
  22 + "follower_count": 0,
  23 + "utc_created_on": "2012-07-29 02:35:38+00:00",
  24 + "resource_uri": "/1.0/repositories/test_user/test_repo/issue/123/",
  25 + "is_spam": false
  26 +}
  27 +EOF
  28 +
  29 + stub_request(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_username/test_repo/issues/").to_return(:status => 200, :headers => {}, :body => body )
  30 +
  31 + problem.app.issue_tracker.create_issue(problem)
  32 + problem.reload
  33 +
  34 + requested = have_requested(:post, "https://#{tracker.api_token}:#{tracker.project_id}@bitbucket.org/api/1.0/repositories/test_username/test_repo/issues/")
  35 + WebMock.should requested.with(:title => /[production][foo#bar] FooError: Too Much Bar/)
  36 + WebMock.should requested.with(:content => /See this exception on Errbit/)
  37 +
  38 + problem.issue_link.should == @issue_link
  39 + end
  40 +end
  41 +
... ...
spec/models/notice_observer_spec.rb
... ... @@ -25,7 +25,6 @@ describe NoticeObserver do
25 25 end
26 26  
27 27 describe "email notifications for a resolved issue" do
28   -
29 28 before do
30 29 Errbit::Config.per_app_email_at_notices = true
31 30 @app = Fabricate(:app_with_watcher, :email_at_notices => [1])
... ... @@ -43,4 +42,65 @@ describe NoticeObserver do
43 42 Fabricate(:notice, :err => @err)
44 43 end
45 44 end
  45 +
  46 + describe "should send a notification if a notification service is configured" do
  47 + let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:campfire_notification_service))}
  48 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }
  49 + let(:backtrace) { Fabricate(:backtrace) }
  50 +
  51 + before do
  52 + Errbit::Config.per_app_email_at_notices = true
  53 + end
  54 +
  55 + after do
  56 + Errbit::Config.per_app_email_at_notices = false
  57 + end
  58 +
  59 + it "should create a campfire notification" do
  60 + app.notification_service.should_receive(:create_notification)
  61 +
  62 + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},
  63 + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' })
  64 + end
  65 + end
  66 +
  67 + describe "should not send a notification if a notification service is not configured" do
  68 + let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:notification_service))}
  69 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }
  70 + let(:backtrace) { Fabricate(:backtrace) }
  71 +
  72 + before do
  73 + Errbit::Config.per_app_email_at_notices = true
  74 + end
  75 +
  76 + after do
  77 + Errbit::Config.per_app_email_at_notices = false
  78 + end
  79 +
  80 + it "should not create a campfire notification" do
  81 + app.notification_service.should_not_receive(:create_notification)
  82 +
  83 + Notice.create!(:err => err, :message => 'FooError: Too Much Bar', :server_environment => {'environment-name' => 'production'},
  84 + :backtrace => backtrace, :notifier => { 'name' => 'Notifier', 'version' => '1', 'url' => 'http://toad.com' })
  85 + end
  86 + end
  87 +
  88 + describe 'hipcat notifications' do
  89 + let(:app) { Fabricate(:app, :email_at_notices => [1], :notification_service => Fabricate(:hipchat_notification_service))}
  90 + let(:err) { Fabricate(:err, :problem => Fabricate(:problem, :app => app, :notices_count => 100)) }
  91 +
  92 + before do
  93 + Errbit::Config.per_app_email_at_notices = true
  94 + end
  95 +
  96 + after do
  97 + Errbit::Config.per_app_email_at_notices = false
  98 + end
  99 +
  100 + it 'creates a hipchat notification' do
  101 + app.notification_service.should_receive(:create_notification)
  102 +
  103 + Fabricate(:notice, :err => err)
  104 + end
  105 + end
46 106 end
... ...
spec/models/notification_service/campfire_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::CampfireService do
  4 + it "it should send a notification to campfire" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :campfire_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + #campy stubbing
  11 + campy = mock('CampfireService')
  12 + Campy::Room.stub(:new).and_return(campy)
  13 + campy.stub(:speak) { true }
  14 +
  15 + #assert
  16 + campy.should_receive(:speak)
  17 +
  18 + notification_service.create_notification(problem)
  19 + end
  20 +end
  21 +
... ...
spec/models/notification_service/gtalk_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::GtalkService do
  4 + it "it should send a notification to gtalk" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :gtalk_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + #gtalk stubbing
  11 + gtalk = mock('GtalkService')
  12 + jid = double("jid")
  13 + message = double("message")
  14 + Jabber::JID.should_receive(:new).with(notification_service.subdomain).and_return(jid)
  15 + Jabber::Client.should_receive(:new).with(jid).and_return(gtalk)
  16 + gtalk.should_receive(:connect)
  17 + gtalk.should_receive(:auth).with(notification_service.api_token)
  18 + Jabber::Message.should_receive(:new).with(notification_service.room_id, "[errbit] http://#{Errbit::Config.host}/apps/#{problem.app.id.to_s} #{notification_service.notification_description problem}").and_return(message)
  19 +
  20 + #assert
  21 + gtalk.should_receive(:send).with(message)
  22 +
  23 +
  24 + notification_service.create_notification(problem)
  25 + end
  26 +end
  27 +
... ...
spec/models/notification_service/hipchat_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationServices::HipchatService do
  4 + let(:service) { Fabricate.build(:hipchat_notification_service) }
  5 + let(:problem) { Fabricate(:problem) }
  6 + let(:room) { double }
  7 +
  8 + before do
  9 + HipChat::Client.any_instance.stub(:[] => room)
  10 + end
  11 +
  12 + it 'sends message' do
  13 + room.should_receive(:send)
  14 + service.create_notification(problem)
  15 + end
  16 +
  17 + it 'escapes html in message' do
  18 + service.stub(:notification_description => '<3')
  19 + room.should_receive(:send) do |_, message|
  20 + message.should_not include('<3')
  21 + message.should include('&lt;3')
  22 + end
  23 + service.create_notification(problem)
  24 + end
  25 +end
... ...
spec/models/notification_service/hoiio_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,21 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::HoiioService do
  4 + it "it should send a notification to hoiio" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :hoiio_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + # hoi stubbing
  11 + sms = mock('HoiioService')
  12 + Hoi::SMS.stub(:new).and_return(sms)
  13 + sms.stub(:send) { true }
  14 +
  15 + #assert
  16 + sms.should_receive(:send)
  17 +
  18 + notification_service.create_notification(problem)
  19 + end
  20 +end
  21 +
... ...
spec/models/notification_service/pushover_service_spec.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +require 'spec_helper'
  2 +
  3 +describe NotificationService::PushoverService do
  4 + it "it should send a notification to Pushover" do
  5 + # setup
  6 + notice = Fabricate :notice
  7 + notification_service = Fabricate :pushover_notification_service, :app => notice.app
  8 + problem = notice.problem
  9 +
  10 + # hoi stubbing
  11 + notification = mock('PushoverService')
  12 + Rushover::Client.stub(:new).and_return(notification)
  13 + notification.stub(:notify) { true }
  14 +
  15 + #assert
  16 + notification.should_receive(:notify)
  17 +
  18 + notification_service.create_notification(problem)
  19 + end
  20 +end
0 21 \ No newline at end of file
... ...
spec/models/problem_spec.rb
... ... @@ -24,14 +24,13 @@ describe Problem do
24 24 end
25 25 end
26 26 end
  27 +
27 28 context '#last_notice_at' do
28 29 it "returns the created_at timestamp of the latest notice" do
29 30 err = Fabricate(:err)
30 31 problem = err.problem
31 32 problem.should_not be_nil
32 33  
33   - problem.last_notice_at.should be_nil
34   -
35 34 notice1 = Fabricate(:notice, :err => err)
36 35 problem.last_notice_at.should == notice1.created_at
37 36  
... ... @@ -40,6 +39,20 @@ describe Problem do
40 39 end
41 40 end
42 41  
  42 + context '#first_notice_at' do
  43 + it "returns the created_at timestamp of the first notice" do
  44 + err = Fabricate(:err)
  45 + problem = err.problem
  46 + problem.should_not be_nil
  47 +
  48 + notice1 = Fabricate(:notice, :err => err)
  49 + problem.first_notice_at.should == notice1.created_at
  50 +
  51 + notice2 = Fabricate(:notice, :err => err)
  52 + problem.first_notice_at.should == notice1.created_at
  53 + end
  54 + end
  55 +
43 56  
44 57 context '#message' do
45 58 it "adding a notice caches its message" do
... ... @@ -87,6 +100,24 @@ describe Problem do
87 100 problem.should be_resolved
88 101 end
89 102  
  103 + it "should record the time when it was resolved" do
  104 + problem = Fabricate(:problem)
  105 + expected_resolved_at = Time.now
  106 + Timecop.freeze(expected_resolved_at) do
  107 + problem.resolve!
  108 + end
  109 + problem.resolved_at.to_s.should == expected_resolved_at.to_s
  110 + end
  111 +
  112 + it "should not reset notice count" do
  113 + problem = Fabricate(:problem, :notices_count => 1)
  114 + original_notices_count = problem.notices_count
  115 + original_notices_count.should > 0
  116 +
  117 + problem.resolve!
  118 + problem.notices_count.should == original_notices_count
  119 + end
  120 +
90 121 it "should throw an err if it's not successful" do
91 122 problem = Fabricate(:problem)
92 123 problem.should_not be_resolved
... ...
spec/spec_helper.rb
... ... @@ -5,6 +5,7 @@ require File.expand_path(&quot;../../config/environment&quot;, __FILE__)
5 5 require 'rspec/rails'
6 6 require 'database_cleaner'
7 7 require 'webmock/rspec'
  8 +require 'xmpp4r'
8 9  
9 10 # Requires supporting files with custom matchers and macros, etc,
10 11 # in ./support/ and its subdirectories.
... ...
spec/views/errs/show.html.haml_spec.rb
... ... @@ -1,125 +0,0 @@
1   -require 'spec_helper'
2   -
3   -describe "errs/show.html.haml" do
4   - before do
5   - err = Fabricate(:err)
6   - problem = err.problem
7   - comment = Fabricate(:comment)
8   - assign :problem, problem
9   - assign :comment, comment
10   - assign :app, problem.app
11   - assign :notices, err.notices.page(1).per(1)
12   - assign :notice, err.notices.first
13   - controller.stub(:current_user) { Fabricate(:user) }
14   - end
15   -
16   - def with_issue_tracker(tracker, problem)
17   - problem.app.issue_tracker = tracker.new :api_token => "token token token", :project_id => "1234"
18   - assign :problem, problem
19   - assign :app, problem.app
20   - end
21   -
22   - describe "content_for :action_bar" do
23   - def action_bar
24   - view.content_for(:action_bar)
25   - end
26   -
27   - it "should confirm the 'resolve' link by default" do
28   - render
29   -
30   - action_bar.should have_selector('a.resolve[data-confirm="Seriously?"]')
31   - end
32   -
33   - it "should confirm the 'resolve' link if configuration is unset" do
34   - Errbit::Config.stub(:confirm_resolve_err).and_return(nil)
35   - render
36   -
37   - action_bar.should have_selector('a.resolve[data-confirm="Seriously?"]')
38   - end
39   -
40   - it "should not confirm the 'resolve' link if configured not to" do
41   - Errbit::Config.stub(:confirm_resolve_err).and_return(false)
42   - render
43   -
44   - action_bar.should have_selector('a.resolve[data-confirm="null"]')
45   - end
46   -
47   - it "should link 'up' to HTTP_REFERER if is set" do
48   - url = 'http://localhost:3000/errs'
49   - controller.request.env['HTTP_REFERER'] = url
50   - render
51   -
52   - action_bar.should have_selector("span a.up[href='#{url}']", :text => 'up')
53   - end
54   -
55   - it "should link 'up' to app_errs_path if HTTP_REFERER isn't set'" do
56   - controller.request.env['HTTP_REFERER'] = nil
57   - problem = Fabricate(:problem_with_comments)
58   - assign :problem, problem
59   - assign :app, problem.app
60   - render
61   -
62   - action_bar.should have_selector("span a.up[href='#{app_errs_path(problem.app)}']", :text => 'up')
63   - end
64   -
65   - context 'create issue links' do
66   - it 'should allow creating issue for github if current user has linked their github account' do
67   - user = Fabricate(:user, :github_login => 'test_user', :github_oauth_token => 'abcdef')
68   - controller.stub(:current_user) { user }
69   -
70   - problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
71   - assign :problem, problem
72   - assign :app, problem.app
73   - render
74   -
75   - action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
76   - end
77   -
78   - it 'should allow creating issue for github if application has a github tracker' do
79   - problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
80   - with_issue_tracker(GithubIssuesTracker, problem)
81   - assign :problem, problem
82   - assign :app, problem.app
83   - render
84   -
85   - action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
86   - end
87   - end
88   - end
89   -
90   - describe "content_for :comments with comments disabled for configured issue tracker" do
91   - before do
92   - Errbit::Config.stub(:allow_comments_with_issue_tracker).and_return(false)
93   - end
94   -
95   - it 'should display comments and new comment form when no issue tracker' do
96   - problem = Fabricate(:problem_with_comments)
97   - assign :problem, problem
98   - assign :app, problem.app
99   - render
100   -
101   - view.content_for(:comments).should include('Test comment')
102   - view.content_for(:comments).should include('Add a comment')
103   - end
104   -
105   - context "with issue tracker" do
106   - it 'should not display the comments section' do
107   - problem = Fabricate(:problem)
108   - with_issue_tracker(PivotalLabsTracker, problem)
109   - render
110   - view.view_flow.get(:comments).should be_blank
111   - end
112   -
113   - it 'should display existing comments' do
114   - problem = Fabricate(:problem_with_comments)
115   - problem.reload
116   - with_issue_tracker(PivotalLabsTracker, problem)
117   - render
118   -
119   - view.content_for(:comments).should include('Test comment')
120   - view.content_for(:comments).should_not include('Add a comment')
121   - end
122   - end
123   - end
124   -end
125   -
spec/views/notices/_backtrace.html.haml_spec.rb
... ... @@ -1,18 +0,0 @@
1   -require 'spec_helper'
2   -
3   -describe "notices/_backtrace.html.haml" do
4   - describe 'missing file in backtrace' do
5   - let(:notice) do
6   - backtrace = { 'number' => rand(999), 'file' => nil, 'method' => ActiveSupport.methods.shuffle.first }
7   - Fabricate(:notice, :backtrace => [backtrace])
8   - end
9   -
10   - it "should replace nil file with [unknown source]" do
11   - assign :app, notice.err.app
12   -
13   - render "notices/backtrace", :lines => notice.backtrace
14   - rendered.should match(/\[unknown source\]/)
15   - end
16   - end
17   -end
18   -
spec/views/problems/show.html.haml_spec.rb 0 → 100644
... ... @@ -0,0 +1,127 @@
  1 +require 'spec_helper'
  2 +
  3 +describe "problems/show.html.haml" do
  4 + before do
  5 + problem = Fabricate(:problem)
  6 + comment = Fabricate(:comment)
  7 + assign :problem, problem
  8 + assign :comment, comment
  9 + assign :app, problem.app
  10 + assign :notices, problem.notices.page(1).per(1)
  11 + assign :notice, problem.notices.first
  12 + controller.stub(:current_user) { Fabricate(:user) }
  13 + end
  14 +
  15 + def with_issue_tracker(tracker, problem)
  16 + problem.app.issue_tracker = tracker.new :api_token => "token token token", :project_id => "1234"
  17 + assign :problem, problem
  18 + assign :app, problem.app
  19 + end
  20 +
  21 + describe "content_for :action_bar" do
  22 + def action_bar
  23 + view.content_for(:action_bar)
  24 + end
  25 +
  26 + it "should confirm the 'resolve' link by default" do
  27 + render
  28 +
  29 + action_bar.should have_selector('a.resolve[data-confirm="Seriously?"]')
  30 + end
  31 +
  32 + it "should confirm the 'resolve' link if configuration is unset" do
  33 + Errbit::Config.stub(:confirm_resolve_err).and_return(nil)
  34 + render
  35 +
  36 + action_bar.should have_selector('a.resolve[data-confirm="Seriously?"]')
  37 + end
  38 +
  39 + it "should not confirm the 'resolve' link if configured not to" do
  40 + Errbit::Config.stub(:confirm_resolve_err).and_return(false)
  41 + render
  42 +
  43 + action_bar.should have_selector('a.resolve[data-confirm="null"]')
  44 + end
  45 +
  46 + it "should link 'up' to HTTP_REFERER if is set" do
  47 + url = 'http://localhost:3000/problems'
  48 + controller.request.env['HTTP_REFERER'] = url
  49 + render
  50 +
  51 + action_bar.should have_selector("span a.up[href='#{url}']", :text => 'up')
  52 + end
  53 +
  54 + it "should link 'up' to app_problems_path if HTTP_REFERER isn't set'" do
  55 + controller.request.env['HTTP_REFERER'] = nil
  56 + problem = Fabricate(:problem_with_comments)
  57 + assign :problem, problem
  58 + assign :app, problem.app
  59 + render
  60 +
  61 + action_bar.should have_selector("span a.up[href='#{app_problems_path(problem.app)}']", :text => 'up')
  62 + end
  63 +
  64 + context 'create issue links' do
  65 + it 'should allow creating issue for github if current user has linked their github account' do
  66 + user = Fabricate(:user, :github_login => 'test_user', :github_oauth_token => 'abcdef')
  67 + controller.stub(:current_user) { user }
  68 +
  69 + problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
  70 + assign :problem, problem
  71 + assign :app, problem.app
  72 + render
  73 +
  74 + action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
  75 + end
  76 +
  77 + it 'should allow creating issue for github if application has a github tracker' do
  78 + problem = Fabricate(:problem_with_comments, :app => Fabricate(:app, :github_repo => "test_user/test_repo"))
  79 + with_issue_tracker(GithubIssuesTracker, problem)
  80 + assign :problem, problem
  81 + assign :app, problem.app
  82 + render
  83 +
  84 + action_bar.should have_selector("span a.github_create.create-issue", :text => 'create issue')
  85 + end
  86 + end
  87 + end
  88 +
  89 + describe "content_for :comments with comments disabled for configured issue tracker" do
  90 + before do
  91 + Errbit::Config.stub(:allow_comments_with_issue_tracker).and_return(false)
  92 + Errbit::Config.stub(:use_gravatar).and_return(true)
  93 + end
  94 +
  95 + it 'should display comments and new comment form when no issue tracker' do
  96 + problem = Fabricate(:problem_with_comments)
  97 + assign :problem, problem
  98 + assign :app, problem.app
  99 + render
  100 +
  101 + view.content_for(:comments).should include('Test comment')
  102 + view.content_for(:comments).should have_selector('img[src^="http://www.gravatar.com/avatar"]')
  103 + view.content_for(:comments).should include('Add a comment')
  104 + end
  105 +
  106 + context "with issue tracker" do
  107 + it 'should not display the comments section' do
  108 + problem = Fabricate(:problem)
  109 + with_issue_tracker(PivotalLabsTracker, problem)
  110 + render
  111 + view.view_flow.get(:comments).should be_blank
  112 + end
  113 +
  114 + it 'should display existing comments' do
  115 + problem = Fabricate(:problem_with_comments)
  116 + problem.reload
  117 + with_issue_tracker(PivotalLabsTracker, problem)
  118 + render
  119 +
  120 + view.content_for(:comments).should include('Test comment')
  121 + view.content_for(:comments).should have_selector('img[src^="http://www.gravatar.com/avatar"]')
  122 + view.content_for(:comments).should_not include('Add a comment')
  123 + end
  124 + end
  125 + end
  126 +end
  127 +
... ...
spec/views/users/show.html.haml_spec.rb
... ... @@ -2,7 +2,7 @@ require &#39;spec_helper&#39;
2 2  
3 3 describe 'users/show.html.haml' do
4 4 let(:user) do
5   - stub_model(User, :created_at => Time.now)
  5 + stub_model(User, :created_at => Time.now, :email => "test@example.com")
6 6 end
7 7  
8 8 before do
... ...