redmine_airbrake_backend 0.6.2 → 1.0.0

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