party_fouls 1.5.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +268 -0
- data/Rakefile +19 -0
- data/lib/generators/party_foul/install_generator.rb +38 -0
- data/lib/generators/party_foul/templates/party_foul.rb +39 -0
- data/lib/party_foul/exception_handler.rb +106 -0
- data/lib/party_foul/issue_renderers/base.rb +187 -0
- data/lib/party_foul/issue_renderers/rack.rb +54 -0
- data/lib/party_foul/issue_renderers/rackless.rb +25 -0
- data/lib/party_foul/issue_renderers/rails.rb +35 -0
- data/lib/party_foul/issue_renderers.rb +5 -0
- data/lib/party_foul/middleware.rb +32 -0
- data/lib/party_foul/processors/base.rb +11 -0
- data/lib/party_foul/processors/delayed_job.rb +16 -0
- data/lib/party_foul/processors/resque.rb +16 -0
- data/lib/party_foul/processors/sidekiq.rb +17 -0
- data/lib/party_foul/processors/sync.rb +11 -0
- data/lib/party_foul/processors.rb +2 -0
- data/lib/party_foul/rackless_exception_handler.rb +17 -0
- data/lib/party_foul/version.rb +3 -0
- data/lib/party_foul.rb +92 -0
- data/test/generator_test.rb +26 -0
- data/test/party_foul/configure_test.rb +37 -0
- data/test/party_foul/exception_handler_test.rb +205 -0
- data/test/party_foul/issue_renderers/base_test.rb +210 -0
- data/test/party_foul/issue_renderers/rack_test.rb +80 -0
- data/test/party_foul/issue_renderers/rackless_test.rb +29 -0
- data/test/party_foul/issue_renderers/rails_test.rb +83 -0
- data/test/party_foul/middleware_test.rb +48 -0
- data/test/party_foul/rackless_exception_handler_test.rb +33 -0
- data/test/test_helper.rb +42 -0
- data/test/tmp/config/initializers/party_foul.rb +39 -0
- metadata +214 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 24bb7632ea8be8c278cd4583225f4f7d5dfbf4fb
|
4
|
+
data.tar.gz: ad2655ec1e1f1b26d46355941b2a77f0ab7b2a19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0eb1eaea0c9d6a0beed7ae74c16cee3e485daeb01cb41f9fb11f0b66069d641b95d59bd9b5746db2eeb0392bc323459a2210e26aa9ed08849b50c7180e0a5670
|
7
|
+
data.tar.gz: da6149b6f020abd7f5f9e3f1f5d2a63a531b90b933b40932ce62bba6f87e0841045b069d79b5cca5a93206e22b766281dc6a1bd1fb0624003932866e5a7ca33a
|
data/README.md
ADDED
@@ -0,0 +1,268 @@
|
|
1
|
+
# PartyFoul #
|
2
|
+
|
3
|
+
Rails exceptions automatically opened as issues on GitHub
|
4
|
+
|
5
|
+
# This is a fork, released as the `party_fouls` gem
|
6
|
+
|
7
|
+
## Looking for help? ##
|
8
|
+
|
9
|
+
If it is a bug [please open an issue on
|
10
|
+
GitHub](https://github.com/dockyard/party_foul/issues).
|
11
|
+
|
12
|
+
## About ##
|
13
|
+
|
14
|
+
`PartyFoul` captures exceptions in your application and does the
|
15
|
+
following:
|
16
|
+
|
17
|
+
1. Attempt to find a matching issue in your GitHub repo
|
18
|
+
2. If no matching issue is found, a new issue is created with a
|
19
|
+
unique title, session information, and stack trace. The issue is
|
20
|
+
tagged as a `bug`. A new comment is added with relevant data on the
|
21
|
+
application state.
|
22
|
+
3. If an open issue is found, the occurrence count and time stamp is
|
23
|
+
updated. A new comment is added with relevant data on the
|
24
|
+
application state.
|
25
|
+
4. If a closed issue is found, the occurrence count and time stamp is
|
26
|
+
updated. The issue is reopened and a `regression` tag is
|
27
|
+
added. A new comment is added with relevant data on the
|
28
|
+
application state.
|
29
|
+
5. If the issue is marked as `wontfix` the issue is not updated nor is
|
30
|
+
a new issue created. No comments are added.
|
31
|
+
|
32
|
+
## Installation ##
|
33
|
+
|
34
|
+
**Note** We highly recommend that you create a new GitHub account that is
|
35
|
+
a collaborator on your repository. Use this new account's credentials
|
36
|
+
for the installation below. If you use your own account you will
|
37
|
+
not receive emails when issues are created, updated, reopened, etc...
|
38
|
+
because all of the work is done as your account.
|
39
|
+
|
40
|
+
In your Gemfile add the following:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
gem 'party_foul'
|
44
|
+
```
|
45
|
+
|
46
|
+
### Rails ###
|
47
|
+
If you are using Rails you can run the install generator.
|
48
|
+
|
49
|
+
```
|
50
|
+
rails g party_foul:install
|
51
|
+
```
|
52
|
+
|
53
|
+
This prompts you for the GitHub credentials of the account that is
|
54
|
+
opening the issues. The OAuth token for that account is stored
|
55
|
+
in `config/initializers/party_foul.rb`. You may want to remove the token
|
56
|
+
string and store in an environment variable. It is best not to store the
|
57
|
+
token in version control.
|
58
|
+
|
59
|
+
Add as the very last middleware in your production `Rack` stack in `config/environments/production.rb`
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
config.middleware.use('PartyFoul::Middleware')
|
63
|
+
```
|
64
|
+
### Other ###
|
65
|
+
|
66
|
+
You need to initialize `PartyFoul`, use the following:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
PartyFoul.configure do |config|
|
70
|
+
# The collection of exceptions PartyFoul should not be allowed to handle
|
71
|
+
# The constants here *must* be represented as strings
|
72
|
+
config.blacklisted_exceptions = ['ActiveRecord::RecordNotFound', 'ActionController::RoutingError']
|
73
|
+
|
74
|
+
# The OAuth token for the account that is opening the issues on GitHub
|
75
|
+
config.oauth_token = 'abcdefgh1234567890'
|
76
|
+
|
77
|
+
# The API endpoint for GitHub. Unless you are hosting a private
|
78
|
+
# instance of Enterprise GitHub you do not need to include this
|
79
|
+
config.api_endpoint = 'https://api.github.com'
|
80
|
+
|
81
|
+
# The Web URL for GitHub. Unless you are hosting a private
|
82
|
+
# instance of Enterprise GitHub you do not need to include this
|
83
|
+
config.web_url = 'https://github.com'
|
84
|
+
|
85
|
+
# The organization or user that owns the target repository
|
86
|
+
config.owner = 'owner_name'
|
87
|
+
|
88
|
+
# The repository for this application
|
89
|
+
config.repo = 'repo_name'
|
90
|
+
|
91
|
+
# The branch for your deployed code
|
92
|
+
# config.branch = 'master'
|
93
|
+
|
94
|
+
# Additional labels to add to issues created
|
95
|
+
# config.additional_labels = ['production']
|
96
|
+
# or
|
97
|
+
# config.additional_labels = Proc.new do |exception, env|
|
98
|
+
# []
|
99
|
+
# end
|
100
|
+
|
101
|
+
# Limit the number of comments per issue
|
102
|
+
# config.comment_limit = 10
|
103
|
+
|
104
|
+
# Setting your title prefix can help with
|
105
|
+
# distinguising the issue between environments
|
106
|
+
# config.title_prefix = Rails.env
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
You can
|
111
|
+
[create an OAuth token](https://help.github.com/articles/creating-an-access-token-for-command-line-use/)
|
112
|
+
or generate an OAuth token via the
|
113
|
+
[OAuth Authorizations API](http://developer.github.com/v3/oauth/#oauth-authorizations-api) with cURL:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
curl -u <github_login> -i -d "{ \"scopes\": [\"repo\"], \"note\":[\"Test\"] }" \
|
117
|
+
https://api.github.com/authorizations
|
118
|
+
```
|
119
|
+
|
120
|
+
## Customization ##
|
121
|
+
|
122
|
+
### Labels ###
|
123
|
+
|
124
|
+
You can specify an additional array of labels that will be applied to the issues PartyFoul creates.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
PartyFoul.configure do |config|
|
128
|
+
config.additional_labels = ['front-end']
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
You can also provide a Proc that is passed the exception and the environment.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
PartyFoul.configure do |config|
|
136
|
+
config.additional_labels = Proc.new do |exception, env|
|
137
|
+
labels = if env["HTTP_HOST"] =~ /beta\./
|
138
|
+
['beta']
|
139
|
+
else
|
140
|
+
['production']
|
141
|
+
end
|
142
|
+
if exception.message =~ /PG::Error/
|
143
|
+
labels << 'database'
|
144
|
+
end
|
145
|
+
labels
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
### Background Processing ###
|
151
|
+
|
152
|
+
You can specify the adapter with which the exceptions should be
|
153
|
+
handled. By default, PartyFoul includes the
|
154
|
+
[`PartyFoul::Processors::Sync`](https://github.com/dockyard/party_foul/tree/master/lib/party_foul/processors/sync.rb)
|
155
|
+
which handles the exception synchronously. To use your own adapter,
|
156
|
+
include the following in your `PartyFoul.configure` block:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
PartyFoul.configure do |config|
|
160
|
+
config.processor = PartyFoul::Processors::MyBackgroundProcessor
|
161
|
+
end
|
162
|
+
|
163
|
+
class PartyFoul::Processors::MyBackgroundProcessor
|
164
|
+
def self.handle(exception, env)
|
165
|
+
# Enqueue the exception, then in your worker, call
|
166
|
+
# PartyFoul::ExceptionHandler.new(exception, env).run
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
```
|
171
|
+
|
172
|
+
`PartyFoul` comes with the following background processing adapters:
|
173
|
+
|
174
|
+
* [PartyFoul::Processors::Sidekiq](https://github.com/dockyard/party_foul/blob/master/lib/party_foul/processors/sidekiq.rb)
|
175
|
+
* [PartyFoul::Processors::Resque](https://github.com/dockyard/party_foul/blob/master/lib/party_foul/processors/resque.rb)
|
176
|
+
* [PartyFoul::Processors::DelayedJob](https://github.com/dockyard/party_foul/blob/master/lib/party_foul/processors/delayed_job.rb)
|
177
|
+
|
178
|
+
These adapters are not loaded by default. You must explicitly require if
|
179
|
+
you want to use:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
require 'party_foul/processors/sidekiq'
|
183
|
+
|
184
|
+
PartyFoul.configure do |config|
|
185
|
+
config.processor = PartyFoul::Processors::Sidekiq
|
186
|
+
end
|
187
|
+
```
|
188
|
+
|
189
|
+
### Limiting Comments
|
190
|
+
|
191
|
+
You can specify a limit on the number of comments added to each issue. The main issue will still be updated
|
192
|
+
with a count and time for each occurrence, regardless of the limit.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
PartyFoul.configure do |config|
|
196
|
+
config.comment_limit = 10
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
## Tracking errors outside of an HTTP request
|
201
|
+
|
202
|
+
You may want to track errors outside of a regular HTTP stack. In that
|
203
|
+
case you will need to make sure of the
|
204
|
+
`PartyFoul::RacklessExceptionHandler`.
|
205
|
+
|
206
|
+
The code that you want to handle should be wrapped like so:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
begin
|
210
|
+
... # some code that might raise an error
|
211
|
+
rescue => error
|
212
|
+
PartyFoul::RacklessExceptionHandler.handle(error, class: class_name, method: method_name, params: message)
|
213
|
+
raise error
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
### Tracking errors in a Sidekiq worker
|
218
|
+
In order to use PartyFoul for exception handling with Sidekiq you will need to create an initializer with some middleware configuration. The following example is based on using [Sidekiq with another exception notifier server](https://github.com/bugsnag/bugsnag-ruby/blob/master/lib/bugsnag/sidekiq.rb).
|
219
|
+
|
220
|
+
File: config/initializers/partyfoul_sidekiq.rb
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
module PartyFoul
|
224
|
+
class Sidekiq
|
225
|
+
def call(worker, msg, queue)
|
226
|
+
begin
|
227
|
+
yield
|
228
|
+
rescue => error
|
229
|
+
PartyFoul::RacklessExceptionHandler.handle(error, {class: worker.class.name, method: queue, params: msg})
|
230
|
+
raise error
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
::Sidekiq.configure_server do |config|
|
237
|
+
config.server_middleware do |chain|
|
238
|
+
chain.add ::PartyFoul::Sidekiq
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
This will pass the worker class name and queue as well as all worker-related parameters off to PartyFoul before passing on the exception.
|
244
|
+
|
245
|
+
## Authors ##
|
246
|
+
|
247
|
+
* [Brian Cardarella](http://twitter.com/bcardarella)
|
248
|
+
* [Dan McClain](http://twitter.com/_danmcclain)
|
249
|
+
|
250
|
+
[We are very thankful for the many contributors](https://github.com/dockyard/party_foul/graphs/contributors)
|
251
|
+
|
252
|
+
## Versioning ##
|
253
|
+
|
254
|
+
This gem follows [Semantic Versioning](http://semver.org)
|
255
|
+
|
256
|
+
## Want to help? ##
|
257
|
+
|
258
|
+
Please do! We are always looking to improve this gem. Please see our
|
259
|
+
[Contribution Guidelines](https://github.com/dockyard/party_foul/blob/master/CONTRIBUTING.md)
|
260
|
+
on how to properly submit issues and pull requests.
|
261
|
+
|
262
|
+
## Legal ##
|
263
|
+
|
264
|
+
[DockYard](http://dockyard.com), LLC © 2013
|
265
|
+
|
266
|
+
[@dockyard](http://twitter.com/dockyard)
|
267
|
+
|
268
|
+
[Licensed under the MIT license](http://www.opensource.org/licenses/mit-license.php)
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
Bundler::GemHelper.install_tasks
|
9
|
+
|
10
|
+
require 'rake/testtask'
|
11
|
+
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
13
|
+
t.libs << 'lib'
|
14
|
+
t.libs << 'test'
|
15
|
+
t.pattern = 'test/**/*_test.rb'
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => :test
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module PartyFoul
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../templates', __FILE__)
|
7
|
+
|
8
|
+
def create_initializer_file
|
9
|
+
login = ask 'GitHub login:'
|
10
|
+
password = ask 'GitHub password:'
|
11
|
+
@owner = ask_with_default "\nRepository owner:", login
|
12
|
+
@repo = ask 'Repository name:'
|
13
|
+
@api_endpoint = ask_with_default 'Api Endpoint:', 'https://api.github.com'
|
14
|
+
@web_url = ask_with_default 'Web URL:', 'https://github.com'
|
15
|
+
|
16
|
+
begin
|
17
|
+
octokit = Octokit::Client.new :login => login, :password => password, :api_endpoint => @api_endpoint
|
18
|
+
@oauth_token = octokit.create_authorization(scopes: ['repo'], note: "PartyFoul #{@owner}/#{@repo}", note_url: "#{@web_url}/#{@owner}/#{@repo}").token
|
19
|
+
template 'party_foul.rb', 'config/initializers/party_foul.rb'
|
20
|
+
rescue Octokit::Unauthorized
|
21
|
+
say 'There was an error retrieving your GitHub OAuth token'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def self.installation_message
|
28
|
+
'Generates the initializer'
|
29
|
+
end
|
30
|
+
|
31
|
+
desc installation_message
|
32
|
+
|
33
|
+
def ask_with_default(prompt, default)
|
34
|
+
value = ask("#{prompt} [#{default}]")
|
35
|
+
value.blank? ? default : value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
PartyFoul.configure do |config|
|
2
|
+
# The collection of exceptions PartyFoul should not be allowed to handle
|
3
|
+
# The constants here *must* be represented as strings
|
4
|
+
config.blacklisted_exceptions = ['ActiveRecord::RecordNotFound', 'ActionController::RoutingError']
|
5
|
+
|
6
|
+
# The OAuth token for the account that is opening the issues on GitHub
|
7
|
+
config.oauth_token = '<%= @oauth_token %>'
|
8
|
+
|
9
|
+
# The API endpoint for GitHub. Unless you are hosting a private
|
10
|
+
# instance of Enterprise GitHub you do not need to include this
|
11
|
+
config.api_endpoint = '<%= @api_endpoint %>'
|
12
|
+
|
13
|
+
# The Web URL for GitHub. Unless you are hosting a private
|
14
|
+
# instance of Enterprise GitHub you do not need to include this
|
15
|
+
config.web_url = '<%= @web_url %>'
|
16
|
+
|
17
|
+
# The organization or user that owns the target repository
|
18
|
+
config.owner = '<%= @owner %>'
|
19
|
+
|
20
|
+
# The repository for this application
|
21
|
+
config.repo = '<%= @repo %>'
|
22
|
+
|
23
|
+
# The branch for your deployed code
|
24
|
+
# config.branch = 'master'
|
25
|
+
|
26
|
+
# Additional labels to add to issues created
|
27
|
+
# config.additional_labels = ['production']
|
28
|
+
# or
|
29
|
+
# config.additional_labels = Proc.new do |exception, env|
|
30
|
+
# []
|
31
|
+
# end
|
32
|
+
|
33
|
+
# Limit the number of comments per issue
|
34
|
+
# config.comment_limit = 10
|
35
|
+
|
36
|
+
# Setting your title prefix can help with
|
37
|
+
# distinguishing the issue between environments
|
38
|
+
# config.title_prefix = Rails.env
|
39
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class PartyFoul::ExceptionHandler
|
2
|
+
attr_accessor :rendered_issue
|
3
|
+
|
4
|
+
# This handler will pass the exception and env from Rack off to a processor.
|
5
|
+
# The default PartyFoul processor will work synchronously. Processor adapters can be written
|
6
|
+
# to push this logic to a background job if desired.
|
7
|
+
#
|
8
|
+
# @param [Exception, Hash]
|
9
|
+
def self.handle(exception, env)
|
10
|
+
PartyFoul.processor.handle(exception, clean_env(env))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Makes an attempt to determine what framework is being used and will use the proper
|
14
|
+
# IssueRenderer.
|
15
|
+
#
|
16
|
+
# @param [Exception, Hash]
|
17
|
+
def initialize(exception, env)
|
18
|
+
renderer_klass = if defined?(Rails)
|
19
|
+
PartyFoul::IssueRenderers::Rails
|
20
|
+
else
|
21
|
+
PartyFoul::IssueRenderers::Rack
|
22
|
+
end
|
23
|
+
|
24
|
+
self.rendered_issue = renderer_klass.new(exception, env)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Begins to process the exception for GitHub Issues. Makes an attempt
|
28
|
+
# to find the issue. If found will update the issue. If not found will create a new issue.
|
29
|
+
def run
|
30
|
+
if issue = find_issue
|
31
|
+
update_issue(issue)
|
32
|
+
else
|
33
|
+
create_issue
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Hits the GitHub API to find the matching issue using the fingerprint.
|
38
|
+
def find_issue
|
39
|
+
find_first_issue('open') || find_first_issue('closed')
|
40
|
+
end
|
41
|
+
|
42
|
+
# Will create a new issue and a comment with the proper details. All issues are labeled as 'bug'.
|
43
|
+
def create_issue
|
44
|
+
self.sha = PartyFoul.github.references(PartyFoul.repo_path, "heads/#{PartyFoul.branch}").object.sha
|
45
|
+
issue = PartyFoul.github.create_issue(PartyFoul.repo_path, rendered_issue.title, rendered_issue.body, labels: ['bug'] + rendered_issue.labels)
|
46
|
+
PartyFoul.github.add_comment(PartyFoul.repo_path, issue[:number], rendered_issue.comment)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Updates the given issue. If the issue is labeled as 'wontfix' nothing is done. If the issue is closed the issue is reopened and labeled as 'regression'.
|
50
|
+
#
|
51
|
+
# @param [Sawyer::Resource]
|
52
|
+
def update_issue(issue)
|
53
|
+
label_names = issue.key?(:labels) ? issue[:labels].map {|label| label[:name] } : []
|
54
|
+
|
55
|
+
unless label_names.include?('wontfix')
|
56
|
+
body = rendered_issue.update_body(issue[:body])
|
57
|
+
params = {state: 'open'}
|
58
|
+
|
59
|
+
if issue[:state] == 'closed'
|
60
|
+
params[:labels] = (['bug', 'regression'] + label_names).uniq
|
61
|
+
end
|
62
|
+
|
63
|
+
self.sha = PartyFoul.github.references(PartyFoul.repo_path, "heads/#{PartyFoul.branch}").object.sha
|
64
|
+
PartyFoul.github.update_issue(PartyFoul.repo_path, issue[:number], issue.title, body, params)
|
65
|
+
|
66
|
+
unless comment_limit_met?(issue[:body])
|
67
|
+
PartyFoul.github.add_comment(PartyFoul.repo_path, issue[:number], rendered_issue.comment)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def self.clean_env(env)
|
75
|
+
env.select do |key, value|
|
76
|
+
begin
|
77
|
+
Marshal.dump(value)
|
78
|
+
rescue TypeError
|
79
|
+
true
|
80
|
+
rescue
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def fingerprint
|
87
|
+
rendered_issue.fingerprint
|
88
|
+
end
|
89
|
+
|
90
|
+
def sha=(sha)
|
91
|
+
rendered_issue.sha = sha
|
92
|
+
end
|
93
|
+
|
94
|
+
def occurrence_count(body)
|
95
|
+
result = body.match(/<th>Count<\/th><td>(\d+)<\/td>/)
|
96
|
+
result.nil? ? 0 : result[1].to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
def comment_limit_met?(body)
|
100
|
+
!!PartyFoul.comment_limit && PartyFoul.comment_limit <= occurrence_count(body)
|
101
|
+
end
|
102
|
+
|
103
|
+
def find_first_issue(state)
|
104
|
+
PartyFoul.github.search_issues("#{fingerprint} repo:#{PartyFoul.repo_path} state:#{state}").items.first
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
class PartyFoul::IssueRenderers::Base
|
4
|
+
attr_accessor :exception, :env, :sha
|
5
|
+
|
6
|
+
# A new renderer instance for GitHub issues
|
7
|
+
#
|
8
|
+
# @param [Exception, Hash]
|
9
|
+
def initialize(exception, env)
|
10
|
+
self.exception = exception
|
11
|
+
self.env = env
|
12
|
+
end
|
13
|
+
|
14
|
+
# Title of the issue with any object ids masked
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def title
|
18
|
+
_title = if PartyFoul.title_prefix
|
19
|
+
"[#{PartyFoul.title_prefix}] #{masked_title}"
|
20
|
+
else
|
21
|
+
masked_title
|
22
|
+
end
|
23
|
+
|
24
|
+
_title[0..255]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Renders the issue body
|
28
|
+
#
|
29
|
+
# Customize by overriding {#body_options}
|
30
|
+
#
|
31
|
+
# @return [String]
|
32
|
+
def body
|
33
|
+
@body ||= <<-BODY
|
34
|
+
#{build_table_from_hash(body_options)}
|
35
|
+
|
36
|
+
## Stack Trace
|
37
|
+
<pre>#{stack_trace}</pre>
|
38
|
+
Fingerprint: `#{fingerprint}`
|
39
|
+
BODY
|
40
|
+
end
|
41
|
+
|
42
|
+
# Renderes the issue comment
|
43
|
+
#
|
44
|
+
# Customize by overriding {#comment_options}
|
45
|
+
#
|
46
|
+
def comment
|
47
|
+
build_table_from_hash(comment_options)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Compiles the stack trace for use in the issue body. Lines in the
|
51
|
+
# stack trace that are part of the application will be rendered as
|
52
|
+
# links to the relative file and line on GitHub based upon
|
53
|
+
# {PartyFoul.web_url}, {PartyFoul.owner}, {PartyFoul.repo}, and
|
54
|
+
# {PartyFoul.branch}. The branch will be used at the time the
|
55
|
+
# exception happens to grab the SHA for that branch at that time for
|
56
|
+
# the purpose of linking.
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def stack_trace
|
60
|
+
exception.backtrace.map do |line|
|
61
|
+
if from_bundler?(line)
|
62
|
+
format_line(line)
|
63
|
+
elsif (matches = extract_file_name_and_line_number(line))
|
64
|
+
"<a href='#{PartyFoul.repo_url}/blob/#{sha}/#{matches[2]}#L#{matches[3]}'>#{format_line(line)}</a>"
|
65
|
+
else
|
66
|
+
format_line(line)
|
67
|
+
end
|
68
|
+
end.join("\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
# A SHA1 hex digested representation of the title. The fingerprint is
|
72
|
+
# used to create a unique value in the issue body. This value is used
|
73
|
+
# for seraching when matching issues happen again in the future.
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
def fingerprint
|
77
|
+
Digest::SHA1.hexdigest(title)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Will update the issue body. The count and the time stamp will both
|
81
|
+
# be updated. If the format of the issue body fails to match for
|
82
|
+
# whatever reason the issue body will be reset.
|
83
|
+
#
|
84
|
+
# @return [String]
|
85
|
+
def update_body(old_body)
|
86
|
+
begin
|
87
|
+
current_count = old_body.match(/<th>Count<\/th><td>(\d+)<\/td>/)[1].to_i
|
88
|
+
old_body.sub!("<th>Count</th><td>#{current_count}</td>", "<th>Count</th><td>#{current_count + 1}</td>")
|
89
|
+
old_body.sub!(/<th>Last Occurrence<\/th><td>.+?<\/td>/, "<th>Last Occurrence</th><td>#{occurred_at}</td>")
|
90
|
+
old_body
|
91
|
+
rescue
|
92
|
+
self.body
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# The timestamp when the exception occurred.
|
97
|
+
#
|
98
|
+
# @return [String]
|
99
|
+
def occurred_at
|
100
|
+
@occurred_at ||= Time.now.strftime('%B %d, %Y %H:%M:%S %z')
|
101
|
+
end
|
102
|
+
|
103
|
+
# The hash used for building the table in issue body
|
104
|
+
#
|
105
|
+
# @return [Hash]
|
106
|
+
def body_options(count = 0)
|
107
|
+
{ Exception: exception, 'Last Occurrence' => occurred_at, Count: count + 1 }
|
108
|
+
end
|
109
|
+
|
110
|
+
# The hash used for building the table in the comment body
|
111
|
+
#
|
112
|
+
# @return [Hash]
|
113
|
+
def comment_options
|
114
|
+
{ 'Occurred At' => occurred_at }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Builds an HTML table from hash
|
118
|
+
#
|
119
|
+
# @return [String]
|
120
|
+
def build_table_from_hash(hash)
|
121
|
+
"<table>#{rows_for_table_from_hash(hash)}</table>"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Builds the rows of an HTML table from hash.
|
125
|
+
# Keys as Headers cells and Values as Data cells
|
126
|
+
# If the Value is a Hash it will be rendered as a table
|
127
|
+
#
|
128
|
+
# @return [String]
|
129
|
+
def rows_for_table_from_hash(hash)
|
130
|
+
hash.inject('') do |rows, row|
|
131
|
+
key, value = row
|
132
|
+
if row[1].kind_of?(Hash)
|
133
|
+
value = build_table_from_hash(row[1])
|
134
|
+
else
|
135
|
+
value = CGI.escapeHTML(value.to_s)
|
136
|
+
end
|
137
|
+
rows << "<tr><th>#{key}</th><td>#{value}</td></tr>"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Provides additional labels using the configured options
|
142
|
+
#
|
143
|
+
# @return [Array]
|
144
|
+
def labels
|
145
|
+
if PartyFoul.additional_labels.respond_to? :call
|
146
|
+
PartyFoul.additional_labels.call(self.exception, self.env) || []
|
147
|
+
else
|
148
|
+
PartyFoul.additional_labels || []
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def app_root
|
155
|
+
Dir.pwd
|
156
|
+
end
|
157
|
+
|
158
|
+
def bundle_root
|
159
|
+
Bundler.bundle_path.to_s if defined?(Bundler)
|
160
|
+
end
|
161
|
+
|
162
|
+
def from_bundler?(line)
|
163
|
+
if bundle_root
|
164
|
+
line.match(bundle_root)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def extract_file_name_and_line_number(line)
|
169
|
+
line.match(/#{app_root}\/((.+?):(\d+))/)
|
170
|
+
end
|
171
|
+
|
172
|
+
def raw_title
|
173
|
+
raise NotImplementedError
|
174
|
+
end
|
175
|
+
|
176
|
+
def masked_title
|
177
|
+
raw_title.gsub(/0x(\w+)/, "0xXXXXXX")
|
178
|
+
end
|
179
|
+
|
180
|
+
def format_line(line)
|
181
|
+
if from_bundler?(line)
|
182
|
+
line.sub(bundle_root, '[bundle]...')
|
183
|
+
else
|
184
|
+
line.sub(app_root, '[app]...')
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|