party_fouls 1.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +268 -0
  3. data/Rakefile +19 -0
  4. data/lib/generators/party_foul/install_generator.rb +38 -0
  5. data/lib/generators/party_foul/templates/party_foul.rb +39 -0
  6. data/lib/party_foul/exception_handler.rb +106 -0
  7. data/lib/party_foul/issue_renderers/base.rb +187 -0
  8. data/lib/party_foul/issue_renderers/rack.rb +54 -0
  9. data/lib/party_foul/issue_renderers/rackless.rb +25 -0
  10. data/lib/party_foul/issue_renderers/rails.rb +35 -0
  11. data/lib/party_foul/issue_renderers.rb +5 -0
  12. data/lib/party_foul/middleware.rb +32 -0
  13. data/lib/party_foul/processors/base.rb +11 -0
  14. data/lib/party_foul/processors/delayed_job.rb +16 -0
  15. data/lib/party_foul/processors/resque.rb +16 -0
  16. data/lib/party_foul/processors/sidekiq.rb +17 -0
  17. data/lib/party_foul/processors/sync.rb +11 -0
  18. data/lib/party_foul/processors.rb +2 -0
  19. data/lib/party_foul/rackless_exception_handler.rb +17 -0
  20. data/lib/party_foul/version.rb +3 -0
  21. data/lib/party_foul.rb +92 -0
  22. data/test/generator_test.rb +26 -0
  23. data/test/party_foul/configure_test.rb +37 -0
  24. data/test/party_foul/exception_handler_test.rb +205 -0
  25. data/test/party_foul/issue_renderers/base_test.rb +210 -0
  26. data/test/party_foul/issue_renderers/rack_test.rb +80 -0
  27. data/test/party_foul/issue_renderers/rackless_test.rb +29 -0
  28. data/test/party_foul/issue_renderers/rails_test.rb +83 -0
  29. data/test/party_foul/middleware_test.rb +48 -0
  30. data/test/party_foul/rackless_exception_handler_test.rb +33 -0
  31. data/test/test_helper.rb +42 -0
  32. data/test/tmp/config/initializers/party_foul.rb +39 -0
  33. 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 &copy; 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