redmine_airbrake_backend 0.6.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd301e3a243a6ff787b2e7ecb17deb82b6fe5cd8
4
- data.tar.gz: b4064e02518fcf19aae3d6bd2000ba4d55d20f7b
3
+ metadata.gz: a4627e9e6862c801b73cf31178ff43d50ba40fae
4
+ data.tar.gz: 0205349f0793115c48e62bdfd85401903ff94421
5
5
  SHA512:
6
- metadata.gz: 5e927b9d60973da1f426a3e96f8b8692b2fa802473263dc53c94a8c14220d694eacb2b622eac8997865776c23efa920fcc08ec42976da34ff3c11808d5b18e0a
7
- data.tar.gz: 55fb70954b0ca1a8c59da66f13a29a365ee68475c5850bfb97b2af2e4a741b2eba512660e4fbabe6c75735b3b62e92be9d39be2a98e92592f5da9b6464382d60
6
+ metadata.gz: 8f50e28e27bb16e93c8d1dbf56c24f0d049d7c7e9133f70170d47536c9e0eadd0c2c359c372dc7497c52f2d7d64d95ab47ca302fec5899d42e69566b03d1f844
7
+ data.tar.gz: 5d086483a614240d063d1cd04617b4aa71733bc731ead73051499011ddced147ce72af47a691fc4cbabdee62e835c4f093b976defe7ea2a962ef1e227d1c3e3f
@@ -1,8 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+ ### Changes
5
+ - Add support for new v3 JSON notices
6
+ - Add support for new v3 JSON iOS reports
7
+ - Dropped support for old notices and reports APIs
8
+ - Add permission to use Airbrake API. No longer bound to issue create permission
9
+ - Added new key *column* in backtrace to ID calculation
10
+ - Require Redmine >= 3.2.0
11
+ - Show hashes in table sections (no recursion)
12
+
3
13
  ## 0.6.1 (2015-04-28)
4
14
  ### Fixes
5
- - Usage as redmine plugin additional to gem
15
+ - Usage as Redmine plugin additional to gem
6
16
 
7
17
  ## 0.6.0 (2015-04-22)
8
18
  ### Changed
data/README.md CHANGED
@@ -8,7 +8,7 @@ This plugin provides the necessary API to use Redmine as an Airbrake backend.
8
8
 
9
9
  ## Installation
10
10
 
11
- Add this line to your Redmine Gemfile:
11
+ Add this line to the Gemfile or Gemfile.local of your Redmine installation:
12
12
  ```ruby
13
13
  gem 'redmine_airbrake_backend'
14
14
  ```
@@ -19,7 +19,7 @@ $ bundle install
19
19
  $ rake redmine:plugins:migrate
20
20
  ```
21
21
 
22
- ### Alternate installation
22
+ ### Alternate installation method
23
23
 
24
24
  Please see http://www.redmine.org/projects/redmine/wiki/Plugins for installation instructions.
25
25
 
@@ -30,8 +30,9 @@ Please see http://www.redmine.org/projects/redmine/wiki/Plugins for installation
30
30
  * Airbrake hash (String) (required)
31
31
  * Number of occurrences (Integer) (optional)
32
32
  3. Configure the plugin to use these 2 custom fields (Administration -> Plugins -> Airbrake -> Configure)
33
- 4. Enable the project module (Airbrake) in your project settings (don't forget to add at least the Airbrake hash field to your project)
34
- 5. Configure additional defaults under the settings tab (Airbrake)
33
+ 4. Check if the custom fields are assigned to the trackers you want to use with airbrake
34
+ 5. Enable the project module (Airbrake) in your project settings (don't forget to add at least the Airbrake notice ID custom field to your project if it is not a global field)
35
+ 6. Configure additional defaults under the settings tab per project (Airbrake)
35
36
 
36
37
  ## Client configuration
37
38
 
@@ -40,20 +41,20 @@ For a Rails application add the airbrake gem to your Gemfile:
40
41
  gem 'airbrake'
41
42
  ```
42
43
 
43
- And configure it, e.g. with a initializer `config/initializers/airbrake.rb`:
44
+ And configure it, e.g. with an initializer `config/initializers/airbrake.rb`:
44
45
  ```ruby
45
- Airbrake.configure do |config|
46
- config.api_key = {
47
- project: 'redmine_project_identifier', # the identifier you specified for your project in Redmine
48
- api_key: 'redmine_api_key', # the api key for a user which has permission to create issues in the project specified in the previous step
49
- tracker: 'Bug', # the name or id of your desired tracker (optional if default is configured)
50
- category: 'Development', # the name or id of a ticket category, optional
51
- priority: 5, # the name or id of the priority for new tickets, optional.
52
- assignee: 'admin' # the login or id of a user the ticket should get assigned to by default, optional
46
+ Airbrake.configure do |c|
47
+ c.project_id = 'redmine_project_identifier'
48
+ c.project_key = {
49
+ key: 'redmine_api_key', # the api key for a user which has permission to create issues in the project specified in the previous step
50
+ tracker: 'Bug', # the name or id of your desired tracker (optional if default is configured)
51
+ category: 'Development', # the name or id of a ticket category, optional
52
+ priority: 5, # the name or id of the priority for new tickets, optional.
53
+ assignee: 'admin' # the login or id of a user the ticket should get assigned to by default, optional
53
54
  }.to_json
54
- config.host = 'my_redmine_host.com' # the hostname your Redmine runs at
55
- config.port = 443 # the port your Redmine runs at
56
- config.secure = true # sends data to your server using SSL, optional
55
+ c.host = 'https://redmine.example.com/'
56
+ c.root_directory = Rails.root
57
+ c.environment = 'production'
57
58
  end
58
59
  ```
59
60
 
@@ -1,148 +1,191 @@
1
1
  require 'tempfile'
2
- require 'redmine_airbrake_backend/request/xml'
3
- require 'redmine_airbrake_backend/request/json'
2
+ require 'redmine_airbrake_backend/error'
3
+
4
4
 
5
5
  # Controller with airbrake related stuff
6
6
  class AirbrakeController < ::ApplicationController
7
- skip_before_filter :verify_authenticity_token
7
+ class InvalidRequest < StandardError; end
8
8
 
9
- before_filter :authorize_airbrake
10
- before_filter :handle_request
9
+ skip_before_action :verify_authenticity_token
11
10
 
12
- after_filter :cleanup_tempfiles
11
+ prepend_before_action :parse_key
12
+ prepend_before_action :find_project
13
+ before_action :set_environment
14
+ before_action :authorize
13
15
 
14
- rescue_from RedmineAirbrakeBackend::Request::Error, with: :render_bad_request
16
+ after_action :cleanup_tempfiles
15
17
 
16
- private
18
+ rescue_from InvalidRequest, with: :render_bad_request
17
19
 
18
- def authorize_airbrake
19
- @project = @request.project
20
+ private
20
21
 
21
- authorize(:issues, :create)
22
+ def find_project
23
+ @project = Project.find(params[:project_id])
22
24
  end
23
25
 
24
- def render_bad_request(error)
25
- ::Rails.logger.warn(error.message) if error.is_a?(RedmineAirbrakeBackend::Request::Invalid)
26
+ def parse_key
27
+ @key = JSON.parse(params[:key]).symbolize_keys #rescue nil
26
28
 
27
- render text: error.message, status: :bad_request
29
+ # API key
30
+ raise InvalidRequest.new('No or invalid API key') if @key.blank? || @key[:key].blank?
31
+ params[:key] = @key[:key]
32
+
33
+ # Tracker
34
+ @tracker = record_for(@project.trackers, :tracker)
35
+ raise InvalidRequest.new('No or invalid tracker') if @tracker.blank?
36
+
37
+ # Notice ID field
38
+ raise InvalidRequest.new('Custom field for notice hash not available on selected tracker') if @tracker.custom_fields.find_by(id: notice_hash_field.id).blank?
39
+
40
+ # Category
41
+ @category = record_for(@project.issue_categories, :category)
42
+
43
+ # Priority
44
+ @priority = record_for(IssuePriority, :priority) || IssuePriority.default
45
+
46
+ # Assignee
47
+ @assignee = record_for(@project.users, :assignee, [:id, :login])
48
+
49
+ # Repository
50
+ @repository = @project.repositories.find_by(identifier: (@key[:repository] || ''))
51
+
52
+ # Type
53
+ @type = @key[:type] || (params[:context][:language].split('/', 2).first.downcase rescue nil)
28
54
  end
29
55
 
30
- def parse_xml_request
31
- request.body.rewind
56
+ def set_environment
57
+ @environment = params[:context][:environment].presence rescue nil
58
+ end
32
59
 
33
- @request = RedmineAirbrakeBackend::Request::XML.parse(request.body)
60
+ def render_bad_request(error)
61
+ ::Rails.logger.warn(error.message)
34
62
 
35
- params[:key] = @request.api_key
63
+ render text: error.message, status: :bad_request
36
64
  end
37
65
 
38
- def parse_json_request
39
- @request = RedmineAirbrakeBackend::Request::JSON.parse(params)
66
+ def record_for(on, key, fields = [:id, :name])
67
+ fields.each do |field|
68
+ val = on.find_by(field => @key[key])
69
+ return val if val.present?
70
+ end
40
71
 
41
- params[:key] = @request.api_key
72
+ project_setting(key)
42
73
  end
43
74
 
44
- def handle_request
45
- @tempfiles = []
75
+ def global_setting(key)
76
+ Setting.plugin_redmine_airbrake_backend[key]
77
+ end
46
78
 
47
- @results = []
79
+ def project_setting(key)
80
+ return nil if @project.airbrake_settings.blank?
48
81
 
49
- @request.errors.each do |error|
50
- issue = find_or_initialize_issue(error)
82
+ @project.airbrake_settings.send(key) if @project.airbrake_settings.respond_to?(key)
83
+ end
51
84
 
52
- set_custom_field_values(issue, @request, error.airbrake_hash)
85
+ def custom_field(key)
86
+ @project.issue_custom_fields.find_by(id: global_setting(key)) || CustomField.find_by(id: global_setting(key), is_for_all: true)
87
+ end
53
88
 
54
- reopen_issue(issue, error) if @request.reopen? && issue.status.is_closed?
89
+ def notice_hash_field
90
+ custom_field(:hash_field)
91
+ end
55
92
 
56
- if issue.save
57
- @results << {
58
- issue: issue,
59
- hash: error.airbrake_hash
60
- }
61
- end
62
- end
93
+ def occurrences_field
94
+ custom_field(:occurrences_field)
63
95
  end
64
96
 
65
97
  # Load or initialize issue by project, tracker and airbrake hash
66
98
  def find_or_initialize_issue(error)
67
- issue_ids = CustomValue.where(customized_type: Issue.name, custom_field_id: @request.notice_hash_field.id, value: error.airbrake_hash).pluck(:customized_id)
99
+ issue_ids = CustomValue.where(customized_type: Issue.name, custom_field_id: notice_hash_field.id, value: error.id).pluck(:customized_id)
68
100
 
69
- issue = Issue.where(id: issue_ids, project_id: @request.project.id, tracker_id: @request.tracker.id).first
101
+ issue = Issue.find_by(id: issue_ids, project_id: @project.id, tracker_id: @tracker.id)
70
102
 
71
103
  return issue if issue.present?
72
104
 
73
- issue = initialize_issue(error)
74
-
75
- add_error_attachments_to_issue(issue, error)
76
-
77
- issue
105
+ initialize_issue(error)
78
106
  end
79
107
 
80
108
  def initialize_issue(error)
81
- Issue.new(
109
+ issue = Issue.new(
82
110
  subject: error.subject,
83
- project: @request.project,
84
- tracker: @request.tracker,
111
+ project: @project,
112
+ tracker: @tracker,
85
113
  author: User.current,
86
- category: @request.category,
87
- priority: @request.priority,
114
+ category: @category,
115
+ priority: @priority,
88
116
  description: render_description(error),
89
- assigned_to: @request.assignee
117
+ assigned_to: @assignee
90
118
  )
119
+
120
+ add_attachments_to_issue(issue, error)
121
+
122
+ issue
91
123
  end
92
124
 
93
- def set_custom_field_values(issue, request, airbrake_hash)
125
+ def set_issue_custom_field_values(issue, error)
94
126
  custom_field_values = {}
95
127
 
96
- # Error hash
97
- custom_field_values[request.notice_hash_field.id] = airbrake_hash if issue.new_record?
128
+ # Error ID
129
+ custom_field_values[notice_hash_field.id] = error.id if issue.new_record?
98
130
 
99
131
  # Update occurrences
100
- if request.occurrences_field.present?
101
- occurrences_value = issue.custom_value_for(request.occurrences_field.id)
102
- custom_field_values[request.occurrences_field.id] = ((occurrences_value ? occurrences_value.value.to_i : 0) + 1).to_s
132
+ if occurrences_field.present?
133
+ occurrences_value = issue.custom_value_for(occurrences_field.id)
134
+ custom_field_values[occurrences_field.id] = ((occurrences_value ? occurrences_value.value.to_i : 0) + 1).to_s
103
135
  end
104
136
 
105
137
  issue.custom_field_values = custom_field_values
106
138
  end
107
139
 
108
- def add_error_attachments_to_issue(issue, error)
140
+ def add_attachments_to_issue(issue, error)
109
141
  return if error.attachments.blank?
110
142
 
111
- error.attachments.each do |attachment_data|
112
- filename = attachment_data[:filename].presence || Redmine::Utils.random_hex(16)
143
+ @tempfiles ||= []
144
+
145
+ error.attachments.each do |data|
146
+ filename = data[:filename].presence || Redmine::Utils.random_hex(16)
113
147
 
114
148
  file = Tempfile.new(filename)
115
149
  @tempfiles << file
116
150
 
117
- file.write(attachment_data[:data])
151
+ file.write(data[:data])
118
152
  file.rewind
119
153
 
120
- attachment = Attachment.new(file: file)
121
- attachment.author = User.current
122
- attachment.filename = filename
123
-
124
- issue.attachments << attachment
154
+ issue.attachments << Attachment.new(file: file, author: User.current, filename: filename)
125
155
  end
126
156
  end
127
157
 
158
+ def reopen_issue?
159
+ reopen_regexp = project_setting(:reopen_regexp)
160
+
161
+ return false if reopen_regexp.blank?
162
+ return false if @environment.blank?
163
+
164
+ !!(@environment =~ /#{reopen_regexp}/i)
165
+ end
166
+
167
+ def issue_reopen_repeat_description?
168
+ !!project_setting(:reopen_repeat_description)
169
+ end
170
+
128
171
  def reopen_issue(issue, error)
129
- return if @request.environment_name.blank?
172
+ return if @environment.blank?
130
173
 
131
- desc = "*Issue reopened after occurring again in _#{@request.environment_name}_ environment.*"
132
- desc << "\n\n#{render_description(error)}" if @request.reopen_repeat_description?
174
+ desc = "*Issue reopened after occurring again in _#{@environment}_ environment.*"
175
+ desc << "\n\n#{render_description(error)}" if issue_reopen_repeat_description?
133
176
 
134
177
  issue.status = issue.tracker.default_status
135
178
 
136
179
  issue.init_journal(User.current, desc)
137
180
 
138
- add_error_attachments_to_issue(issue, error)
181
+ add_attachments_to_issue(issue, error)
139
182
  end
140
183
 
141
184
  def render_description(error)
142
- locals = { request: @request, error: error }
185
+ locals = { error: error }
143
186
 
144
- if template_exists?("airbrake/issue_description/#{@request.type}")
145
- render_to_string("airbrake/issue_description/#{@request.type}", layout: false, locals: locals)
187
+ if template_exists?("airbrake/issue_description/#{@type}")
188
+ render_to_string("airbrake/issue_description/#{@type}", layout: false, locals: locals)
146
189
  else
147
190
  render_to_string('airbrake/issue_description/default', layout: false, locals: locals)
148
191
  end
@@ -1,25 +1,30 @@
1
1
  # Controller for airbrake notices
2
2
  class AirbrakeNoticeController < ::AirbrakeController
3
- prepend_before_filter :parse_xml_request, only: [:notice_xml]
4
- prepend_before_filter :parse_json_request, only: [:notice_json]
3
+ accept_api_auth :notices
5
4
 
6
- accept_api_auth :notice_xml, :notice_json
5
+ # Handle airbrake notices
6
+ def notices
7
+ @issue = nil
7
8
 
8
- # Handle airbrake XML notices
9
- def notice_xml
10
- render xml: {
11
- notice: {
12
- id: (@results.first[:hash] rescue nil)
13
- }
14
- }
15
- end
9
+ params[:errors].each do |e|
10
+ error = RedmineAirbrakeBackend::Error.new(e)
11
+
12
+ issue = find_or_initialize_issue(error)
13
+
14
+ set_issue_custom_field_values(issue, error)
15
+
16
+ reopen_issue(issue, error) if issue.persisted? && issue.status.is_closed? && reopen_issue?
17
+
18
+ @issue ||= issue if issue.save
19
+ end
16
20
 
17
- # Handle airbrake JSON notices
18
- def notice_json
19
- render json: {
20
- notice: {
21
- id: (@results.first[:hash] rescue nil)
21
+ if @issue.present?
22
+ render json: {
23
+ id: (CustomValue.find_by(customized_type: Issue.name, customized_id: @issue.id, custom_field_id: notice_hash_field.id).value rescue nil),
24
+ url: issue_url(@issue)
22
25
  }
23
- }
26
+ else
27
+ render json: {}
28
+ end
24
29
  end
25
30
  end
@@ -1,15 +1,29 @@
1
- # Controller for airbrake notices
1
+ require 'redmine_airbrake_backend/ios_report'
2
+
3
+
4
+ # Controller for airbrake reports
2
5
  class AirbrakeReportController < ::AirbrakeController
3
- prepend_before_filter :parse_json_request
6
+ accept_api_auth :ios_reports
7
+
8
+ # Handle airbrake iOS reports
9
+ def ios_reports
10
+ error = RedmineAirbrakeBackend::IosReport.new(params[:report])
11
+
12
+ @issue = find_or_initialize_issue(error)
13
+
14
+ set_issue_custom_field_values(@issue, error)
15
+
16
+ reopen_issue(@issue, error) if @issue.persisted? && @issue.status.is_closed? && reopen_issue?
17
+
18
+ unless @issue.save
19
+ render json: {}
4
20
 
5
- accept_api_auth :report
21
+ return
22
+ end
6
23
 
7
- # Handle airbrake reports
8
- def report
9
24
  render json: {
10
- report: {
11
- id: (@results.first[:hash] rescue nil)
12
- }
25
+ id: (CustomValue.find_by(customized_type: Issue.name, customized_id: @issue.id, custom_field_id: notice_hash_field.id).value rescue nil),
26
+ url: issue_url(@issue)
13
27
  }
14
28
  end
15
29
  end
@@ -3,18 +3,13 @@ module AirbrakeHelper
3
3
  def format_table(data)
4
4
  lines = []
5
5
  data.each do |key, value|
6
- next unless value.is_a?(String)
7
- lines << "|@#{key}@|#{value.strip.blank? ? value : "@#{value}@"}|"
8
- end
9
- lines.join("\n")
10
- end
6
+ next if value.blank?
11
7
 
12
- # Wiki markup for logs
13
- def format_log(data)
14
- lines = []
15
- data.each do |log|
16
- next unless log.is_a?(Hash)
17
- lines << "[#{log[:time].strftime('%F %T')}] #{log[:line]}"
8
+ if value.is_a?(String)
9
+ lines << "|@#{key}@|@#{value}@|"
10
+ elsif value.is_a?(Hash)
11
+ lines << "|@#{key}@|@#{value.map { |k, v| "#{k}: #{v}"}.join(',')}@|"
12
+ end
18
13
  end
19
14
  lines.join("\n")
20
15
  end
@@ -31,28 +26,28 @@ module AirbrakeHelper
31
26
  repository = repository_for_backtrace_element(element)
32
27
 
33
28
  if repository.blank?
34
- if element[:number].blank?
35
- markup = "@#{element[:file]}@"
29
+ if element.line.blank?
30
+ markup = "@#{element.file}@"
36
31
  else
37
- markup = "@#{element[:file]}:#{element[:number]}@"
32
+ markup = "@#{element.file}:#{element.line}@"
38
33
  end
39
34
  else
40
35
  filename = filename_for_backtrace_element(element)
41
36
 
42
37
  if repository.identifier.blank?
43
- markup = "source:\"#{filename}#L#{element[:number]}\""
38
+ markup = "source:\"#{filename}#L#{element.line}\""
44
39
  else
45
- markup = "source:\"#{repository.identifier}|#{filename}#L#{element[:number]}\""
40
+ markup = "source:\"#{repository.identifier}|#{filename}#L#{element.line}\""
46
41
  end
47
42
  end
48
43
 
49
- markup + " in ??<notextile>#{element[:method]}</notextile>??"
44
+ markup + " in ??<notextile>#{element.function}</notextile>??"
50
45
  end
51
46
 
52
47
  private
53
48
 
54
49
  def repository_for_backtrace_element(element)
55
- return nil unless element[:file].start_with?('[PROJECT_ROOT]')
50
+ return nil unless element.file.start_with?('[PROJECT_ROOT]')
56
51
 
57
52
  filename = filename_for_backtrace_element(element)
58
53
 
@@ -62,18 +57,18 @@ module AirbrakeHelper
62
57
  def repositories_for_backtrace
63
58
  return @_bactrace_repositories unless @_bactrace_repositories.nil?
64
59
 
65
- if @request.repository.present?
66
- @_bactrace_repositories = [@request.repository]
60
+ if @repository.present?
61
+ @_bactrace_repositories = [@repository]
67
62
  else
68
- @_bactrace_repositories = @request.project.repositories.to_a
63
+ @_bactrace_repositories = @project.repositories.to_a
69
64
  end
70
65
 
71
66
  @_bactrace_repositories
72
67
  end
73
68
 
74
69
  def filename_for_backtrace_element(element)
75
- return nil if element[:file].blank?
70
+ return nil if element.file.blank?
76
71
 
77
- element[:file][14..-1]
72
+ element.file[14..-1]
78
73
  end
79
74
  end