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