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 +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +17 -16
- data/app/controllers/airbrake_controller.rb +113 -70
- data/app/controllers/airbrake_notice_controller.rb +22 -17
- data/app/controllers/airbrake_report_controller.rb +22 -8
- data/app/helpers/airbrake_helper.rb +18 -23
- data/app/views/airbrake/issue_description/default.erb +21 -27
- data/config/locales/de.yml +1 -1
- data/config/locales/en.yml +1 -1
- data/config/routes.rb +3 -4
- data/lib/redmine_airbrake_backend.rb +4 -0
- data/lib/redmine_airbrake_backend/backtrace_element.rb +37 -0
- data/lib/redmine_airbrake_backend/engine.rb +2 -1
- data/lib/redmine_airbrake_backend/error.rb +21 -45
- data/lib/redmine_airbrake_backend/ios_report.rb +87 -0
- data/lib/redmine_airbrake_backend/version.rb +1 -1
- data/redmine_airbrake_backend.gemspec +0 -1
- metadata +5 -21
- data/lib/redmine_airbrake_backend/report/ios.rb +0 -82
- data/lib/redmine_airbrake_backend/request.rb +0 -133
- data/lib/redmine_airbrake_backend/request/json.rb +0 -85
- data/lib/redmine_airbrake_backend/request/xml.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4627e9e6862c801b73cf31178ff43d50ba40fae
|
4
|
+
data.tar.gz: 0205349f0793115c48e62bdfd85401903ff94421
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f50e28e27bb16e93c8d1dbf56c24f0d049d7c7e9133f70170d47536c9e0eadd0c2c359c372dc7497c52f2d7d64d95ab47ca302fec5899d42e69566b03d1f844
|
7
|
+
data.tar.gz: 5d086483a614240d063d1cd04617b4aa71733bc731ead73051499011ddced147ce72af47a691fc4cbabdee62e835c4f093b976defe7ea2a962ef1e227d1c3e3f
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
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.
|
34
|
-
5.
|
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
|
44
|
+
And configure it, e.g. with an initializer `config/initializers/airbrake.rb`:
|
44
45
|
```ruby
|
45
|
-
Airbrake.configure do |
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
tracker:
|
50
|
-
category: 'Development',
|
51
|
-
priority: 5,
|
52
|
-
assignee: 'admin'
|
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
|
-
|
55
|
-
|
56
|
-
|
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/
|
3
|
-
|
2
|
+
require 'redmine_airbrake_backend/error'
|
3
|
+
|
4
4
|
|
5
5
|
# Controller with airbrake related stuff
|
6
6
|
class AirbrakeController < ::ApplicationController
|
7
|
-
|
7
|
+
class InvalidRequest < StandardError; end
|
8
8
|
|
9
|
-
|
10
|
-
before_filter :handle_request
|
9
|
+
skip_before_action :verify_authenticity_token
|
11
10
|
|
12
|
-
|
11
|
+
prepend_before_action :parse_key
|
12
|
+
prepend_before_action :find_project
|
13
|
+
before_action :set_environment
|
14
|
+
before_action :authorize
|
13
15
|
|
14
|
-
|
16
|
+
after_action :cleanup_tempfiles
|
15
17
|
|
16
|
-
|
18
|
+
rescue_from InvalidRequest, with: :render_bad_request
|
17
19
|
|
18
|
-
|
19
|
-
@project = @request.project
|
20
|
+
private
|
20
21
|
|
21
|
-
|
22
|
+
def find_project
|
23
|
+
@project = Project.find(params[:project_id])
|
22
24
|
end
|
23
25
|
|
24
|
-
def
|
25
|
-
|
26
|
+
def parse_key
|
27
|
+
@key = JSON.parse(params[:key]).symbolize_keys #rescue nil
|
26
28
|
|
27
|
-
|
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
|
31
|
-
|
56
|
+
def set_environment
|
57
|
+
@environment = params[:context][:environment].presence rescue nil
|
58
|
+
end
|
32
59
|
|
33
|
-
|
60
|
+
def render_bad_request(error)
|
61
|
+
::Rails.logger.warn(error.message)
|
34
62
|
|
35
|
-
|
63
|
+
render text: error.message, status: :bad_request
|
36
64
|
end
|
37
65
|
|
38
|
-
def
|
39
|
-
|
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
|
-
|
72
|
+
project_setting(key)
|
42
73
|
end
|
43
74
|
|
44
|
-
def
|
45
|
-
|
75
|
+
def global_setting(key)
|
76
|
+
Setting.plugin_redmine_airbrake_backend[key]
|
77
|
+
end
|
46
78
|
|
47
|
-
|
79
|
+
def project_setting(key)
|
80
|
+
return nil if @project.airbrake_settings.blank?
|
48
81
|
|
49
|
-
@
|
50
|
-
|
82
|
+
@project.airbrake_settings.send(key) if @project.airbrake_settings.respond_to?(key)
|
83
|
+
end
|
51
84
|
|
52
|
-
|
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
|
-
|
89
|
+
def notice_hash_field
|
90
|
+
custom_field(:hash_field)
|
91
|
+
end
|
55
92
|
|
56
|
-
|
57
|
-
|
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:
|
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.
|
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
|
-
|
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: @
|
84
|
-
tracker: @
|
111
|
+
project: @project,
|
112
|
+
tracker: @tracker,
|
85
113
|
author: User.current,
|
86
|
-
category: @
|
87
|
-
priority: @
|
114
|
+
category: @category,
|
115
|
+
priority: @priority,
|
88
116
|
description: render_description(error),
|
89
|
-
assigned_to: @
|
117
|
+
assigned_to: @assignee
|
90
118
|
)
|
119
|
+
|
120
|
+
add_attachments_to_issue(issue, error)
|
121
|
+
|
122
|
+
issue
|
91
123
|
end
|
92
124
|
|
93
|
-
def
|
125
|
+
def set_issue_custom_field_values(issue, error)
|
94
126
|
custom_field_values = {}
|
95
127
|
|
96
|
-
# Error
|
97
|
-
custom_field_values[
|
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
|
101
|
-
occurrences_value = issue.custom_value_for(
|
102
|
-
custom_field_values[
|
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
|
140
|
+
def add_attachments_to_issue(issue, error)
|
109
141
|
return if error.attachments.blank?
|
110
142
|
|
111
|
-
|
112
|
-
|
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(
|
151
|
+
file.write(data[:data])
|
118
152
|
file.rewind
|
119
153
|
|
120
|
-
|
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 @
|
172
|
+
return if @environment.blank?
|
130
173
|
|
131
|
-
desc = "*Issue reopened after occurring again in _#{@
|
132
|
-
desc << "\n\n#{render_description(error)}" if
|
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
|
-
|
181
|
+
add_attachments_to_issue(issue, error)
|
139
182
|
end
|
140
183
|
|
141
184
|
def render_description(error)
|
142
|
-
locals = {
|
185
|
+
locals = { error: error }
|
143
186
|
|
144
|
-
if template_exists?("airbrake/issue_description/#{@
|
145
|
-
render_to_string("airbrake/issue_description/#{@
|
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
|
-
|
4
|
-
prepend_before_filter :parse_json_request, only: [:notice_json]
|
3
|
+
accept_api_auth :notices
|
5
4
|
|
6
|
-
|
5
|
+
# Handle airbrake notices
|
6
|
+
def notices
|
7
|
+
@issue = nil
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
1
|
+
require 'redmine_airbrake_backend/ios_report'
|
2
|
+
|
3
|
+
|
4
|
+
# Controller for airbrake reports
|
2
5
|
class AirbrakeReportController < ::AirbrakeController
|
3
|
-
|
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
|
-
|
21
|
+
return
|
22
|
+
end
|
6
23
|
|
7
|
-
# Handle airbrake reports
|
8
|
-
def report
|
9
24
|
render json: {
|
10
|
-
|
11
|
-
|
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
|
7
|
-
lines << "|@#{key}@|#{value.strip.blank? ? value : "@#{value}@"}|"
|
8
|
-
end
|
9
|
-
lines.join("\n")
|
10
|
-
end
|
6
|
+
next if value.blank?
|
11
7
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
35
|
-
markup = "@#{element
|
29
|
+
if element.line.blank?
|
30
|
+
markup = "@#{element.file}@"
|
36
31
|
else
|
37
|
-
markup = "@#{element
|
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
|
38
|
+
markup = "source:\"#{filename}#L#{element.line}\""
|
44
39
|
else
|
45
|
-
markup = "source:\"#{repository.identifier}|#{filename}#L#{element
|
40
|
+
markup = "source:\"#{repository.identifier}|#{filename}#L#{element.line}\""
|
46
41
|
end
|
47
42
|
end
|
48
43
|
|
49
|
-
markup + " in ??<notextile>#{element
|
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
|
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 @
|
66
|
-
@_bactrace_repositories = [@
|
60
|
+
if @repository.present?
|
61
|
+
@_bactrace_repositories = [@repository]
|
67
62
|
else
|
68
|
-
@_bactrace_repositories = @
|
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
|
70
|
+
return nil if element.file.blank?
|
76
71
|
|
77
|
-
element
|
72
|
+
element.file[14..-1]
|
78
73
|
end
|
79
74
|
end
|