redmine_airbrake_backend 1.0.3 → 1.1.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: 3e17bf3121cc154d47b38ad482b7269a04b30f25
4
- data.tar.gz: 8a26cee201e9a7d6b5b8e9a7b3491e76ad4da4c5
3
+ metadata.gz: 5926fca2b10cfdf572494c9891adc633b583bbdb
4
+ data.tar.gz: 0dc91282b47937a48254ff1e3da0fbd4b555c0be
5
5
  SHA512:
6
- metadata.gz: cf46bb9b3d773987e39309ecc34d6dba06da4649f35459e11d1dae3ffdd6586d366b9f8fa7c4e979b23c2db3f5b966e995de3663b84058be5dbc7759c117b409
7
- data.tar.gz: b3cf1f9f4514a42fa90e1019df70ed9dbed0efac0a10f4c6f42632eeee2b821c53aa055e70d4b56d5409e8eb533dd2ac7b1ff7f735128c76d0b5cc455bf5de19
6
+ metadata.gz: 58cfc22082b4c0098a5c756d915401fe334a3346c55541038b21fcd500978f5646ab7627234b5eb882f00561df2bbc28569de2b3c2976d1708c31857a59036ad
7
+ data.tar.gz: 71de8ff6c61f7894afbafb4a763cef7402651beaac638f0756839a140fdc877c0ba7f6cc0466b5f39883b39460472ddd1531d968a37599b2aaaf772bd5c589ff
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 (2016-01-29)
4
+ ### Fixes
5
+ - Handle new ruby lambda backtrace syntax
6
+ - Don't treat nested errors separately
7
+
3
8
  ## 1.0.3 (2016-01-26)
4
9
  ### Fixes
5
10
  - Fix syntax error
@@ -1,5 +1,5 @@
1
1
  require 'tempfile'
2
- require 'redmine_airbrake_backend/error'
2
+ require 'redmine_airbrake_backend/notice'
3
3
 
4
4
 
5
5
  # Controller with airbrake related stuff
@@ -11,7 +11,6 @@ class AirbrakeController < ::ApplicationController
11
11
  prepend_before_action :load_records
12
12
  prepend_before_action :parse_key
13
13
  prepend_before_action :find_project
14
- before_action :set_environment
15
14
  before_action :authorize
16
15
 
17
16
  after_action :cleanup_tempfiles
@@ -25,14 +24,11 @@ class AirbrakeController < ::ApplicationController
25
24
  end
26
25
 
27
26
  def parse_key
28
- @key = JSON.parse(params[:key]).symbolize_keys #rescue nil
27
+ @key = JSON.parse(params[:key]).symbolize_keys rescue nil
29
28
 
30
29
  # API key
31
30
  invalid_request!('No or invalid API key') if @key.blank? || @key[:key].blank?
32
31
  params[:key] = @key[:key]
33
-
34
- # Type
35
- @type = @key[:type] || (params[:context][:language].split('/', 2).first.downcase rescue nil)
36
32
  end
37
33
 
38
34
  def load_records
@@ -56,10 +52,6 @@ class AirbrakeController < ::ApplicationController
56
52
  @repository = @project.repositories.find_by(identifier: (@key[:repository] || ''))
57
53
  end
58
54
 
59
- def set_environment
60
- @environment ||= params[:context][:environment].presence rescue nil
61
- end
62
-
63
55
  def invalid_request!(message)
64
56
  raise InvalidRequest.new(message)
65
57
  end
@@ -112,39 +104,52 @@ class AirbrakeController < ::ApplicationController
112
104
  custom_field(:occurrences_field)
113
105
  end
114
106
 
107
+ def create_issue!
108
+ return if @notice.blank?
109
+ return if @notice.errors.blank?
110
+
111
+ @issue = find_or_initialize_issue(@notice)
112
+
113
+ set_issue_custom_field_values(@issue, @notice)
114
+
115
+ reopen_issue(@issue, @notice) if @issue.persisted? && @issue.status.is_closed? && reopen_issue?(@notice)
116
+
117
+ @issue = nil unless @issue.save
118
+ end
119
+
115
120
  # Load or initialize issue by project, tracker and airbrake hash
116
- def find_or_initialize_issue(error)
117
- issue_ids = CustomValue.where(customized_type: Issue.name, custom_field_id: notice_hash_field.id, value: error.id).pluck(:customized_id)
121
+ def find_or_initialize_issue(notice)
122
+ issue_ids = CustomValue.where(customized_type: Issue.name, custom_field_id: notice_hash_field.id, value: notice.id).pluck(:customized_id)
118
123
 
119
124
  issue = Issue.find_by(id: issue_ids, project_id: @project.id, tracker_id: @tracker.id)
120
125
 
121
126
  return issue if issue.present?
122
127
 
123
- initialize_issue(error)
128
+ initialize_issue(notice)
124
129
  end
125
130
 
126
- def initialize_issue(error)
131
+ def initialize_issue(notice)
127
132
  issue = Issue.new(
128
- subject: error.subject,
133
+ subject: notice.subject,
129
134
  project: @project,
130
135
  tracker: @tracker,
131
136
  author: User.current,
132
137
  category: @category,
133
138
  priority: @priority,
134
- description: render_description(error),
139
+ description: render_description(notice),
135
140
  assigned_to: @assignee
136
141
  )
137
142
 
138
- add_attachments_to_issue(issue, error)
143
+ add_attachments_to_issue(issue, notice)
139
144
 
140
145
  issue
141
146
  end
142
147
 
143
- def set_issue_custom_field_values(issue, error)
148
+ def set_issue_custom_field_values(issue, notice)
144
149
  custom_field_values = {}
145
150
 
146
151
  # Error ID
147
- custom_field_values[notice_hash_field.id] = error.id if issue.new_record?
152
+ custom_field_values[notice_hash_field.id] = notice.id if issue.new_record?
148
153
 
149
154
  # Update occurrences
150
155
  if occurrences_field.present?
@@ -155,12 +160,12 @@ class AirbrakeController < ::ApplicationController
155
160
  issue.custom_field_values = custom_field_values
156
161
  end
157
162
 
158
- def add_attachments_to_issue(issue, error)
159
- return if error.attachments.blank?
163
+ def add_attachments_to_issue(issue, notice)
164
+ return if notice.attachments.blank?
160
165
 
161
166
  @tempfiles ||= []
162
167
 
163
- error.attachments.each do |data|
168
+ notice.attachments.each do |data|
164
169
  filename = data[:filename].presence || Redmine::Utils.random_hex(16)
165
170
 
166
171
  file = Tempfile.new(filename)
@@ -173,37 +178,37 @@ class AirbrakeController < ::ApplicationController
173
178
  end
174
179
  end
175
180
 
176
- def reopen_issue?
181
+ def reopen_issue?(notice)
177
182
  reopen_regexp = project_setting(:reopen_regexp)
178
183
 
179
184
  return false if reopen_regexp.blank?
180
- return false if @environment.blank?
185
+ return false if notice.environment_name.blank?
181
186
 
182
- !!(@environment =~ /#{reopen_regexp}/i)
187
+ !!(notice.environment_name =~ /#{reopen_regexp}/i)
183
188
  end
184
189
 
185
190
  def issue_reopen_repeat_description?
186
191
  !!project_setting(:reopen_repeat_description)
187
192
  end
188
193
 
189
- def reopen_issue(issue, error)
190
- return if @environment.blank?
194
+ def reopen_issue(issue, notice)
195
+ return if notice.environment_name.blank?
191
196
 
192
- desc = "*Issue reopened after occurring again in _#{@environment}_ environment.*"
193
- desc << "\n\n#{render_description(error)}" if issue_reopen_repeat_description?
197
+ desc = "*Issue reopened after occurring again in _#{notice.environment_name}_ environment.*"
198
+ desc << "\n\n#{render_description(notice)}" if issue_reopen_repeat_description?
194
199
 
195
200
  issue.status = issue.tracker.default_status
196
201
 
197
202
  issue.init_journal(User.current, desc)
198
203
 
199
- add_attachments_to_issue(issue, error)
204
+ add_attachments_to_issue(issue, notice)
200
205
  end
201
206
 
202
- def render_description(error)
203
- locals = { error: error }
207
+ def render_description(notice)
208
+ locals = { notice: notice }
204
209
 
205
- if template_exists?("airbrake/issue_description/#{@type}")
206
- render_to_string("airbrake/issue_description/#{@type}", layout: false, locals: locals)
210
+ if template_exists?("airbrake/issue_description/#{notice.type}")
211
+ render_to_string("airbrake/issue_description/#{notice.type}", layout: false, locals: locals)
207
212
  else
208
213
  render_to_string('airbrake/issue_description/default', layout: false, locals: locals)
209
214
  end
@@ -2,26 +2,24 @@
2
2
  class AirbrakeNoticeController < ::AirbrakeController
3
3
  accept_api_auth :notices
4
4
 
5
+ before_action :parse_notice
6
+
5
7
  # Handle airbrake notices
6
8
  def notices
7
- create_issues
9
+ create_issue!
8
10
 
9
11
  render_airbrake_response
10
12
  end
11
13
 
12
14
  private
13
15
 
14
- def create_issues
15
- params[:errors].each do |e|
16
- error = RedmineAirbrakeBackend::Error.new(e)
17
-
18
- issue = find_or_initialize_issue(error)
19
-
20
- set_issue_custom_field_values(issue, error)
21
-
22
- reopen_issue(issue, error) if issue.persisted? && issue.status.is_closed? && reopen_issue?
23
-
24
- @issue ||= issue if issue.save
25
- end
16
+ def parse_notice
17
+ @notice = RedmineAirbrakeBackend::Notice.new(
18
+ errors: params[:errors].map { |e| RedmineAirbrakeBackend::Error.new(e) },
19
+ params: params[:params],
20
+ session: params[:session],
21
+ context: params[:context],
22
+ environment: params[:environment]
23
+ )
26
24
  end
27
25
  end
@@ -5,24 +5,18 @@ require 'redmine_airbrake_backend/ios_report'
5
5
  class AirbrakeReportController < ::AirbrakeController
6
6
  accept_api_auth :ios_reports
7
7
 
8
+ before_action :parse_report
9
+
8
10
  # Handle airbrake iOS reports
9
11
  def ios_reports
10
- create_issue
12
+ create_issue!
11
13
 
12
14
  render_airbrake_response
13
15
  end
14
16
 
15
17
  private
16
18
 
17
- def create_issue
18
- error = RedmineAirbrakeBackend::IosReport.new(params[:report])
19
-
20
- @issue = find_or_initialize_issue(error)
21
-
22
- set_issue_custom_field_values(@issue, error)
23
-
24
- reopen_issue(@issue, error) if @issue.persisted? && @issue.status.is_closed? && reopen_issue?
25
-
26
- @issue = nil unless @issue.save
19
+ def parse_report
20
+ @notice = RedmineAirbrakeBackend::IosReport.new(params)
27
21
  end
28
22
  end
@@ -1,7 +1,17 @@
1
1
  module AirbrakeHelper
2
+ # Error title
3
+ def airbrake_error_title(error)
4
+ if error.type.blank?
5
+ error.message
6
+ else
7
+ "#{error.type}: #{error.message}"
8
+ end
9
+ end
10
+
2
11
  # Wiki markup for a table
3
- def format_table(data)
12
+ def airbrake_format_table(data)
4
13
  lines = []
14
+
5
15
  data.each do |key, value|
6
16
  next if value.blank?
7
17
 
@@ -11,19 +21,20 @@ module AirbrakeHelper
11
21
  lines << "|@#{key}@|@#{value.map { |k, v| "#{k}: #{v}"}.join(', ')}@|"
12
22
  end
13
23
  end
24
+
14
25
  lines.join("\n")
15
26
  end
16
27
 
17
28
  # Wiki markup for a list item
18
- def format_list_item(name, value)
29
+ def airbrake_format_list_item(name, value)
19
30
  return '' if value.blank?
20
31
 
21
32
  "* *#{name}:* #{value}"
22
33
  end
23
34
 
24
35
  # Wiki markup for backtrace element with link to repository if possible
25
- def format_backtrace_element(element)
26
- repository = repository_for_backtrace_element(element)
36
+ def airbrake_format_backtrace_element(element)
37
+ repository = airbrake_repository_for_backtrace_element(element)
27
38
 
28
39
  if repository.blank?
29
40
  if element.line.blank?
@@ -32,7 +43,7 @@ module AirbrakeHelper
32
43
  markup = "@#{element.file}:#{element.line}@"
33
44
  end
34
45
  else
35
- filename = filename_for_backtrace_element(element)
46
+ filename = airbrake_filename_for_backtrace_element(element)
36
47
 
37
48
  if repository.identifier.blank?
38
49
  markup = "source:\"#{filename}#L#{element.line}\""
@@ -46,15 +57,15 @@ module AirbrakeHelper
46
57
 
47
58
  private
48
59
 
49
- def repository_for_backtrace_element(element)
60
+ def airbrake_repository_for_backtrace_element(element)
50
61
  return nil unless element.file.start_with?('[PROJECT_ROOT]')
51
62
 
52
- filename = filename_for_backtrace_element(element)
63
+ filename = airbrake_filename_for_backtrace_element(element)
53
64
 
54
- repositories_for_backtrace.find { |r| r.entry(filename) }
65
+ airbrake_repositories_for_backtrace.find { |r| r.entry(filename) }
55
66
  end
56
67
 
57
- def repositories_for_backtrace
68
+ def airbrake_repositories_for_backtrace
58
69
  return @_bactrace_repositories unless @_bactrace_repositories.nil?
59
70
 
60
71
  if @repository.present?
@@ -66,7 +77,7 @@ module AirbrakeHelper
66
77
  @_bactrace_repositories
67
78
  end
68
79
 
69
- def filename_for_backtrace_element(element)
80
+ def airbrake_filename_for_backtrace_element(element)
70
81
  return nil if element.file.blank?
71
82
 
72
83
  element.file[14..-1]
@@ -1,44 +1,45 @@
1
- h1. <%= error.message %>
2
-
1
+ <% notice.errors.each do |error| %>
2
+ h1. <%= airbrake_error_title(error) %>
3
3
 
4
4
  <% if error.backtrace.present? %>
5
5
  h2. Stacktrace:
6
6
 
7
- p((. <%= raw error.backtrace.map { |e| format_backtrace_element(e) }.join("\n") %>
7
+ p((. <%= raw error.backtrace.map { |e| airbrake_format_backtrace_element(e) }.join("\n") %>
8
8
  <% end %>
9
9
 
10
+ <% end %>
10
11
 
11
- <% if error.application.present? %>
12
+ <% if notice.application.present? %>
12
13
  h2. Application:
13
14
 
14
- <%= format_list_item('Name', error.application[:name]) %>
15
- <%= format_list_item('Version', error.application[:version]) %>
15
+ <%= airbrake_format_list_item('Name', notice.application[:name]) %>
16
+ <%= airbrake_format_list_item('Version', notice.application[:version]) %>
16
17
  <% end %>
17
18
 
18
19
 
19
- <% if params[:params].present? %>
20
+ <% if notice.params.present? %>
20
21
  h2. Parameters:
21
22
 
22
- <%= format_table(params[:params]) %>
23
+ <%= airbrake_format_table(notice.params) %>
23
24
  <% end %>
24
25
 
25
26
 
26
- <% if params[:session].present? %>
27
+ <% if notice.session.present? %>
27
28
  h2. Session
28
29
 
29
- <%= format_table(params[:session]) %>
30
+ <%= airbrake_format_table(notice.session) %>
30
31
  <% end %>
31
32
 
32
33
 
33
- <% if params[:context].present? %>
34
+ <% if notice.context.present? %>
34
35
  h2. Context
35
36
 
36
- <%= format_table(params[:context].reject { |k, v| ['notifier'].include?(k) }) %>
37
+ <%= airbrake_format_table(notice.context) %>
37
38
  <% end %>
38
39
 
39
40
 
40
- <% if params[:environment].present? %>
41
+ <% if notice.environment.present? %>
41
42
  h2. Environment
42
43
 
43
- <%= format_table(params[:environment]) %>
44
+ <%= airbrake_format_table(notice.environment) %>
44
45
  <% end %>
@@ -29,7 +29,7 @@ module RedmineAirbrakeBackend
29
29
  def normalize_function_name(function_name)
30
30
  name = @function
31
31
  .downcase
32
- .gsub(/_\d+_/, '') # ruby blocks
32
+ .gsub(/([\d_]+$)?/, '') # ruby blocks
33
33
 
34
34
  RedmineAirbrakeBackend.filter_hex_values(name)
35
35
  end
@@ -5,30 +5,20 @@ require 'redmine_airbrake_backend/backtrace_element'
5
5
  module RedmineAirbrakeBackend
6
6
  # Error received by airbrake
7
7
  class Error
8
- attr_reader :type, :message, :backtrace
9
- attr_reader :id, :subject, :application, :attachments
8
+ attr_reader :id, :type, :message, :backtrace
10
9
 
11
- def initialize(data)
10
+ def initialize(options)
12
11
  # Type
13
- @type = data[:type]
12
+ @type = options[:type]
14
13
 
15
14
  # Message
16
- @message = data[:message]
15
+ @message = options[:message]
17
16
 
18
17
  # Backtrace
19
- @backtrace = data[:backtrace].map { |b| BacktraceElement.new(b) }
18
+ @backtrace = options[:backtrace].map { |b| BacktraceElement.new(b) }
20
19
 
21
20
  # Error ID
22
21
  @id = generate_id
23
-
24
- # Subject
25
- @subject = generate_subject
26
-
27
- # Attachments
28
- @attachments = (data[:attachments].presence || []).compact
29
-
30
- # Application
31
- @application = data[:application].presence
32
22
  end
33
23
 
34
24
  private
@@ -41,17 +31,5 @@ module RedmineAirbrakeBackend
41
31
 
42
32
  Digest::MD5.hexdigest(h.compact.join("\n"))
43
33
  end
44
-
45
- def generate_subject
46
- s = ''
47
-
48
- if @type.blank? || @message.starts_with?("#{@type}:")
49
- s = "[#{@id[0..7]}] #{@message}"
50
- else
51
- s = "[#{@id[0..7]}] #{@type}: #{@message}"
52
- end
53
-
54
- s[0..254].strip
55
- end
56
34
  end
57
35
  end
@@ -1,21 +1,26 @@
1
- require 'redmine_airbrake_backend/error'
1
+ require 'redmine_airbrake_backend/notice'
2
2
 
3
3
 
4
4
  module RedmineAirbrakeBackend
5
5
  # iOS Report received by airbrake
6
- class IosReport < Error
7
- def initialize(data)
8
- super(IosReport.parse(data))
6
+ class IosReport < Notice
7
+ def initialize(options)
8
+ error, application, attachments = self.class.parse(options[:report])
9
+
10
+ super({
11
+ errors: [Error.new(error)],
12
+ context: options[:context],
13
+ application: application,
14
+ attachments: attachments
15
+ })
9
16
  end
10
17
 
11
18
  private
12
19
 
13
20
  def self.parse(data)
14
- error = {
15
- application: {},
16
- backtrace: [],
17
- attachments: [],
18
- }
21
+ error = { backtrace: [] }
22
+ application = {}
23
+ attachments = []
19
24
 
20
25
  header_finished = false
21
26
  next_line_is_message = false
@@ -25,8 +30,8 @@ module RedmineAirbrakeBackend
25
30
  data.split("\n").each do |line|
26
31
  header_finished = true if line =~ /^(Application Specific Information|Last Exception Backtrace|Thread \d+( Crashed)?):$/
27
32
 
28
- unless header_finishedii
29
- ii = parse_header_line(line, error)
33
+ unless header_finished
34
+ ii = parse_header_line(line, error, application)
30
35
  indicent_identifier ||= ii if ii
31
36
  end
32
37
 
@@ -38,7 +43,10 @@ module RedmineAirbrakeBackend
38
43
 
39
44
  crashed_thread = false if line =~ /^Thread \d+:$/
40
45
 
41
- error[:backtrace] << parse_backtrace_element(line) if crashed_thread
46
+ if crashed_thread
47
+ backtrace = parse_backtrace_element(line)
48
+ error[:backtrace] << backtrace if backtrace
49
+ end
42
50
 
43
51
  crashed_thread = true if error[:backtrace].compact.blank? && line =~ /^(Last Exception Backtrace|Thread \d+ Crashed):$/
44
52
 
@@ -47,15 +55,15 @@ module RedmineAirbrakeBackend
47
55
 
48
56
  return nil if error.blank?
49
57
 
50
- error[:attachments] << {
58
+ attachments << {
51
59
  filename: "#{indicent_identifier}.crash",
52
60
  data: data
53
61
  } if indicent_identifier.present?
54
62
 
55
- error
63
+ [error, application, attachments]
56
64
  end
57
65
 
58
- def self.parse_header_line(line, error)
66
+ def self.parse_header_line(line, error, application)
59
67
  key, value = line.split(':', 2).map { |s| s.strip }
60
68
 
61
69
  return nil if key.blank? || value.blank?
@@ -68,9 +76,9 @@ module RedmineAirbrakeBackend
68
76
  when 'Incident Identifier'
69
77
  return value
70
78
  when 'Identifier'
71
- error[:application][:name] = value
79
+ application[:name] = value
72
80
  when 'Version'
73
- error[:application][:version] = value
81
+ application[:version] = value
74
82
  end
75
83
 
76
84
  nil
@@ -0,0 +1,64 @@
1
+ require 'redmine_airbrake_backend/error'
2
+
3
+
4
+ module RedmineAirbrakeBackend
5
+ # Notice received by airbrake
6
+ class Notice
7
+ attr_reader :id, :subject, :type, :environment_name
8
+ attr_reader :errors, :params, :session, :context, :environment, :application, :attachments
9
+
10
+ def initialize(options)
11
+ # Errors
12
+ @errors = options[:errors].compact
13
+
14
+ # Params
15
+ @params = options[:params]
16
+
17
+ # Session
18
+ @session = options[:session]
19
+
20
+ # Context
21
+ @context = options[:context].reject { |k, v| ['notifier'].include?(k) }
22
+
23
+ # Environment
24
+ @environment = options[:environment]
25
+
26
+ # Application
27
+ @application = options[:application]
28
+
29
+ # Attachments
30
+ @attachments = (options[:attachments].presence || []).compact
31
+
32
+ # Environment name
33
+ @environment_name = options[:context][:environment].presence rescue nil
34
+
35
+ # Type
36
+ @type = options[:type] || (options[:context][:language].split('/', 2).first.downcase rescue nil)
37
+
38
+ # Error ID
39
+ @id = generate_id
40
+
41
+ # Subject
42
+ @subject = generate_subject
43
+ end
44
+
45
+ private
46
+
47
+ def generate_id
48
+ Digest::MD5.hexdigest(@errors.map(&:id).join("\n"))
49
+ end
50
+
51
+ def generate_subject
52
+ error = @errors.first
53
+ s = ''
54
+
55
+ if error.type.blank? || error.message.starts_with?("#{error.type}:")
56
+ s = "[#{@id[0..7]}] #{error.message}"
57
+ else
58
+ s = "[#{@id[0..7]}] #{error.type}: #{error.message}"
59
+ end
60
+
61
+ s[0..254].strip
62
+ end
63
+ end
64
+ end
@@ -1,4 +1,4 @@
1
1
  module RedmineAirbrakeBackend
2
2
  # Version of this gem
3
- VERSION = '1.0.3'
3
+ VERSION = '1.1.0'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redmine_airbrake_backend
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Schwab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-26 00:00:00.000000000 Z
11
+ date: 2016-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -85,6 +85,7 @@ files:
85
85
  - lib/redmine_airbrake_backend/engine.rb
86
86
  - lib/redmine_airbrake_backend/error.rb
87
87
  - lib/redmine_airbrake_backend/ios_report.rb
88
+ - lib/redmine_airbrake_backend/notice.rb
88
89
  - lib/redmine_airbrake_backend/patches/issue_category.rb
89
90
  - lib/redmine_airbrake_backend/patches/issue_priority.rb
90
91
  - lib/redmine_airbrake_backend/patches/project.rb