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.
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