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

Too many changes.

To preserve performance only 100 of 163 files displayed.

.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  
... ...