party_foul 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -87,6 +87,9 @@ PartyFoul.configure do |config|
87
87
 
88
88
  # The repository for this application
89
89
  config.repo = 'repo_name'
90
+
91
+ # The branch for your deployed code
92
+ # config.branch = 'master'
90
93
  end
91
94
  ```
92
95
 
@@ -147,9 +150,8 @@ the change in one of the [different issue renderer adapters](https://github.com/
147
150
 
148
151
  ## Authors ##
149
152
 
150
- [Brian Cardarella](http://twitter.com/bcardarella)
151
-
152
- [Dan McClain](http://twitter.com/_danmcclain)
153
+ * [Brian Cardarella](http://twitter.com/bcardarella)
154
+ * [Dan McClain](http://twitter.com/_danmcclain)
153
155
 
154
156
  [We are very thankful for the many contributors](https://github.com/dockyard/party_foul/graphs/contributors)
155
157
 
@@ -22,4 +22,7 @@ PartyFoul.configure do |config|
22
22
 
23
23
  # The repository for this application
24
24
  config.repo = '<%= @repo %>'
25
+
26
+ # The branch for your deployed code
27
+ # config.branch = 'master'
25
28
  end
@@ -1,10 +1,19 @@
1
1
  class PartyFoul::ExceptionHandler
2
2
  attr_accessor :rendered_issue
3
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]
4
9
  def self.handle(exception, env)
5
10
  PartyFoul.processor.handle(exception, env)
6
11
  end
7
12
 
13
+ # Makes an attempt to determine what framework is being used and will use the proper
14
+ # IssueRenderer.
15
+ #
16
+ # @param [Exception, Hash]
8
17
  def initialize(exception, env)
9
18
  renderer_klass = if defined?(Rails)
10
19
  PartyFoul::RailsIssueRenderer
@@ -15,6 +24,8 @@ class PartyFoul::ExceptionHandler
15
24
  self.rendered_issue = renderer_klass.new(exception, env)
16
25
  end
17
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.
18
29
  def run
19
30
  if issue = find_issue
20
31
  update_issue(issue)
@@ -23,6 +34,7 @@ class PartyFoul::ExceptionHandler
23
34
  end
24
35
  end
25
36
 
37
+ # Hits the Github API to find the matching issue using the fingerprint.
26
38
  def find_issue
27
39
  unless issue = PartyFoul.github.search.issues(owner: PartyFoul.owner, repo: PartyFoul.repo, state: 'open', keyword: fingerprint).issues.first
28
40
  issue = PartyFoul.github.search.issues(owner: PartyFoul.owner, repo: PartyFoul.repo, state: 'closed', keyword: fingerprint).issues.first
@@ -31,11 +43,16 @@ class PartyFoul::ExceptionHandler
31
43
  issue
32
44
  end
33
45
 
46
+ # Will create a new issue and a comment with the proper details. All issues are labeled as 'bug'.
34
47
  def create_issue
48
+ self.sha = PartyFoul.github.git_data.references.get(PartyFoul.owner, PartyFoul.repo, "heads/#{PartyFoul.branch}").object.sha
35
49
  issue = PartyFoul.github.issues.create(PartyFoul.owner, PartyFoul.repo, title: rendered_issue.title, body: rendered_issue.body, labels: ['bug'])
36
50
  PartyFoul.github.issues.comments.create(PartyFoul.owner, PartyFoul.repo, issue['number'], body: rendered_issue.comment)
37
51
  end
38
52
 
53
+ # 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'.
54
+ #
55
+ # @param [Hashie::Mash]
39
56
  def update_issue(issue)
40
57
  unless issue.key?('labels') && issue['labels'].include?('wontfix')
41
58
  params = {body: rendered_issue.update_body(issue['body']), state: 'open'}
@@ -44,6 +61,7 @@ class PartyFoul::ExceptionHandler
44
61
  params[:labels] = ['bug', 'regression']
45
62
  end
46
63
 
64
+ self.sha = PartyFoul.github.git_data.references.get(PartyFoul.owner, PartyFoul.repo, "heads/#{PartyFoul.branch}").object.sha
47
65
  PartyFoul.github.issues.edit(PartyFoul.owner, PartyFoul.repo, issue['number'], params)
48
66
  PartyFoul.github.issues.comments.create(PartyFoul.owner, PartyFoul.repo, issue['number'], body: rendered_issue.comment)
49
67
  end
@@ -54,4 +72,8 @@ class PartyFoul::ExceptionHandler
54
72
  def fingerprint
55
73
  rendered_issue.fingerprint
56
74
  end
75
+
76
+ def sha=(sha)
77
+ rendered_issue.sha = sha
78
+ end
57
79
  end
@@ -1,37 +1,68 @@
1
1
  class PartyFoul::IssueRenderer
2
- attr_accessor :exception, :env
2
+ attr_accessor :exception, :env, :sha
3
3
 
4
+ # A new renderer instance for Githug issues
5
+ #
6
+ # @param [Exception, Hash]
4
7
  def initialize(exception, env)
5
8
  self.exception = exception
6
9
  self.env = env
7
10
  end
8
11
 
12
+ # Derived title of the issue. Must be implemented by the adapter class
13
+ #
14
+ # @return [NotImplementedError]
9
15
  def title
10
16
  raise NotImplementedError
11
17
  end
12
18
 
19
+ # Will compile the template for an issue body as defined in
20
+ # {PartyFoul.issue_template}
21
+ #
22
+ # @return [String]
13
23
  def body
14
24
  compile_template(PartyFoul.issue_template)
15
25
  end
16
26
 
27
+ # Will compile the template for a comment body as defined in
28
+ # {PartyFoul.comment_template}
17
29
  def comment
18
30
  compile_template(PartyFoul.comment_template)
19
31
  end
20
32
 
33
+ # Compiles the stack trace for use in the issue body. Lines in the
34
+ # stack trace that are part of the application will be rendered as
35
+ # links to the relative file and line on Github based upon
36
+ # {PartyFoul.web_url}, {PartyFoul.owner}, {PartyFoul.repo}, and
37
+ # {PartyFoul.branch}. The branch will be used at the time the
38
+ # exception happens to grab the SHA for that branch at that time for
39
+ # the purpose of linking.
40
+ #
41
+ # @return [String]
21
42
  def stack_trace
22
43
  exception.backtrace.map do |line|
23
44
  if matches = extract_file_name_and_line_number(line)
24
- "<a href='#{PartyFoul.repo_url}/tree/master/#{matches[2]}#L#{matches[3]}'>#{line}</a>"
45
+ "<a href='#{PartyFoul.repo_url}/blob/#{sha}/#{matches[2]}#L#{matches[3]}'>#{line}</a>"
25
46
  else
26
47
  line
27
48
  end
28
49
  end.join("\n")
29
50
  end
30
51
 
52
+ # A SHA1 hex digested representation of the title. The fingerprint is
53
+ # used to create a unique value in the issue body. This value is used
54
+ # for seraching when matching issues happen again in the future.
55
+ #
56
+ # @return [String]
31
57
  def fingerprint
32
58
  Digest::SHA1.hexdigest(title)
33
59
  end
34
60
 
61
+ # Will update the issue body. The count and the time stamp will both
62
+ # be updated. If the format of the issue body fails to match for
63
+ # whatever reason the issue body will be reset.
64
+ #
65
+ # @return [String]
35
66
  def update_body(old_body)
36
67
  begin
37
68
  current_count = old_body.match(/<th>Count<\/th><td>(\d+)<\/td>/)[1].to_i
@@ -43,22 +74,39 @@ class PartyFoul::IssueRenderer
43
74
  end
44
75
  end
45
76
 
77
+ # The params hash at the time the exception occurred. This method is
78
+ # overriden for each framework adapter. It should return a hash.
79
+ #
80
+ # @return [NotImplementedError]
46
81
  def params
47
82
  raise NotImplementedError
48
83
  end
49
84
 
85
+ # The timestamp when the exception occurred.
86
+ #
87
+ # @return [String]
50
88
  def occurred_at
51
89
  Time.now.strftime('%B %d, %Y %H:%M:%S %z')
52
90
  end
53
91
 
92
+ # IP address of the client who triggered the exception
93
+ #
94
+ # @return [String]
54
95
  def ip_address
55
96
  env['REMOTE_ADDR']
56
97
  end
57
98
 
99
+ # The session hash for the client at the time of the exception
100
+ #
101
+ # @return [Hash]
58
102
  def session
59
103
  env['rack.session']
60
104
  end
61
105
 
106
+ # HTTP Headers hash from the request. Headers can be filtered out by
107
+ # adding matching key names to {PartyFoul.filtered_http_headers}
108
+ #
109
+ # @return [Hash]
62
110
  def http_headers
63
111
  env.keys.select { |key| key =~ /^HTTP_(\w+)/ && !(PartyFoul.filtered_http_headers || []).include?($1.split('_').map(&:capitalize).join('-')) }.sort.inject({}) do |hash, key|
64
112
  hash[key.split('HTTP_').last.split('_').map(&:capitalize).join('-')] = env[key]
@@ -2,10 +2,16 @@ require 'party_foul/issue_renderer'
2
2
 
3
3
  module PartyFoul
4
4
  class RackIssueRenderer < IssueRenderer
5
+ # Rack params
6
+ #
7
+ # @return [Hash]
5
8
  def params
6
9
  env['QUERY_STRING']
7
10
  end
8
11
 
12
+ # Title for the issue comprised of (exception) "message"
13
+ #
14
+ # @return [String]
9
15
  def title
10
16
  %{(#{exception.class}) "#{exception.message}"}
11
17
  end
@@ -2,11 +2,17 @@ require 'party_foul/issue_renderer'
2
2
 
3
3
  module PartyFoul
4
4
  class RailsIssueRenderer < IssueRenderer
5
+ # Rails params hash. Filtered parms are respected.
6
+ #
7
+ # @return [Hash]
5
8
  def params
6
9
  parameter_filter = ActionDispatch::Http::ParameterFilter.new(env["action_dispatch.parameter_filter"])
7
10
  parameter_filter.filter(env['action_dispatch.request.path_parameters'])
8
11
  end
9
12
 
13
+ # Title for the issue comprised of Controller#action (exception) "message"
14
+ #
15
+ # @return [String]
10
16
  def title
11
17
  %{#{env['action_controller.instance'].class}##{env['action_dispatch.request.path_parameters']['action']} (#{exception.class}) "#{exception.message}"}
12
18
  end
@@ -1,4 +1,8 @@
1
1
  class PartyFoul::Processors::Sync
2
+ # Passes the exception and rack env data to the ExceptionHandler and
3
+ # runs everything synchronously.
4
+ #
5
+ # @param [Exception, Hash]
2
6
  def self.handle(exception, env)
3
7
  PartyFoul::ExceptionHandler.new(exception, env).run
4
8
  end
@@ -1,3 +1,3 @@
1
1
  module PartyFoul
2
- VERSION = '0.4.0'
2
+ VERSION = '0.5.0'
3
3
  end
data/lib/party_foul.rb CHANGED
@@ -4,13 +4,27 @@ module PartyFoul
4
4
  class << self
5
5
  attr_accessor :github, :oauth_token, :endpoint, :owner, :repo,
6
6
  :ignored_exceptions, :processor, :issue_template,
7
- :comment_template, :filtered_http_headers, :web_url
7
+ :comment_template, :filtered_http_headers, :web_url, :branch
8
8
  end
9
9
 
10
+ # The git branch that is used for linking in the stack trace
11
+ #
12
+ # @return [String] Defaults to 'master' if not set
13
+ def self.branch
14
+ @branch ||= 'master'
15
+ end
16
+
17
+ # The web url for Github. This is only interesting for Enterprise
18
+ # users
19
+ #
20
+ # @return [String] Defaults to 'https://github.com' if not set
10
21
  def self.web_url
11
22
  @web_url ||= 'https://github.com'
12
23
  end
13
24
 
25
+ # The template used for rendering the body of a new issue
26
+ #
27
+ # @return [String]
14
28
  def self.issue_template
15
29
  @issue_template ||=
16
30
  <<-TEMPLATE
@@ -26,6 +40,9 @@ Fingerprint: `:fingerprint`
26
40
  TEMPLATE
27
41
  end
28
42
 
43
+ # The template used for rendering the body of a new comment
44
+ #
45
+ # @return [String]
29
46
  def self.comment_template
30
47
  @comment_template ||=
31
48
  <<-TEMPLATE
@@ -39,14 +56,38 @@ Fingerprint: `:fingerprint`
39
56
  TEMPLATE
40
57
  end
41
58
 
59
+ # The collection of exceptions that should not be captured. Members of
60
+ # the collection must be string representations of the exception. For
61
+ # example:
62
+ #
63
+ # # This is good
64
+ # ['ActiveRecord::RecordNotFound']
65
+ #
66
+ # # This is not
67
+ # [ActiveRecord::RecordNotFound]
68
+ #
69
+ # @return [Array]
42
70
  def self.ignored_exceptions
43
71
  @ignored_exceptions || []
44
72
  end
45
73
 
74
+ # The url of the repository. Built using the {.web_url}, {.owner}, and {.repo}
75
+ # values
76
+ #
77
+ # @return [String]
46
78
  def self.repo_url
47
79
  "#{web_url}/#{owner}/#{repo}"
48
80
  end
49
81
 
82
+ # The configure block for PartyFoul. Use to initialize settings
83
+ #
84
+ # PartyFoul.configure do |config|
85
+ # config.owner 'dockyard'
86
+ # config.repo 'test_app'
87
+ # config.oauth_token = ENV['oauth_token']
88
+ # end
89
+ #
90
+ # @param [Block]
50
91
  def self.configure(&block)
51
92
  yield self
52
93
  self.processor ||= PartyFoul::Processors::Sync
@@ -14,6 +14,7 @@ describe 'Party Foul Confg' do
14
14
  config.endpoint = 'http://api.example.com'
15
15
  config.owner = 'test_owner'
16
16
  config.repo = 'test_repo'
17
+ config.branch = 'master'
17
18
  end
18
19
 
19
20
  PartyFoul.ignored_exceptions.must_equal ['StandardError']
@@ -23,5 +24,11 @@ describe 'Party Foul Confg' do
23
24
  PartyFoul.owner.must_equal 'test_owner'
24
25
  PartyFoul.repo.must_equal 'test_repo'
25
26
  PartyFoul.repo_url.must_equal 'http://example.com/test_owner/test_repo'
27
+ PartyFoul.branch.must_equal 'master'
28
+ end
29
+
30
+ it 'has default values' do
31
+ PartyFoul.web_url.must_equal 'https://github.com'
32
+ PartyFoul.branch.must_equal 'master'
26
33
  end
27
34
  end
@@ -8,8 +8,10 @@ describe 'Party Foul Exception Handler' do
8
8
  config.repo = 'test_repo'
9
9
  end
10
10
 
11
+ PartyFoul.stubs(:branch).returns('deploy')
11
12
  PartyFoul.github.stubs(:issues).returns(mock('Issues'))
12
13
  PartyFoul.github.stubs(:search).returns(mock('Search'))
14
+ PartyFoul.github.git_data.references.stubs(:get)
13
15
  PartyFoul.github.issues.stubs(:create)
14
16
  PartyFoul.github.issues.stubs(:edit)
15
17
  PartyFoul.github.issues.stubs(:comments).returns(mock('Comments'))
@@ -25,6 +27,7 @@ describe 'Party Foul Exception Handler' do
25
27
  PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'open').returns(Hashie::Mash.new(issues: []))
26
28
  PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'closed').returns(Hashie::Mash.new(issues: []))
27
29
  PartyFoul.github.issues.expects(:create).with('test_owner', 'test_repo', title: 'Test Title', body: 'Test Body', :labels => ['bug']).returns(Hashie::Mash.new('number' => 1))
30
+ PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
28
31
  PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
29
32
  PartyFoul::ExceptionHandler.new(nil, {}).run
30
33
  end
@@ -41,6 +44,7 @@ describe 'Party Foul Exception Handler' do
41
44
  PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'open').returns(Hashie::Mash.new(issues: [{title: 'Test Title', body: 'Test Body', state: 'open', number: 1}]))
42
45
  PartyFoul.github.issues.expects(:edit).with('test_owner', 'test_repo', 1, body: 'New Body', state: 'open')
43
46
  PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
47
+ PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
44
48
  PartyFoul::ExceptionHandler.new(nil, {}).run
45
49
  end
46
50
  end
@@ -51,6 +55,7 @@ describe 'Party Foul Exception Handler' do
51
55
  PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'closed').returns(Hashie::Mash.new(issues: [{title: 'Test Title', body: 'Test Body', state: 'closed', number: 1}]))
52
56
  PartyFoul.github.issues.expects(:edit).with('test_owner', 'test_repo', 1, body: 'New Body', state: 'open', labels: ['bug', 'regression'])
53
57
  PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
58
+ PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
54
59
  PartyFoul::ExceptionHandler.new(nil, {}).run
55
60
  end
56
61
  end
@@ -64,6 +69,7 @@ describe 'Party Foul Exception Handler' do
64
69
  PartyFoul.github.issues.expects(:create).never
65
70
  PartyFoul.github.issues.expects(:edit).never
66
71
  PartyFoul.github.issues.comments.expects(:create).never
72
+ PartyFoul.github.git_data.references.expects(:get).never
67
73
  PartyFoul::ExceptionHandler.new(nil, {}).run
68
74
  end
69
75
  end
data/test/test_helper.rb CHANGED
@@ -21,7 +21,7 @@ module MiniTest::Expectations
21
21
  end
22
22
 
23
23
  def clean_up_party
24
- %w{github oauth_token endpoint owner repo ignored_exceptions processor issue_template comment_template filtered_http_headers web_url}.each do |attr|
24
+ %w{github oauth_token endpoint owner repo ignored_exceptions processor issue_template comment_template filtered_http_headers web_url branch}.each do |attr|
25
25
  PartyFoul.send("#{attr}=", nil)
26
26
  end
27
27
  end
@@ -22,4 +22,7 @@ PartyFoul.configure do |config|
22
22
 
23
23
  # The repository for this application
24
24
  config.repo = 'test_repo'
25
+
26
+ # The branch for your deployed code
27
+ # config.branch = 'master'
25
28
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: party_foul
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-01-27 00:00:00.000000000 Z
13
+ date: 2013-01-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: github_api
@@ -200,7 +200,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
200
  version: '0'
201
201
  segments:
202
202
  - 0
203
- hash: -3123915090860232817
203
+ hash: -549281873562555121
204
204
  required_rubygems_version: !ruby/object:Gem::Requirement
205
205
  none: false
206
206
  requirements:
@@ -209,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
209
  version: '0'
210
210
  segments:
211
211
  - 0
212
- hash: -3123915090860232817
212
+ hash: -549281873562555121
213
213
  requirements: []
214
214
  rubyforge_project:
215
215
  rubygems_version: 1.8.23