party_fouls 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|