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