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.

@@ -3,6 +3,7 @@ rvm: @@ -3,6 +3,7 @@ rvm:
3 - 1.9.3 3 - 1.9.3
4 - 1.9.2 4 - 1.9.2
5 - 1.8.7 5 - 1.8.7
  6 +services: mongodb
6 7
7 # To stop Travis from running tests for a new commit, 8 # To stop Travis from running tests for a new commit,
8 # add the following to your commit message: [ci skip] 9 # add the following to your commit message: [ci skip]
1 source 'http://rubygems.org' 1 source 'http://rubygems.org'
2 2
3 -gem 'rails', '3.2.6'  
4 -  
5 -gem 'nokogiri' 3 +gem 'rails', '3.2.8'
6 gem 'mongoid', '~> 2.4.10' 4 gem 'mongoid', '~> 2.4.10'
7 - 5 +gem 'mongoid_rails_migrations'
  6 +gem 'devise', '~> 1.5.3'
  7 +gem 'nokogiri'
8 gem 'haml' 8 gem 'haml'
9 gem 'htmlentities', "~> 4.3.0" 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 gem 'lighthouse-api' 31 gem 'lighthouse-api'
  32 +# Redmine
17 gem 'oruen_redmine_client', :require => 'redmine_client' 33 gem 'oruen_redmine_client', :require => 'redmine_client'
18 -gem 'mongoid_rails_migrations'  
19 -gem 'useragent', '~> 0.3.1' 34 +# Pivotal Tracker
20 gem 'pivotal-tracker' 35 gem 'pivotal-tracker'
  36 +# Fogbugz
21 gem 'ruby-fogbugz', :require => 'fogbugz' 37 gem 'ruby-fogbugz', :require => 'fogbugz'
22 - 38 +# Github Issues
23 gem 'octokit', '~> 1.0.0' 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 platform :ruby do 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 end 67 end
39 68
  69 +gem 'omniauth'
  70 +gem 'oa-core'
40 gem 'ri_cal' 71 gem 'ri_cal'
41 -gem 'yajl-ruby' 72 +gem 'yajl-ruby', :require => "yajl"
42 73
43 group :development, :test do 74 group :development, :test do
44 gem 'rspec-rails', '~> 2.6' 75 gem 'rspec-rails', '~> 2.6'
@@ -46,8 +77,13 @@ group :development, :test do @@ -46,8 +77,13 @@ group :development, :test do
46 unless ENV["CI"] 77 unless ENV["CI"]
47 gem 'ruby-debug', :platform => :mri_18 78 gem 'ruby-debug', :platform => :mri_18
48 gem 'debugger', :platform => :mri_19 79 gem 'debugger', :platform => :mri_19
  80 + gem 'pry'
  81 + gem 'pry-rails'
49 end 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 end 87 end
52 88
53 group :test do 89 group :test do
@@ -56,6 +92,7 @@ group :test do @@ -56,6 +92,7 @@ group :test do
56 gem 'rspec', '~> 2.6' 92 gem 'rspec', '~> 2.6'
57 gem 'database_cleaner', '~> 0.6.0' 93 gem 'database_cleaner', '~> 0.6.0'
58 gem 'email_spec' 94 gem 'email_spec'
  95 + gem 'timecop'
59 end 96 end
60 97
61 group :heroku do 98 group :heroku do
@@ -72,3 +109,5 @@ group :assets do @@ -72,3 +109,5 @@ group :assets do
72 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows 109 gem 'therubyracer', :platform => :ruby # C Ruby (MRI) or Rubinius, but NOT Windows
73 gem 'uglifier', '>= 1.0.3' 110 gem 'uglifier', '>= 1.0.3'
74 end 111 end
  112 +
  113 +gem 'turbo-sprockets-rails3'
@@ -2,43 +2,60 @@ GEM @@ -2,43 +2,60 @@ GEM
2 remote: http://rubygems.org/ 2 remote: http://rubygems.org/
3 specs: 3 specs:
4 SystemTimer (1.2.3) 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 mail (~> 2.4.4) 7 mail (~> 2.4.4)
8 actionmailer_inline_css (1.3.1) 8 actionmailer_inline_css (1.3.1)
9 actionmailer (>= 3.0.0) 9 actionmailer (>= 3.0.0)
10 nokogiri (>= 1.4.4) 10 nokogiri (>= 1.4.4)
11 premailer (>= 1.7.1) 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 builder (~> 3.0.0) 15 builder (~> 3.0.0)
16 erubis (~> 2.7.0) 16 erubis (~> 2.7.0)
17 - journey (~> 1.0.1) 17 + journey (~> 1.0.4)
18 rack (~> 1.4.0) 18 rack (~> 1.4.0)
19 rack-cache (~> 1.2) 19 rack-cache (~> 1.2)
20 rack-test (~> 0.6.1) 20 rack-test (~> 0.6.1)
21 sprockets (~> 2.1.3) 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 builder (~> 3.0.0) 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 arel (~> 3.0.2) 28 arel (~> 3.0.2)
29 tzinfo (~> 0.3.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 i18n (~> 0.6) 34 i18n (~> 0.6)
35 multi_json (~> 1.0) 35 multi_json (~> 1.0)
36 - addressable (2.2.8) 36 + addressable (2.3.2)
37 arel (3.0.2) 37 arel (3.0.2)
38 bcrypt-ruby (3.0.1) 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 capybara (1.1.2) 59 capybara (1.1.2)
43 mime-types (>= 1.16) 60 mime-types (>= 1.16)
44 nokogiri (>= 1.3.3) 61 nokogiri (>= 1.3.3)
@@ -46,8 +63,9 @@ GEM @@ -46,8 +63,9 @@ GEM
46 rack-test (>= 0.5.4) 63 rack-test (>= 0.5.4)
47 selenium-webdriver (~> 2.0) 64 selenium-webdriver (~> 2.0)
48 xpath (~> 0.1.4) 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 columnize (0.3.6) 69 columnize (0.3.6)
52 crack (0.3.1) 70 crack (0.3.1)
53 css_parser (1.2.6) 71 css_parser (1.2.6)
@@ -55,13 +73,13 @@ GEM @@ -55,13 +73,13 @@ GEM
55 rdoc 73 rdoc
56 daemons (1.1.8) 74 daemons (1.1.8)
57 database_cleaner (0.6.7) 75 database_cleaner (0.6.7)
58 - debugger (1.1.3) 76 + debugger (1.2.0)
59 columnize (>= 0.3.1) 77 columnize (>= 0.3.1)
60 debugger-linecache (~> 1.1.1) 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 debugger-ruby_core_source (>= 1.1.1) 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 devise (1.5.3) 83 devise (1.5.3)
66 bcrypt-ruby (~> 3.0) 84 bcrypt-ruby (~> 3.0)
67 orm_adapter (~> 0.0.3) 85 orm_adapter (~> 0.0.3)
@@ -75,38 +93,49 @@ GEM @@ -75,38 +93,49 @@ GEM
75 execjs (1.4.0) 93 execjs (1.4.0)
76 multi_json (~> 1.0) 94 multi_json (~> 1.0)
77 fabrication (1.3.2) 95 fabrication (1.3.2)
78 - faraday (0.8.1) 96 + faraday (0.8.4)
79 multipart-post (~> 1.1) 97 multipart-post (~> 1.1)
80 - faraday_middleware (0.8.7) 98 + faraday_middleware (0.8.8)
81 faraday (>= 0.7.4, < 0.9) 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 happymapper (0.4.0) 102 happymapper (0.4.0)
85 libxml-ruby (~> 2.0) 103 libxml-ruby (~> 2.0)
86 has_scope (0.5.1) 104 has_scope (0.5.1)
87 hashie (1.2.0) 105 hashie (1.2.0)
  106 + highline (1.6.15)
88 hike (1.2.1) 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 hoptoad_notifier (2.4.11) 113 hoptoad_notifier (2.4.11)
90 activesupport 114 activesupport
91 builder 115 builder
92 htmlentities (4.3.1) 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 inherited_resources (1.3.1) 122 inherited_resources (1.3.1)
95 has_scope (~> 0.5.0) 123 has_scope (~> 0.5.0)
96 responders (~> 0.6) 124 responders (~> 0.6)
97 journey (1.0.4) 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 actionpack (>= 3.0.0) 130 actionpack (>= 3.0.0)
101 activesupport (>= 3.0.0) 131 activesupport (>= 3.0.0)
102 - railties (>= 3.0.0)  
103 kgio (2.7.4) 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 libv8 (3.3.10.4) 135 libv8 (3.3.10.4)
107 - libwebsocket (0.1.3) 136 + libwebsocket (0.1.5)
108 addressable 137 addressable
109 - libxml-ruby (2.3.2) 138 + libxml-ruby (2.3.3)
110 lighthouse-api (2.0) 139 lighthouse-api (2.0)
111 activeresource (>= 3.0.0) 140 activeresource (>= 3.0.0)
112 activesupport (>= 3.0.0) 141 activesupport (>= 3.0.0)
@@ -116,9 +145,10 @@ GEM @@ -116,9 +145,10 @@ GEM
116 i18n (>= 0.4.0) 145 i18n (>= 0.4.0)
117 mime-types (~> 1.16) 146 mime-types (~> 1.16)
118 treetop (~> 1.4.8) 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 mongoid (2.4.10) 152 mongoid (2.4.10)
123 activemodel (~> 3.1) 153 activemodel (~> 3.1)
124 mongo (~> 1.3) 154 mongo (~> 1.3)
@@ -129,26 +159,37 @@ GEM @@ -129,26 +159,37 @@ GEM
129 rails (>= 3.0.0) 159 rails (>= 3.0.0)
130 railties (>= 3.0.0) 160 railties (>= 3.0.0)
131 multi_json (1.3.6) 161 multi_json (1.3.6)
  162 + multi_xml (0.5.1)
132 multipart-post (1.1.5) 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 oa-core (0.3.2) 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 multi_json (~> 1.0) 177 multi_json (~> 1.0)
  178 + rack (~> 1.2)
138 octokit (1.0.7) 179 octokit (1.0.7)
139 addressable (~> 2.2) 180 addressable (~> 2.2)
140 faraday (~> 0.8) 181 faraday (~> 0.8)
141 faraday_middleware (~> 0.8) 182 faraday_middleware (~> 0.8)
142 hashie (~> 1.2) 183 hashie (~> 1.2)
143 multi_json (~> 1.3) 184 multi_json (~> 1.3)
144 - omniauth (1.0.3) 185 + omniauth (1.1.1)
145 hashie (~> 1.2) 186 hashie (~> 1.2)
146 rack 187 rack
147 - omniauth-github (1.0.1) 188 + omniauth-github (1.0.2)
148 omniauth (~> 1.0) 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 omniauth (~> 1.0) 193 omniauth (~> 1.0)
153 orm_adapter (0.0.7) 194 orm_adapter (0.0.7)
154 oruen_redmine_client (0.0.1) 195 oruen_redmine_client (0.0.1)
@@ -166,54 +207,60 @@ GEM @@ -166,54 +207,60 @@ GEM
166 premailer (1.7.3) 207 premailer (1.7.3)
167 css_parser (>= 1.1.9) 208 css_parser (>= 1.1.9)
168 htmlentities (>= 4.0.0) 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 rack (1.4.1) 216 rack (1.4.1)
170 rack-cache (1.2) 217 rack-cache (1.2)
171 rack (>= 0.4) 218 rack (>= 0.4)
172 rack-ssl (1.3.2) 219 rack-ssl (1.3.2)
173 rack 220 rack
174 rack-ssl-enforcer (0.2.4) 221 rack-ssl-enforcer (0.2.4)
175 - rack-test (0.6.1) 222 + rack-test (0.6.2)
176 rack (>= 1.0) 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 bundler (~> 1.0) 230 bundler (~> 1.0)
184 - railties (= 3.2.6) 231 + railties (= 3.2.8)
185 rails_autolink (1.0.9) 232 rails_autolink (1.0.9)
186 rails (~> 3.1) 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 rack-ssl (~> 1.3.2) 237 rack-ssl (~> 1.3.2)
191 rake (>= 0.8.7) 238 rake (>= 0.8.7)
192 rdoc (~> 3.4) 239 rdoc (~> 3.4)
193 thor (>= 0.14.6, < 2.0) 240 thor (>= 0.14.6, < 2.0)
194 - raindrops (0.8.0) 241 + raindrops (0.10.0)
195 rake (0.9.2.2) 242 rake (0.9.2.2)
196 rbx-require-relative (0.0.9) 243 rbx-require-relative (0.0.9)
197 rdoc (3.12) 244 rdoc (3.12)
198 json (~> 1.4) 245 json (~> 1.4)
199 - responders (0.9.1) 246 + responders (0.9.2)
200 railties (~> 3.1) 247 railties (~> 3.1)
201 rest-client (1.6.7) 248 rest-client (1.6.7)
202 mime-types (>= 1.16) 249 mime-types (>= 1.16)
203 ri_cal (0.8.8) 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 diff-lcs (~> 1.1.3) 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 actionpack (>= 3.0) 260 actionpack (>= 3.0)
214 activesupport (>= 3.0) 261 activesupport (>= 3.0)
215 railties (>= 3.0) 262 railties (>= 3.0)
216 - rspec (~> 2.10.0) 263 + rspec (~> 2.11.0)
217 ruby-debug (0.10.4) 264 ruby-debug (0.10.4)
218 columnize (>= 0.1) 265 columnize (>= 0.1)
219 ruby-debug-base (~> 0.10.4.0) 266 ruby-debug-base (~> 0.10.4.0)
@@ -221,42 +268,51 @@ GEM @@ -221,42 +268,51 @@ GEM
221 linecache (>= 0.3) 268 linecache (>= 0.3)
222 ruby-fogbugz (0.1.1) 269 ruby-fogbugz (0.1.1)
223 crack 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 childprocess (>= 0.2.5) 276 childprocess (>= 0.2.5)
227 - ffi (~> 1.0)  
228 libwebsocket (~> 0.1.3) 277 libwebsocket (~> 0.1.3)
229 multi_json (~> 1.0) 278 multi_json (~> 1.0)
230 rubyzip 279 rubyzip
  280 + simple_oauth (0.1.9)
  281 + slop (2.4.4)
231 sprockets (2.1.3) 282 sprockets (2.1.3)
232 hike (~> 1.2) 283 hike (~> 1.2)
233 rack (~> 1.0) 284 rack (~> 1.0)
234 tilt (~> 1.1, != 1.3.0) 285 tilt (~> 1.1, != 1.3.0)
235 - therubyracer (0.10.1) 286 + therubyracer (0.10.2)
236 libv8 (~> 3.3.10) 287 libv8 (~> 3.3.10)
237 - thin (1.3.1) 288 + thin (1.4.1)
238 daemons (>= 1.0.9) 289 daemons (>= 1.0.9)
239 eventmachine (>= 0.12.6) 290 eventmachine (>= 0.12.6)
240 rack (>= 1.0.0) 291 rack (>= 1.0.0)
241 - thor (0.15.2) 292 + thor (0.16.0)
242 tilt (1.3.3) 293 tilt (1.3.3)
  294 + timecop (0.3.5)
243 treetop (1.4.10) 295 treetop (1.4.10)
244 polyglot 296 polyglot
245 polyglot (>= 0.3.1) 297 polyglot (>= 0.3.1)
  298 + turbo-sprockets-rails3 (0.1.10)
  299 + railties (>= 3.1.0)
  300 + sprockets (>= 2.0.0)
246 tzinfo (0.3.33) 301 tzinfo (0.3.33)
247 - uglifier (1.2.4) 302 + uglifier (1.2.7)
248 execjs (>= 0.3.0) 303 execjs (>= 0.3.0)
249 - multi_json (>= 1.0.2) 304 + multi_json (~> 1.3)
250 unicorn (4.3.1) 305 unicorn (4.3.1)
251 kgio (~> 2.6) 306 kgio (~> 2.6)
252 rack 307 rack
253 raindrops (~> 0.7) 308 raindrops (~> 0.7)
254 useragent (0.3.2) 309 useragent (0.3.2)
255 - warden (1.2.0) 310 + warden (1.2.1)
256 rack (>= 1.0) 311 rack (>= 1.0)
257 - webmock (1.8.6) 312 + webmock (1.8.7)
258 addressable (>= 2.2.7) 313 addressable (>= 2.2.7)
259 crack (>= 0.1.7) 314 crack (>= 0.1.7)
  315 + xmpp4r (0.5)
260 xpath (0.1.4) 316 xpath (0.1.4)
261 nokogiri (~> 1.3) 317 nokogiri (~> 1.3)
262 yajl-ruby (1.1.0) 318 yajl-ruby (1.1.0)
@@ -267,8 +323,12 @@ PLATFORMS @@ -267,8 +323,12 @@ PLATFORMS
267 DEPENDENCIES 323 DEPENDENCIES
268 SystemTimer 324 SystemTimer
269 actionmailer_inline_css (~> 1.3.0) 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 capybara 332 capybara
273 database_cleaner (~> 0.6.0) 333 database_cleaner (~> 0.6.0)
274 debugger 334 debugger
@@ -277,33 +337,43 @@ DEPENDENCIES @@ -277,33 +337,43 @@ DEPENDENCIES
277 execjs 337 execjs
278 fabrication (~> 1.3.0) 338 fabrication (~> 1.3.0)
279 haml 339 haml
  340 + hipchat
  341 + hoi
280 hoptoad_notifier (~> 2.4) 342 hoptoad_notifier (~> 2.4)
281 htmlentities (~> 4.3.0) 343 htmlentities (~> 4.3.0)
282 inherited_resources 344 inherited_resources
283 kaminari 345 kaminari
284 launchy 346 launchy
285 lighthouse-api 347 lighthouse-api
286 - mongo (= 1.3.1) 348 + mongo (= 1.6.2)
287 mongoid (~> 2.4.10) 349 mongoid (~> 2.4.10)
288 mongoid_rails_migrations 350 mongoid_rails_migrations
289 nokogiri 351 nokogiri
290 oa-core 352 oa-core
291 octokit (~> 1.0.0) 353 octokit (~> 1.0.0)
  354 + omniauth
292 omniauth-github 355 omniauth-github
293 oruen_redmine_client 356 oruen_redmine_client
294 pivotal-tracker 357 pivotal-tracker
  358 + pry
  359 + pry-rails
  360 + rack-ssl
295 rack-ssl-enforcer 361 rack-ssl-enforcer
296 - rails (= 3.2.6) 362 + rails (= 3.2.8)
297 rails_autolink (~> 1.0.9) 363 rails_autolink (~> 1.0.9)
298 ri_cal 364 ri_cal
299 rspec (~> 2.6) 365 rspec (~> 2.6)
300 rspec-rails (~> 2.6) 366 rspec-rails (~> 2.6)
301 ruby-debug 367 ruby-debug
302 ruby-fogbugz 368 ruby-fogbugz
  369 + rushover
303 therubyracer 370 therubyracer
304 thin 371 thin
  372 + timecop
  373 + turbo-sprockets-rails3
305 uglifier (>= 1.0.3) 374 uglifier (>= 1.0.3)
306 unicorn 375 unicorn
307 useragent (~> 0.3.1) 376 useragent (~> 0.3.1)
308 webmock 377 webmock
  378 + xmpp4r
309 yajl-ruby 379 yajl-ruby
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 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master 3 [travis-img-url]: https://secure.travis-ci.org/errbit/errbit.png?branch=master
4 [travis-ci-url]: http://travis-ci.org/errbit/errbit 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 ### The open source, self-hosted error catcher 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,12 +60,17 @@ If this doesn&#39;t sound like you, you should probably stick with [Airbrake](http:/
57 The [Thoughtbot](http://thoughtbot.com) guys offer great support for it and it is much more worry-free. 60 The [Thoughtbot](http://thoughtbot.com) guys offer great support for it and it is much more worry-free.
58 They have a free package and even offer a *"Airbrake behind your firewall"* solution. 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 Demo 68 Demo
61 ---- 69 ----
62 70
63 There is a demo available at [http://errbit-demo.herokuapp.com/](http://errbit-demo.herokuapp.com/) 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 Password: password 74 Password: password
67 75
68 Installation 76 Installation
@@ -145,9 +153,6 @@ git clone http://github.com/errbit/errbit.git @@ -145,9 +153,6 @@ git clone http://github.com/errbit/errbit.git
145 gem install heroku 153 gem install heroku
146 heroku create example-errbit --stack cedar 154 heroku create example-errbit --stack cedar
147 heroku addons:add mongolab:starter 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 heroku addons:add sendgrid:starter 156 heroku addons:add sendgrid:starter
152 heroku config:add HEROKU=true 157 heroku config:add HEROKU=true
153 heroku config:add ERRBIT_HOST=some-hostname.example.com 158 heroku config:add ERRBIT_HOST=some-hostname.example.com
@@ -168,7 +173,7 @@ heroku run rake db:seed @@ -168,7 +173,7 @@ heroku run rake db:seed
168 ```bash 173 ```bash
169 # Install the heroku scheduler add-on 174 # Install the heroku scheduler add-on
170 heroku addons:add scheduler:standard 175 heroku addons:add scheduler:standard
171 - 176 +
172 # Go open the dashboard to schedule the job. You should use 177 # Go open the dashboard to schedule the job. You should use
173 # 'rake errbit:db:clear_resolved' as the task command, and schedule it 178 # 'rake errbit:db:clear_resolved' as the task command, and schedule it
174 # at whatever frequency you like (once/day should work great). 179 # at whatever frequency you like (once/day should work great).
@@ -185,7 +190,7 @@ heroku run rake db:seed @@ -185,7 +190,7 @@ heroku run rake db:seed
185 * Or clear resolved errors manually: 190 * Or clear resolved errors manually:
186 191
187 ```bash 192 ```bash
188 - heroku rake errbit:db:clear_resolved 193 + heroku run rake errbit:db:clear_resolved
189 ``` 194 ```
190 195
191 * You may want to enable the deployment hook for heroku : 196 * You may want to enable the deployment hook for heroku :
@@ -275,6 +280,11 @@ GITHUB_ACCESS_SCOPE=repo,public_repo @@ -275,6 +280,11 @@ GITHUB_ACCESS_SCOPE=repo,public_repo
275 * In `config/config.yml`, set `user_has_username` to `true` 280 * In `config/config.yml`, set `user_has_username` to `true`
276 * Follow the instructions at https://github.com/cschiewek/devise_ldap_authenticatable 281 * Follow the instructions at https://github.com/cschiewek/devise_ldap_authenticatable
277 to set up the devise_ldap_authenticatable gem. 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 * If you are authenticating by `username`, you will need to set the user's email manually 289 * If you are authenticating by `username`, you will need to set the user's email manually
280 before authentication. You must add the following lines to `app/models/user.rb`: 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,6 +296,15 @@ GITHUB_ACCESS_SCOPE=repo,public_repo
286 end 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 Upgrading 308 Upgrading
290 --------- 309 ---------
291 When upgrading Errbit, please run: 310 When upgrading Errbit, please run:
@@ -357,6 +376,12 @@ card_type = Defect, status = Open, priority = Essential @@ -357,6 +376,12 @@ card_type = Defect, status = Open, priority = Essential
357 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit** 376 * For 'Account/Repository', the account will either be a username or organization. i.e. **errbit/errbit**
358 * You will also need to provide your username and password for your GitHub account. 377 * You will also need to provide your username and password for your GitHub account.
359 * (We'd really appreciate it if you wanted to help us implement OAuth instead!) 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 What if Errbit has an error? 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,6 +409,23 @@ or you can set up the GitHub Issues tracker for your **Self.Errbit** app:
384 * You can now easily post bug reports to GitHub Issues by clicking the **Create Issue** button on a **Self.Errbit** error. 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 TODO 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,5 +130,10 @@ $(function() {
130 // Hide external backtrace on page load 130 // Hide external backtrace on page load
131 hide_external_backtrace(); 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 init(); 138 init();
134 }); 139 });
app/assets/javascripts/form.js
@@ -8,6 +8,9 @@ $(function(){ @@ -8,6 +8,9 @@ $(function(){
8 if($('div.issue_tracker.nested').length) 8 if($('div.issue_tracker.nested').length)
9 activateTypeSelector('issue_tracker', 'tracker_params'); 9 activateTypeSelector('issue_tracker', 'tracker_params');
10 10
  11 + if($('div.notification_service.nested').length)
  12 + activateTypeSelector('notification_service', 'notification_params');
  13 +
11 $('body').addClass('has-js'); 14 $('body').addClass('has-js');
12 $('.label_radio').click(function(){ 15 $('.label_radio').click(function(){
13 activateLabelIcons(); 16 activateLabelIcons();
app/assets/stylesheets/application.css.erb
@@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
3 *= require jquery.alerts 3 *= require jquery.alerts
4 *= require errbit 4 *= require errbit
5 *= require issue_tracker_icons 5 *= require issue_tracker_icons
  6 + *= require notification_service_icons
6 *= require_self 7 *= require_self
7 */ 8 */
8 9
app/assets/stylesheets/errbit.css
@@ -535,10 +535,11 @@ a.button.active { @@ -535,10 +535,11 @@ a.button.active {
535 display: inline-block; 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 display: none; 540 display: none;
541 } 541 }
  542 +
542 div.nested .chosen { 543 div.nested .chosen {
543 display: block !important; 544 display: block !important;
544 } 545 }
@@ -546,35 +547,35 @@ div.nested .choose { @@ -546,35 +547,35 @@ div.nested .choose {
546 margin-bottom: 0.5em; 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 background-color: #ebebeb; 551 background-color: #ebebeb;
551 border: 1px solid #dddddd; 552 border: 1px solid #dddddd;
552 margin: 0 0 15px; 553 margin: 0 0 15px;
553 padding: 12px; 554 padding: 12px;
554 } 555 }
555 -div.issue_tracker.nested img { 556 +div.issue_tracker.nested img, div.notification_service.nested img {
556 vertical-align: middle; 557 vertical-align: middle;
557 } 558 }
558 559
559 /* Icons for Issue Tracker Radio Buttons */ 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 color: #929292; 562 color: #929292;
562 padding-left: 33px; 563 padding-left: 33px;
563 margin-bottom: 6px; 564 margin-bottom: 6px;
564 margin-right: 8px; 565 margin-right: 8px;
565 line-height: 30px; 566 line-height: 30px;
566 } 567 }
567 -div.issue_tracker.nested .choose { 568 +div.issue_tracker.nested .choose, div.notification_service.nested .choose {
568 padding-bottom: 6px; 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 color: #696969; 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 position: absolute; left: -9999px; 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 color: #191919; 579 color: #191919;
579 } 580 }
580 581
@@ -648,6 +649,13 @@ table.errs td.app .environment { @@ -648,6 +649,13 @@ table.errs td.app .environment {
648 table.errs td.message a { 649 table.errs td.message a {
649 display: block; 650 display: block;
650 word-wrap: break-word; 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 table.errs td.message em { 660 table.errs td.message em {
653 color: #727272; 661 color: #727272;
@@ -834,15 +842,32 @@ table.comment tbody th { @@ -834,15 +842,32 @@ table.comment tbody th {
834 height: 20px; 842 height: 20px;
835 line-height: 0.5em; 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 table.comment tbody td { 857 table.comment tbody td {
838 background-color: #F9F9F9; 858 background-color: #F9F9F9;
839 } 859 }
840 #content-comments a.destroy-comment { 860 #content-comments a.destroy-comment {
841 color: #EE0000; 861 color: #EE0000;
842 margin-right: 5px; 862 margin-right: 5px;
  863 + margin-top: 2px;
  864 + font-size: 21px;
  865 + line-height: 1;
  866 + float: right;
843 } 867 }
844 #content-comments a.destroy-comment:hover { 868 #content-comments a.destroy-comment:hover {
845 text-decoration: none; 869 text-decoration: none;
  870 + color: #AA0000;
846 } 871 }
847 #content-comments #comment_submit { 872 #content-comments #comment_submit {
848 margin-top: 15px; 873 margin-top: 15px;
@@ -871,3 +896,12 @@ table.errs tr td.message .inline_comment em.commenter { @@ -871,3 +896,12 @@ table.errs tr td.message .inline_comment em.commenter {
871 896
872 .current.asc:after { content: ' ↑'; } 897 .current.asc:after { content: ' ↑'; }
873 .current.desc:after { content: ' ↓'; } 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 /* Issue Tracker inactive, select, create and goto icons */ 1 /* Issue Tracker inactive, select, create and goto icons */
2 <% trackers = IssueTracker.subclasses.map{|t| t.label } << 'none' %> 2 <% trackers = IssueTracker.subclasses.map{|t| t.label } << 'none' %>
  3 +
3 <% trackers.each do |tracker| %> 4 <% trackers.each do |tracker| %>
4 div.issue_tracker.nested label.<%= tracker %> { 5 div.issue_tracker.nested label.<%= tracker %> {
5 background: url(/assets/<%= tracker %>_inactive.png) no-repeat; 6 background: url(/assets/<%= tracker %>_inactive.png) no-repeat;
@@ -14,3 +15,4 @@ div.issue_tracker.nested label.r_on.&lt;%= tracker %&gt; { @@ -14,3 +15,4 @@ div.issue_tracker.nested label.r_on.&lt;%= tracker %&gt; {
14 background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat; 15 background: transparent url(/assets/<%= tracker %>_goto.png) 6px 5px no-repeat;
15 } 16 }
16 <% end %> 17 <% end %>
  18 +
app/assets/stylesheets/notification_service_icons.css.erb 0 → 100644
@@ -0,0 +1,18 @@ @@ -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 @@ @@ -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 @@ @@ -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,12 +29,14 @@ class AppsController &lt; InheritedResources::Base
29 def create 29 def create
30 @app = App.new(params[:app]) 30 @app = App.new(params[:app])
31 initialize_subclassed_issue_tracker 31 initialize_subclassed_issue_tracker
  32 + initialize_subclassed_notification_service
32 create! 33 create!
33 end 34 end
34 35
35 def update 36 def update
36 @app = resource 37 @app = resource
37 initialize_subclassed_issue_tracker 38 initialize_subclassed_issue_tracker
  39 + initialize_subclassed_notification_service
38 update! 40 update!
39 end 41 end
40 42
@@ -70,6 +72,7 @@ class AppsController &lt; InheritedResources::Base @@ -70,6 +72,7 @@ class AppsController &lt; InheritedResources::Base
70 end 72 end
71 73
72 def initialize_subclassed_issue_tracker 74 def initialize_subclassed_issue_tracker
  75 + # set the app's issue tracker
73 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type] 76 if params[:app][:issue_tracker_attributes] && tracker_type = params[:app][:issue_tracker_attributes][:type]
74 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type) 77 if IssueTracker.subclasses.map(&:name).concat(["IssueTracker"]).include?(tracker_type)
75 @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes]) 78 @app.issue_tracker = tracker_type.constantize.new(params[:app][:issue_tracker_attributes])
@@ -77,6 +80,15 @@ class AppsController &lt; InheritedResources::Base @@ -77,6 +80,15 @@ class AppsController &lt; InheritedResources::Base
77 end 80 end
78 end 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 def begin_of_association_chain 92 def begin_of_association_chain
81 # Filter the @apps collection to apps watched by the current user, unless user is an admin. 93 # Filter the @apps collection to apps watched by the current user, unless user is an admin.
82 # If user is an admin, then no filter is applied, and all apps are shown. 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,6 +102,7 @@ class AppsController &lt; InheritedResources::Base
90 def plug_params app 102 def plug_params app
91 app.watchers.build if app.watchers.none? 103 app.watchers.build if app.watchers.none?
92 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured? 104 app.issue_tracker = IssueTracker.new unless app.issue_tracker_configured?
  105 + app.notification_service = NotificationService.new unless app.notification_service_configured?
93 app.copy_attributes_from(params[:copy_attributes_from]) if params[:copy_attributes_from] 106 app.copy_attributes_from(params[:copy_attributes_from]) if params[:copy_attributes_from]
94 end 107 end
95 108
app/controllers/comments_controller.rb
@@ -11,7 +11,7 @@ class CommentsController &lt; ApplicationController @@ -11,7 +11,7 @@ class CommentsController &lt; ApplicationController
11 else 11 else
12 flash[:error] = "I'm sorry, your comment was blank! Try again?" 12 flash[:error] = "I'm sorry, your comment was blank! Try again?"
13 end 13 end
14 - redirect_to app_err_path(@app, @problem) 14 + redirect_to app_problem_path(@app, @problem)
15 end 15 end
16 16
17 def destroy 17 def destroy
@@ -21,7 +21,7 @@ class CommentsController &lt; ApplicationController @@ -21,7 +21,7 @@ class CommentsController &lt; ApplicationController
21 else 21 else
22 flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!" 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 end 23 end
24 - redirect_to app_err_path(@app, @problem) 24 + redirect_to app_problem_path(@app, @problem)
25 end 25 end
26 26
27 protected 27 protected
@@ -34,7 +34,7 @@ class CommentsController &lt; ApplicationController @@ -34,7 +34,7 @@ class CommentsController &lt; ApplicationController
34 end 34 end
35 35
36 def find_problem 36 def find_problem
37 - @problem = @app.problems.find(params[:err_id]) 37 + @problem = @app.problems.find(params[:problem_id])
38 end 38 end
39 end 39 end
40 40
app/controllers/errs_controller.rb
@@ -1,157 +0,0 @@ @@ -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,9 +5,16 @@ class NoticesController &lt; ApplicationController
5 5
6 def create 6 def create
7 # params[:data] if the notice came from a GET request, raw_post if it came via POST 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 end 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 end 20 end
13 -  
app/controllers/problems_controller.rb 0 → 100644
@@ -0,0 +1,157 @@ @@ -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,7 +13,7 @@ module ApplicationHelper
13 event.dtend = notice.created_at.utc + 60.minutes 13 event.dtend = notice.created_at.utc + 60.minutes
14 event.organizer = notice.server_environment && notice.server_environment["hostname"] 14 event.organizer = notice.server_environment && notice.server_environment["hostname"]
15 event.location = notice.server_environment && notice.server_environment["project-root"] 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 end 17 end
18 end 18 end
19 end.to_s 19 end.to_s
@@ -58,12 +58,25 @@ module ApplicationHelper @@ -58,12 +58,25 @@ module ApplicationHelper
58 percent = 100.0 / total.to_f 58 percent = 100.0 / total.to_f
59 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \ 59 rows = tallies.map {|value, count| [(count.to_f * percent), value]} \
60 .sort {|a, b| a[0] <=> b[0]} 60 .sort {|a, b| 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 end 70 end
63 71
64 private 72 private
65 def total_from_tallies(tallies) 73 def total_from_tallies(tallies)
66 tallies.values.inject(0) {|sum, n| sum + n} 74 tallies.values.inject(0) {|sum, n| sum + n}
67 end 75 end
  76 +
  77 + def head_size
  78 + 4
  79 + end
  80 +
68 end 81 end
69 82
app/helpers/apps_helper.rb
@@ -16,6 +16,16 @@ module AppsHelper @@ -16,6 +16,16 @@ module AppsHelper
16 @any_github_repos 16 @any_github_repos
17 end 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 def any_issue_trackers? 29 def any_issue_trackers?
20 detect_any_apps_with_attributes unless @any_issue_trackers 30 detect_any_apps_with_attributes unless @any_issue_trackers
21 @any_issue_trackers 31 @any_issue_trackers
@@ -29,11 +39,14 @@ module AppsHelper @@ -29,11 +39,14 @@ module AppsHelper
29 private 39 private
30 40
31 def detect_any_apps_with_attributes 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 @apps.each do |app| 44 @apps.each do |app|
34 @any_github_repos ||= app.github_repo? 45 @any_github_repos ||= app.github_repo?
  46 + @any_bitbucket_repos ||= app.bitbucket_repo?
35 @any_issue_trackers ||= app.issue_tracker_configured? 47 @any_issue_trackers ||= app.issue_tracker_configured?
36 @any_deploys ||= !!app.last_deploy_at 48 @any_deploys ||= !!app.last_deploy_at
  49 + @any_notification_services ||= app.notification_service_configured?
37 end 50 end
38 end 51 end
39 end 52 end
app/helpers/backtrace_helper.rb 0 → 100644
@@ -0,0 +1,16 @@ @@ -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 @@ @@ -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,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 # encoding: utf-8 1 # encoding: utf-8
2 module NoticesHelper 2 module NoticesHelper
3 - def in_app_backtrace_line?(line)  
4 - !!(line['file'] =~ %r{^\[PROJECT_ROOT\]/(?!(vendor))})  
5 - end  
6 -  
7 def notice_atom_summary(notice) 3 def notice_atom_summary(notice)
8 render "notices/atom_entry.html.haml", :notice => notice 4 render "notices/atom_entry.html.haml", :notice => notice
9 end 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 end 6 end
68 7
app/helpers/problems_helper.rb 0 → 100644
@@ -0,0 +1,27 @@ @@ -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,6 +5,8 @@ class App
5 field :name, :type => String 5 field :name, :type => String
6 field :api_key 6 field :api_key
7 field :github_repo 7 field :github_repo
  8 + field :bitbucket_repo
  9 + field :repository_branch
8 field :resolve_errs_on_deploy, :type => Boolean, :default => false 10 field :resolve_errs_on_deploy, :type => Boolean, :default => false
9 field :notify_all_users, :type => Boolean, :default => false 11 field :notify_all_users, :type => Boolean, :default => false
10 field :notify_on_errs, :type => Boolean, :default => true 12 field :notify_on_errs, :type => Boolean, :default => true
@@ -17,6 +19,8 @@ class App @@ -17,6 +19,8 @@ class App
17 embeds_many :watchers 19 embeds_many :watchers
18 embeds_many :deploys 20 embeds_many :deploys
19 embeds_one :issue_tracker 21 embeds_one :issue_tracker
  22 + embeds_one :notification_service
  23 +
20 has_many :problems, :inverse_of => :app, :dependent => :destroy 24 has_many :problems, :inverse_of => :app, :dependent => :destroy
21 25
22 before_validation :generate_api_key, :on => :create 26 before_validation :generate_api_key, :on => :create
@@ -33,7 +37,8 @@ class App @@ -33,7 +37,8 @@ class App
33 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } 37 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
34 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, 38 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
35 :reject_if => proc { |attrs| !IssueTracker.subclasses.map(&:to_s).include?(attrs[:type].to_s) } 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 # Processes a new error report. 43 # Processes a new error report.
39 # 44 #
@@ -74,8 +79,7 @@ class App @@ -74,8 +79,7 @@ class App
74 end 79 end
75 80
76 def find_or_create_err!(attrs) 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 end 83 end
80 84
81 # Mongoid Bug: find(id) on association proxies returns an Enumerator 85 # Mongoid Bug: find(id) on association proxies returns an Enumerator
@@ -103,6 +107,9 @@ class App @@ -103,6 +107,9 @@ class App
103 end 107 end
104 alias :notify_on_deploys? :notify_on_deploys 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 def github_repo? 114 def github_repo?
108 self.github_repo.present? 115 self.github_repo.present?
@@ -113,7 +120,19 @@ class App @@ -113,7 +120,19 @@ class App
113 end 120 end
114 121
115 def github_url_to_file(file) 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 end 136 end
118 137
119 138
@@ -121,6 +140,11 @@ class App @@ -121,6 +140,11 @@ class App
121 !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?) 140 !!(issue_tracker && issue_tracker.class < IssueTracker && issue_tracker.project_id.present?)
122 end 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 def notification_recipients 148 def notification_recipients
125 if notify_all_users 149 if notify_all_users
126 (User.all.map(&:email).reject(&:blank?) + watchers.map(&:address)).uniq 150 (User.all.map(&:email).reject(&:blank?) + watchers.map(&:address)).uniq
@@ -137,7 +161,7 @@ class App @@ -137,7 +161,7 @@ class App
137 self.send("#{k}=", copy_app.send(k)) 161 self.send("#{k}=", copy_app.send(k))
138 end 162 end
139 # Clone the embedded objects that can be changed via apps/edit (ignore errs & deploys, etc.) 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 if obj = copy_app.send(relation) 165 if obj = copy_app.send(relation)
142 self.send("#{relation}=", obj.is_a?(Array) ? obj.map(&:clone) : obj.clone) 166 self.send("#{relation}=", obj.is_a?(Array) ? obj.map(&:clone) : obj.clone)
143 end 167 end
app/models/backtrace.rb 0 → 100644
@@ -0,0 +1,36 @@ @@ -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 @@ @@ -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 @@ @@ -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,20 +6,19 @@ class Err
6 include Mongoid::Document 6 include Mongoid::Document
7 include Mongoid::Timestamps 7 include Mongoid::Timestamps
8 8
9 - field :error_class 9 + field :error_class, :default => "UnknownError"
10 field :component 10 field :component
11 field :action 11 field :action
12 - field :environment 12 + field :environment, :default => "unknown"
13 field :fingerprint 13 field :fingerprint
14 14
15 belongs_to :problem 15 belongs_to :problem
16 index :problem_id 16 index :problem_id
17 index :error_class 17 index :error_class
  18 + index :fingerprint
18 19
19 has_many :notices, :inverse_of => :err, :dependent => :destroy 20 has_many :notices, :inverse_of => :err, :dependent => :destroy
20 21
21 - validates_presence_of :error_class, :environment  
22 -  
23 delegate :app, :resolved?, :to => :problem 22 delegate :app, :resolved?, :to => :problem
24 23
25 end 24 end
app/models/error_report.rb
1 -require 'digest/md5' 1 +require 'digest/sha1'
2 require 'hoptoad_notifier' 2 require 'hoptoad_notifier'
3 3
4 class ErrorReport 4 class ErrorReport
5 - attr_reader :error_class, :message, :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 def initialize(xml_or_attributes) 7 def initialize(xml_or_attributes)
8 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access 8 @attributes = (xml_or_attributes.is_a?(String) ? Hoptoad.parse_xml!(xml_or_attributes) : xml_or_attributes).with_indifferent_access
@@ -10,11 +10,7 @@ class ErrorReport @@ -10,11 +10,7 @@ class ErrorReport
10 end 10 end
11 11
12 def fingerprint 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 end 14 end
19 15
20 def rails_env 16 def rails_env
@@ -33,15 +29,21 @@ class ErrorReport @@ -33,15 +29,21 @@ class ErrorReport
33 @app ||= App.find_by_api_key!(api_key) 29 @app ||= App.find_by_api_key!(api_key)
34 end 30 end
35 31
  32 + def backtrace
  33 + @normalized_backtrace ||= Backtrace.find_or_create(:raw => @backtrace)
  34 + end
  35 +
36 def generate_notice! 36 def generate_notice!
37 notice = Notice.new( 37 notice = Notice.new(
38 :message => message, 38 :message => message,
39 :error_class => error_class, 39 :error_class => error_class,
40 - :backtrace => backtrace, 40 + :backtrace_id => backtrace.id,
41 :request => request, 41 :request => request,
42 :server_environment => server_environment, 42 :server_environment => server_environment,
43 :notifier => notifier, 43 :notifier => notifier,
44 - :user_attributes => user_attributes) 44 + :user_attributes => user_attributes,
  45 + :current_user => current_user
  46 + )
45 47
46 err = app.find_or_create_err!( 48 err = app.find_or_create_err!(
47 :error_class => error_class, 49 :error_class => error_class,
@@ -53,5 +55,18 @@ class ErrorReport @@ -53,5 +55,18 @@ class ErrorReport
53 err.notices << notice 55 err.notices << notice
54 notice 56 notice
55 end 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 end 71 end
57 72
app/models/issue_tracker.rb
@@ -14,6 +14,7 @@ class IssueTracker @@ -14,6 +14,7 @@ class IssueTracker
14 field :username, :type => String 14 field :username, :type => String
15 field :password, :type => String 15 field :password, :type => String
16 field :ticket_properties, :type => String 16 field :ticket_properties, :type => String
  17 + field :subdomain, :type => String
17 18
18 validate :check_params 19 validate :check_params
19 20
app/models/issue_trackers/bitbucket_issues_tracker.rb 0 → 100644
@@ -0,0 +1,47 @@ @@ -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 end 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 end 52 end
51 end 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 end 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 end 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 end 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 end 56 end
55 -end 57 -end
  58 +end
56 \ No newline at end of file 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 end 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 end 49 end
48 -end  
49 - 50 +end
50 \ No newline at end of file 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 end 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 end 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 end 42 end
41 -end  
42 - 43 +end
43 \ No newline at end of file 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 end 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 end 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 end 63 end
62 end 64 end
63 -  
app/models/notice.rb
@@ -6,14 +6,17 @@ class Notice @@ -6,14 +6,17 @@ class Notice
6 include Mongoid::Timestamps 6 include Mongoid::Timestamps
7 7
8 field :message 8 field :message
9 - field :backtrace, :type => Array  
10 field :server_environment, :type => Hash 9 field :server_environment, :type => Hash
11 field :request, :type => Hash 10 field :request, :type => Hash
12 field :notifier, :type => Hash 11 field :notifier, :type => Hash
13 field :user_attributes, :type => Hash 12 field :user_attributes, :type => Hash
  13 + field :current_user, :type => Hash
14 field :error_class 14 field :error_class
  15 + delegate :lines, :to => :backtrace, :prefix => true
  16 + delegate :app, :to => :err
15 17
16 belongs_to :err 18 belongs_to :err
  19 + belongs_to :backtrace, :index => true
17 index :created_at 20 index :created_at
18 index( 21 index(
19 [ 22 [
@@ -89,17 +92,8 @@ class Notice @@ -89,17 +92,8 @@ class Notice
89 request['session'] || {} 92 request['session'] || {}
90 end 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 end 97 end
104 98
105 protected 99 protected
@@ -113,11 +107,11 @@ class Notice @@ -113,11 +107,11 @@ class Notice
113 end 107 end
114 108
115 def remove_cached_attributes_from_problem 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 end 111 end
118 112
119 def unresolve_problem 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 end 115 end
122 116
123 def cache_attributes_on_problem 117 def cache_attributes_on_problem
@@ -128,8 +122,6 @@ class Notice @@ -128,8 +122,6 @@ class Notice
128 [:server_environment, :request, :notifier].each do |h| 122 [:server_environment, :request, :notifier].each do |h|
129 send("#{h}=",sanitize_hash(send(h))) 123 send("#{h}=",sanitize_hash(send(h)))
130 end 124 end
131 - # Set unknown backtrace files  
132 - read_attribute(:backtrace).each{|line| line['file'] = "[unknown source]" if line['file'].blank? }  
133 end 125 end
134 126
135 def sanitize_hash(h) 127 def sanitize_hash(h)
app/models/notice_observer.rb
@@ -2,18 +2,21 @@ class NoticeObserver &lt; Mongoid::Observer @@ -2,18 +2,21 @@ class NoticeObserver &lt; Mongoid::Observer
2 observe :notice 2 observe :notice
3 3
4 def after_create notice 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 end 13 end
9 14
10 private 15 private
11 16
12 def should_notify? notice 17 def should_notify? notice
13 app = notice.app 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 end 21 end
18 -  
19 end 22 end
app/models/notification_service.rb 0 → 100644
@@ -0,0 +1,33 @@ @@ -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 @@ @@ -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 @@ @@ -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 \ No newline at end of file 38 \ No newline at end of file
app/models/notification_services/hipchat_service.rb 0 → 100644
@@ -0,0 +1,31 @@ @@ -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 \ No newline at end of file 32 \ No newline at end of file
app/models/notification_services/hoiio_service.rb 0 → 100644
@@ -0,0 +1,38 @@ @@ -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 \ No newline at end of file 39 \ No newline at end of file
app/models/notification_services/pushover_service.rb 0 → 100644
@@ -0,0 +1,28 @@ @@ -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 \ No newline at end of file 29 \ No newline at end of file
app/models/problem.rb
@@ -6,9 +6,11 @@ class Problem @@ -6,9 +6,11 @@ class Problem
6 include Mongoid::Document 6 include Mongoid::Document
7 include Mongoid::Timestamps 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 field :last_deploy_at, :type => Time 11 field :last_deploy_at, :type => Time
11 field :resolved, :type => Boolean, :default => false 12 field :resolved, :type => Boolean, :default => false
  13 + field :resolved_at, :type => Time
12 field :issue_link, :type => String 14 field :issue_link, :type => String
13 field :issue_type, :type => String 15 field :issue_type, :type => String
14 16
@@ -28,7 +30,9 @@ class Problem @@ -28,7 +30,9 @@ class Problem
28 index :app_name 30 index :app_name
29 index :message 31 index :message
30 index :last_notice_at 32 index :last_notice_at
  33 + index :first_notice_at
31 index :last_deploy_at 34 index :last_deploy_at
  35 + index :resolved_at
32 index :notices_count 36 index :notices_count
33 37
34 belongs_to :app 38 belongs_to :app
@@ -41,6 +45,8 @@ class Problem @@ -41,6 +45,8 @@ class Problem
41 scope :unresolved, where(:resolved => false) 45 scope :unresolved, where(:resolved => false)
42 scope :ordered, order_by(:last_notice_at.desc) 46 scope :ordered, order_by(:last_notice_at.desc)
43 scope :for_apps, lambda {|apps| where(:app_id.in => apps.all.map(&:id))} 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 def self.in_env(env) 52 def self.in_env(env)
@@ -52,11 +58,11 @@ class Problem @@ -52,11 +58,11 @@ class Problem
52 end 58 end
53 59
54 def resolve! 60 def resolve!
55 - self.update_attributes!(:resolved => true, :notices_count => 0) 61 + self.update_attributes!(:resolved => true, :resolved_at => Time.now)
56 end 62 end
57 63
58 def unresolve! 64 def unresolve!
59 - self.update_attributes!(:resolved => false) 65 + self.update_attributes!(:resolved => false, :resolved_at => nil)
60 end 66 end
61 67
62 def unresolved? 68 def unresolved?
@@ -103,6 +109,10 @@ class Problem @@ -103,6 +109,10 @@ class Problem
103 else raise("\"#{sort}\" is not a recognized sort") 109 else raise("\"#{sort}\" is not a recognized sort")
104 end 110 end
105 end 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 def reset_cached_attributes 118 def reset_cached_attributes
@@ -124,21 +134,26 @@ class Problem @@ -124,21 +134,26 @@ class Problem
124 end 134 end
125 135
126 def cache_notice_attributes(notice=nil) 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 attrs.merge!( 144 attrs.merge!(
130 - :message => notice.message, 145 + :message => notice.message,
131 :environment => notice.environment_name, 146 :environment => notice.environment_name,
132 :error_class => notice.error_class, 147 :error_class => notice.error_class,
133 - :where => notice.where, 148 + :where => notice.where,
134 :messages => attribute_count_increase(:messages, notice.message), 149 :messages => attribute_count_increase(:messages, notice.message),
135 :hosts => attribute_count_increase(:hosts, notice.host), 150 :hosts => attribute_count_increase(:hosts, notice.host),
136 :user_agents => attribute_count_increase(:user_agents, notice.user_agent_string) 151 :user_agents => attribute_count_increase(:user_agents, notice.user_agent_string)
137 - ) if notice 152 + ) if notice
138 update_attributes!(attrs) 153 update_attributes!(attrs)
139 end 154 end
140 155
141 - def remove_cached_notice_attribures(notice) 156 + def remove_cached_notice_attributes(notice)
142 update_attributes!( 157 update_attributes!(
143 :messages => attribute_count_descrease(:messages, notice.message), 158 :messages => attribute_count_descrease(:messages, notice.message),
144 :hosts => attribute_count_descrease(:hosts, notice.host), 159 :hosts => attribute_count_descrease(:hosts, notice.host),
app/views/apps/_configuration_instructions.html.haml
1 %pre 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,8 +5,14 @@
5 = f.text_field :name 5 = f.text_field :name
6 6
7 %div 7 %div
  8 + = f.label :repository_branch
  9 + = f.text_field :repository_branch, :placeholder => "master"
  10 +%div
8 = f.label :github_repo 11 = f.label :github_repo
9 = f.text_field :github_repo, :placeholder => "errbit/errbit from https://github.com/errbit/errbit" 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 %fieldset 17 %fieldset
12 %legend Notifications 18 %legend Notifications
@@ -46,4 +52,5 @@ @@ -46,4 +52,5 @@
46 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy' 52 = f.label :resolve_errs_on_deploy, 'Resolve errs on deploy'
47 53
48 = render "issue_tracker_fields", :f => f 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 @@ @@ -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,8 +6,10 @@
6 %thead 6 %thead
7 %tr 7 %tr
8 %th Name 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 - if any_issue_trackers? 13 - if any_issue_trackers?
12 %th Tracker 14 %th Tracker
13 - if any_deploys? 15 - if any_deploys?
@@ -17,10 +19,20 @@ @@ -17,10 +19,20 @@
17 - @apps.each do |app| 19 - @apps.each do |app|
18 %tr 20 %tr
19 %td.name= link_to app.name, app_path(app) 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 %td.github_repo 23 %td.github_repo
22 - if app.github_repo? 24 - if app.github_repo?
23 = link_to(app.github_repo, app.github_url, :target => '_blank') 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 - if any_issue_trackers? 36 - if any_issue_trackers?
25 %td.issue_tracker 37 %td.issue_tracker
26 - if app.issue_tracker_configured? 38 - if app.issue_tracker_configured?
app/views/apps/show.atom.builder
1 atom_feed do |feed| 1 atom_feed do |feed|
2 feed.title("Errbit notices for #{h @app.name} at #{root_url}") 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 end 4 end
app/views/apps/show.html.haml
@@ -12,10 +12,10 @@ @@ -12,10 +12,10 @@
12 - if current_user.admin? 12 - if current_user.admin?
13 = link_to 'edit', edit_app_path(@app), :class => 'button' 13 = link_to 'edit', edit_app_path(@app), :class => 'button'
14 = link_to 'destroy', app_path(@app), :method => :delete, :data => { :confirm => 'Seriously?' }, :class => 'button' 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 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?' 19 = link_to 'unwatch', app_watcher_path({:app_id => @app, :id => current_user.id}), :method => :delete, :class => 'button', :confirm => 'Are you sure?'
20 %h3#watchers_toggle 20 %h3#watchers_toggle
21 Watchers 21 Watchers
@@ -83,7 +83,7 @@ @@ -83,7 +83,7 @@
83 83
84 - if @app.problems.any? 84 - if @app.problems.any?
85 %h3.clear Errors 85 %h3.clear Errors
86 - = render 'errs/table', :errs => @problems 86 + = render 'problems/table', :problems => @problems
87 - else 87 - else
88 %h3.clear No errs have been caught yet, make sure you setup your app 88 %h3.clear No errs have been caught yet, make sure you setup your app
89 = render 'configuration_instructions', :app => @app 89 = render 'configuration_instructions', :app => @app
app/views/errs/_issue_tracker_links.html.haml
@@ -1,15 +0,0 @@ @@ -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,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,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,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,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,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,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,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 +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 @@ @@ -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 <% if notice = problem.notices.first %> 2 <% if notice = problem.notices.first %>
3 <%= notice.message %> 3 <%= notice.message %>
4 4
@@ -19,8 +19,8 @@ @@ -19,8 +19,8 @@
19 <%= pretty_hash(notice.session) %> 19 <%= pretty_hash(notice.session) %>
20 20
21 Backtrace 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 <% end %> 24 <% end %>
25 25
26 Environment 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 <% if notice = problem.notices.first %> 2 <% if notice = problem.notices.first %>
3 # <%= notice.message %> # 3 # <%= notice.message %> #
4 ## Summary ## 4 ## Summary ##
@@ -27,7 +27,7 @@ @@ -27,7 +27,7 @@
27 27
28 ## Backtrace ## 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 <% end %> 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 <% if notice = problem.notices.first %> 2 <% if notice = problem.notices.first %>
3 # <%= notice.message %> # 3 # <%= notice.message %> #
4 ## Summary ## 4 ## Summary ##
@@ -23,7 +23,7 @@ @@ -23,7 +23,7 @@
23 23
24 ## Backtrace ## 24 ## Backtrace ##
25 <code> 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 <% end %> 27 <% end %>
28 </code> 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 <% if notice = problem.notices.first %> 2 <% if notice = problem.notices.first %>
3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %> 3 <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
4 Where: <%= notice.where %> 4 Where: <%= notice.where %>
@@ -12,6 +12,6 @@ See this exception on Errbit: &lt;%= app_err_url problem.app, problem %&gt; @@ -12,6 +12,6 @@ See this exception on Errbit: &lt;%= app_err_url problem.app, problem %&gt;
12 <%= pretty_hash notice.session %> 12 <%= pretty_hash notice.session %>
13 13
14 Backtrace: 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 <% end %> 16 <% end %>
17 17
app/views/issue_trackers/textile_body.txt.erb
1 <% if notice = problem.notices.first %> 1 <% if notice = problem.notices.first %>
2 h1. <%= notice.message %> 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 h2. Summary 6 h2. Summary
7 <% if notice.request['url'].present? %> 7 <% if notice.request['url'].present? %>
@@ -32,7 +32,7 @@ h2. Session @@ -32,7 +32,7 @@ h2. Session
32 h2. Backtrace 32 h2. Backtrace
33 33
34 | Line | File | Method | 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 <% end %> 36 <% end %>
37 37
38 h2. Environment 38 h2. Environment
app/views/kaminari/_first_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the first page 3 -# url: url to the first page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.first 8 %span.first
app/views/kaminari/_gap.html.haml
1 -# Non-link tag that stands for skipped pages... 1 -# Non-link tag that stands for skipped pages...
2 -# available local variables 2 -# available local variables
3 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 5 -# per_page: number of items to fetch per page
6 -# remote: data-remote 6 -# remote: data-remote
7 %span.page.gap 7 %span.page.gap
app/views/kaminari/_last_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the last page 3 -# url: url to the last page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.last 8 %span.last
app/views/kaminari/_next_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the next page 3 -# url: url to the next page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.next 8 %span.next
app/views/kaminari/_page.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 -# page: a page object for "this" page 3 -# page: a page object for "this" page
4 -# url: url to this page 4 -# url: url to this page
5 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 7 -# per_page: number of items to fetch per page
8 -# remote: data-remote 8 -# remote: data-remote
9 %span{:class => "page#{' current' if page.current?}"} 9 %span{:class => "page#{' current' if page.current?}"}
app/views/kaminari/_paginator.html.haml
1 -# The container tag 1 -# The container tag
2 -# available local variables 2 -# available local variables
3 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 5 -# per_page: number of items to fetch per page
6 -# remote: data-remote 6 -# remote: data-remote
7 -# paginator: the paginator that renders the pagination tags inside 7 -# paginator: the paginator that renders the pagination tags inside
app/views/kaminari/_prev_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the previous page 3 -# url: url to the previous page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.prev 8 %span.prev
app/views/kaminari/notices/_first_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the first page 3 -# url: url to the first page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.first 8 %span.first
app/views/kaminari/notices/_gap.html.haml
1 -# Non-link tag that stands for skipped pages... 1 -# Non-link tag that stands for skipped pages...
2 -# available local variables 2 -# available local variables
3 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 5 -# per_page: number of items to fetch per page
6 -# remote: data-remote 6 -# remote: data-remote
7 %span.page.gap 7 %span.page.gap
app/views/kaminari/notices/_last_page.html.haml
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 -# available local variables 2 -# available local variables
3 -# url: url to the last page 3 -# url: url to the last page
4 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 6 -# per_page: number of items to fetch per page
7 -# remote: data-remote 7 -# remote: data-remote
8 %span.last 8 %span.last
app/views/kaminari/notices/_page.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 -# page: a page object for "this" page 3 -# page: a page object for "this" page
4 -# url: url to this page 4 -# url: url to this page
5 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 7 -# per_page: number of items to fetch per page
8 -# remote: data-remote 8 -# remote: data-remote
9 %span{:class => "page#{' current' if page.current?}"} 9 %span{:class => "page#{' current' if page.current?}"}
app/views/kaminari/notices/_paginator.html.haml
1 -# The container tag 1 -# The container tag
2 -# available local variables 2 -# available local variables
3 -# current_page: a page object for the currently displayed page 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 -# per_page: number of items to fetch per page 5 -# per_page: number of items to fetch per page
6 -# remote: data-remote 6 -# remote: data-remote
7 -# paginator: the paginator that renders the pagination tags inside 7 -# paginator: the paginator that renders the pagination tags inside
@@ -11,4 +11,4 @@ @@ -11,4 +11,4 @@
11 |&nbsp; 11 |&nbsp;
12 = prev_page_tag 12 = prev_page_tag
13 .notice-pagination-loader= image_tag 'loader.gif' 13 .notice-pagination-loader= image_tag 'loader.gif'
14 -viewing occurrence #{page_count_from_end(current_page, 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,7 +18,7 @@
18 = render 'shared/navigation' if current_user 18 = render 'shared/navigation' if current_user
19 = render 'shared/session' 19 = render 'shared/session'
20 #content-wrapper 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 %h1= yield :title 22 %h1= yield :title
23 %span.meta= yield :meta 23 %span.meta= yield :meta
24 - if (action_bar = yield(:action_bar)).present? 24 - if (action_bar = yield(:action_bar)).present?
@@ -33,3 +33,4 @@ @@ -33,3 +33,4 @@
33 #footer= "Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.".html_safe 33 #footer= "Powered by #{link_to 'Errbit', 'http://github.com/errbit/errbit', :target => '_blank'}: the open source error catcher.".html_safe
34 = yield :scripts 34 = yield :scripts
35 35
  36 += yield :before_title
36 \ No newline at end of file 37 \ No newline at end of file
app/views/mailer/err_notification.html.haml
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 %br 14 %br
15 This err has occurred #{pluralize @notice.problem.notices_count, 'time'}. 15 This err has occurred #{pluralize @notice.problem.notices_count, 'time'}.
16 %p 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 %tr 18 %tr
19 %td.section 19 %td.section
20 %table(cellpadding="0" cellspacing="0" border="0" align="left") 20 %table(cellpadding="0" cellspacing="0" border="0" align="left")
@@ -27,16 +27,15 @@ @@ -27,16 +27,15 @@
27 %p.heading WHERE: 27 %p.heading WHERE:
28 %p.monospace 28 %p.monospace
29 = @notice.where 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 %br 32 %br
34 %p.heading URL: 33 %p.heading URL:
35 %p.monospace 34 %p.monospace
36 - if @notice.request['url'].present? 35 - if @notice.request['url'].present?
37 = link_to @notice.request['url'], @notice.request['url'] 36 = link_to @notice.request['url'], @notice.request['url']
38 %p.heading BACKTRACE: 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 %p.backtrace= line 39 %p.backtrace= line
41 %br 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,7 +2,7 @@ An err has just occurred in &lt;%= @notice.environment_name %&gt;: &lt;%= raw(@notice.mes
2 2
3 This err has occurred <%= pluralize @notice.problem.notices_count, 'time' %>. You should really look into it here: 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 ERROR MESSAGE: 8 ERROR MESSAGE:
@@ -14,7 +14,7 @@ WHERE: @@ -14,7 +14,7 @@ WHERE:
14 14
15 <%= @notice.where %> 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 <%= line %> 18 <%= line %>
19 <% end %> 19 <% end %>
20 20
@@ -26,7 +26,7 @@ URL: @@ -26,7 +26,7 @@ URL:
26 26
27 BACKTRACE: 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 <%= line %> 30 <%= line %>
31 <% end %> 31 <% end %>
32 32