redmine_airbrake_backend 1.2.1 → 1.2.2
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/.gitlab-ci.yml +42 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +3 -1
- data/README.md +0 -4
- data/Rakefile +2 -1
- data/app/controllers/airbrake_controller.rb +32 -137
- data/app/controllers/airbrake_notice_controller.rb +8 -6
- data/app/controllers/airbrake_project_settings_controller.rb +6 -4
- data/app/controllers/airbrake_report_controller.rb +2 -0
- data/app/controllers/concerns/airbrake_attachments.rb +56 -0
- data/app/controllers/concerns/airbrake_issue_handling.rb +109 -0
- data/app/controllers/concerns/airbrake_rendering.rb +16 -0
- data/app/helpers/airbrake_helper.rb +35 -23
- data/app/models/airbrake_project_setting.rb +2 -0
- data/bin/rails +3 -3
- data/config/routes.rb +2 -0
- data/db/migrate/20130708102357_create_airbrake_project_settings.rb +3 -0
- data/db/migrate/20131003151228_add_reopen_repeat_description.rb +3 -0
- data/lib/redmine_airbrake_backend.rb +3 -0
- data/lib/redmine_airbrake_backend/backtrace_element.rb +6 -4
- data/lib/redmine_airbrake_backend/engine.rb +27 -25
- data/lib/redmine_airbrake_backend/error.rb +2 -0
- data/lib/redmine_airbrake_backend/ios_report.rb +33 -28
- data/lib/redmine_airbrake_backend/notice.rb +43 -27
- data/lib/redmine_airbrake_backend/patches/issue_category.rb +10 -5
- data/lib/redmine_airbrake_backend/patches/issue_priority.rb +10 -5
- data/lib/redmine_airbrake_backend/patches/project.rb +10 -5
- data/lib/redmine_airbrake_backend/patches/projects_helper.rb +16 -11
- data/lib/redmine_airbrake_backend/patches/tracker.rb +10 -5
- data/lib/redmine_airbrake_backend/version.rb +3 -1
- data/lib/tasks/test.rake +11 -9
- data/redmine_airbrake_backend.gemspec +10 -6
- data/test/unit/notice_test.rb +1 -1
- metadata +24 -5
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
# Handling for creating / updating issues
|
6
|
+
module AirbrakeIssueHandling
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include AirbrakeAttachments
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def reopen_issue_if_qualifies(issue, notice)
|
13
|
+
return unless issue.persisted?
|
14
|
+
return unless issue.status.is_closed?
|
15
|
+
return unless reopen_issue?(notice)
|
16
|
+
|
17
|
+
reopen_issue(issue, notice)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_issue!
|
21
|
+
return if @notice.blank?
|
22
|
+
return if @notice.errors.blank?
|
23
|
+
|
24
|
+
@issue = find_or_initialize_issue(@notice)
|
25
|
+
|
26
|
+
set_issue_custom_field_values(@issue, @notice)
|
27
|
+
|
28
|
+
reopen_issue_if_qualifies(@issue, @notice)
|
29
|
+
|
30
|
+
@issue = nil unless @issue.save
|
31
|
+
end
|
32
|
+
|
33
|
+
# Load or initialize issue by project, tracker and airbrake hash
|
34
|
+
def find_or_initialize_issue(notice)
|
35
|
+
issue_ids = CustomValue
|
36
|
+
.where(customized_type: Issue.name, custom_field_id: notice_hash_field.id, value: notice.id)
|
37
|
+
.pluck(:customized_id)
|
38
|
+
|
39
|
+
issue = Issue.find_by(id: issue_ids, project_id: @project.id, tracker_id: @tracker.id)
|
40
|
+
|
41
|
+
return issue if issue.present?
|
42
|
+
|
43
|
+
initialize_issue(notice)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize_issue(notice)
|
47
|
+
issue = Issue.new(
|
48
|
+
subject: notice.subject,
|
49
|
+
project: @project,
|
50
|
+
tracker: @tracker,
|
51
|
+
author: User.current,
|
52
|
+
category: @category,
|
53
|
+
priority: @priority,
|
54
|
+
description: render_description(notice),
|
55
|
+
assigned_to: @assignee
|
56
|
+
)
|
57
|
+
|
58
|
+
add_attachments_to_issue(issue, notice)
|
59
|
+
|
60
|
+
issue
|
61
|
+
end
|
62
|
+
|
63
|
+
def update_occurrences(issue, custom_field_values)
|
64
|
+
return if occurrences_field.blank?
|
65
|
+
|
66
|
+
current_value = issue.custom_value_for(occurrences_field.id)
|
67
|
+
current_value = current_value ? current_value.value.to_i : 0
|
68
|
+
|
69
|
+
custom_field_values[occurrences_field.id] = (current_value + 1).to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_issue_custom_field_values(issue, notice)
|
73
|
+
custom_field_values = {}
|
74
|
+
|
75
|
+
# Error ID
|
76
|
+
custom_field_values[notice_hash_field.id] = notice.id if issue.new_record?
|
77
|
+
|
78
|
+
# Update occurrences
|
79
|
+
update_occurrences(issue, custom_field_values)
|
80
|
+
|
81
|
+
issue.custom_field_values = custom_field_values
|
82
|
+
end
|
83
|
+
|
84
|
+
def reopen_issue?(notice)
|
85
|
+
reopen_regexp = project_setting(:reopen_regexp)
|
86
|
+
|
87
|
+
return false if reopen_regexp.blank?
|
88
|
+
return false if notice.environment_name.blank?
|
89
|
+
|
90
|
+
notice.environment_name =~ /#{reopen_regexp}/i
|
91
|
+
end
|
92
|
+
|
93
|
+
def issue_reopen_repeat_description?
|
94
|
+
project_setting(:reopen_repeat_description)
|
95
|
+
end
|
96
|
+
|
97
|
+
def reopen_issue(issue, notice)
|
98
|
+
return if notice.environment_name.blank?
|
99
|
+
|
100
|
+
desc = "*Issue reopened after occurring again in _#{notice.environment_name}_ environment.*"
|
101
|
+
desc << "\n\n#{render_description(notice)}" if issue_reopen_repeat_description?
|
102
|
+
|
103
|
+
issue.status = issue.tracker.default_status
|
104
|
+
|
105
|
+
issue.init_journal(User.current, desc)
|
106
|
+
|
107
|
+
add_attachments_to_issue(issue, notice)
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Rendering of issue description
|
4
|
+
module AirbrakeRendering
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def render_description(notice)
|
8
|
+
locals = { notice: notice }
|
9
|
+
|
10
|
+
if template_exists?("airbrake/issue_description/#{notice.type}")
|
11
|
+
render_to_string("airbrake/issue_description/#{notice.type}", layout: false, locals: locals)
|
12
|
+
else
|
13
|
+
render_to_string('airbrake/issue_description/default', layout: false, locals: locals)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Helper methods for rendering airbrake errors
|
1
4
|
module AirbrakeHelper
|
2
5
|
# Error title
|
3
6
|
def airbrake_error_title(error)
|
@@ -36,21 +39,11 @@ module AirbrakeHelper
|
|
36
39
|
def airbrake_format_backtrace_element(element)
|
37
40
|
repository = airbrake_repository_for_backtrace_element(element)
|
38
41
|
|
39
|
-
if repository.blank?
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
45
|
-
else
|
46
|
-
filename = airbrake_filename_for_backtrace_element(element)
|
47
|
-
|
48
|
-
if repository.identifier.blank?
|
49
|
-
markup = "source:\"#{filename}#L#{element.line}\""
|
50
|
-
else
|
51
|
-
markup = "source:\"#{repository.identifier}|#{filename}#L#{element.line}\""
|
52
|
-
end
|
53
|
-
end
|
42
|
+
markup = if repository.blank?
|
43
|
+
airbrake_backtrace_element_markup_without_repository(element)
|
44
|
+
else
|
45
|
+
airbrake_backtrace_element_markup_with_repository(repository, element)
|
46
|
+
end
|
54
47
|
|
55
48
|
markup + " in ??<notextile>#{element.function}</notextile>??"
|
56
49
|
end
|
@@ -64,6 +57,7 @@ module AirbrakeHelper
|
|
64
57
|
private
|
65
58
|
|
66
59
|
def airbrake_repository_for_backtrace_element(element)
|
60
|
+
return nil unless element.file.present?
|
67
61
|
return nil unless element.file.start_with?('[PROJECT_ROOT]')
|
68
62
|
|
69
63
|
filename = airbrake_filename_for_backtrace_element(element)
|
@@ -72,20 +66,38 @@ module AirbrakeHelper
|
|
72
66
|
end
|
73
67
|
|
74
68
|
def airbrake_repositories_for_backtrace
|
75
|
-
return @
|
69
|
+
return @bactrace_repositories unless @bactrace_repositories.nil?
|
76
70
|
|
77
|
-
if @repository.present?
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
71
|
+
@bactrace_repositories = if @repository.present?
|
72
|
+
[@repository]
|
73
|
+
else
|
74
|
+
@project.repositories.to_a
|
75
|
+
end
|
82
76
|
|
83
|
-
@
|
77
|
+
@bactrace_repositories
|
84
78
|
end
|
85
79
|
|
86
80
|
def airbrake_filename_for_backtrace_element(element)
|
87
|
-
return nil if
|
81
|
+
return nil if element.file.blank?
|
88
82
|
|
89
83
|
element.file[14..-1]
|
90
84
|
end
|
85
|
+
|
86
|
+
def airbrake_backtrace_element_markup_without_repository(element)
|
87
|
+
if element.line.blank?
|
88
|
+
"@#{element.file}@"
|
89
|
+
else
|
90
|
+
"@#{element.file}:#{element.line}@"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def airbrake_backtrace_element_markup_with_repository(repository, element)
|
95
|
+
filename = airbrake_filename_for_backtrace_element(element)
|
96
|
+
|
97
|
+
if repository.identifier.blank?
|
98
|
+
"source:\"#{filename}#L#{element.line}\""
|
99
|
+
else
|
100
|
+
"source:\"#{repository.identifier}|#{filename}#L#{element.line}\""
|
101
|
+
end
|
102
|
+
end
|
91
103
|
end
|
data/bin/rails
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
|
-
ENGINE_ROOT = File.expand_path('
|
5
|
-
ENGINE_PATH = File.expand_path('
|
4
|
+
ENGINE_ROOT = File.expand_path('..', __dir__)
|
5
|
+
ENGINE_PATH = File.expand_path('../lib/redmine_airbrake_backend/engine', __dir__)
|
6
6
|
|
7
7
|
require 'rails/all'
|
8
8
|
require 'rails/engine/commands'
|
data/config/routes.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'digest/md5'
|
2
4
|
|
3
5
|
module RedmineAirbrakeBackend
|
@@ -20,15 +22,15 @@ module RedmineAirbrakeBackend
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def checksum
|
23
|
-
@
|
25
|
+
@checksum ||= Digest::MD5.hexdigest("#{@file}|#{normalized_function_name}|#{@line}|#{@column}")
|
24
26
|
end
|
25
27
|
|
26
28
|
private
|
27
29
|
|
28
|
-
def
|
30
|
+
def normalized_function_name
|
29
31
|
name = @function
|
30
|
-
|
31
|
-
|
32
|
+
.downcase
|
33
|
+
.gsub(/([\d_]+$)?/, '') # ruby blocks
|
32
34
|
|
33
35
|
RedmineAirbrakeBackend.filter_hex_values(name)
|
34
36
|
end
|
@@ -1,51 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'redmine_airbrake_backend/version'
|
2
4
|
|
3
5
|
module RedmineAirbrakeBackend
|
6
|
+
# Engine for Airbrake integration into Redmine
|
4
7
|
class Engine < ::Rails::Engine
|
5
|
-
initializer '
|
6
|
-
register_redmine_plugin
|
8
|
+
initializer 'airbrake_backend.register_redmine_plugin', after: 'load_config_initializers' do |_|
|
9
|
+
RedmineAirbrakeBackend::Engine.register_redmine_plugin
|
7
10
|
end
|
8
11
|
|
9
|
-
initializer '
|
12
|
+
initializer 'airbrake_backend.apply_patches', after: 'airbrake_backend.register_redmine_plugin' do |_|
|
10
13
|
ActionDispatch::Callbacks.to_prepare do
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
RedmineAirbrakeBackend::Engine.apply_patch(Project, RedmineAirbrakeBackend::Patches::Project)
|
18
|
-
RedmineAirbrakeBackend::Engine.apply_patch(Tracker, RedmineAirbrakeBackend::Patches::Tracker)
|
19
|
-
RedmineAirbrakeBackend::Engine.apply_patch(IssueCategory, RedmineAirbrakeBackend::Patches::IssueCategory)
|
20
|
-
RedmineAirbrakeBackend::Engine.apply_patch(IssuePriority, RedmineAirbrakeBackend::Patches::IssuePriority)
|
21
|
-
RedmineAirbrakeBackend::Engine.apply_patch(ProjectsHelper, RedmineAirbrakeBackend::Patches::ProjectsHelper)
|
14
|
+
RedmineAirbrakeBackend::Engine.add_redmine_patch('Project')
|
15
|
+
RedmineAirbrakeBackend::Engine.add_redmine_patch('Tracker')
|
16
|
+
RedmineAirbrakeBackend::Engine.add_redmine_patch('IssueCategory')
|
17
|
+
RedmineAirbrakeBackend::Engine.add_redmine_patch('IssuePriority')
|
18
|
+
RedmineAirbrakeBackend::Engine.add_redmine_patch('ProjectsHelper')
|
22
19
|
end
|
23
20
|
end
|
24
21
|
|
25
|
-
def apply_patch(clazz, patch)
|
26
|
-
clazz.send(:include, patch) unless clazz.included_modules.include?(patch)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
22
|
def register_redmine_plugin
|
32
23
|
Redmine::Plugin.register :redmine_airbrake_backend do
|
33
24
|
name 'Airbrake Backend'
|
34
25
|
author 'Florian Schwab'
|
35
|
-
author_url 'https://
|
26
|
+
author_url 'https://ydkn.de'
|
36
27
|
description 'Airbrake Backend for Redmine'
|
37
|
-
url 'https://
|
28
|
+
url 'https://gitlab.com/ydkn/redmine_airbrake_backend'
|
38
29
|
version ::RedmineAirbrakeBackend::VERSION
|
39
|
-
requires_redmine version_or_higher: '3.
|
30
|
+
requires_redmine version_or_higher: '3.4.0'
|
40
31
|
directory RedmineAirbrakeBackend.directory
|
41
32
|
|
42
33
|
project_module :airbrake do
|
43
|
-
permission :airbrake,
|
44
|
-
permission :manage_airbrake,
|
34
|
+
permission :airbrake, airbrake_notice: [:notices], airbrake_report: [:ios_reports]
|
35
|
+
permission :manage_airbrake, airbrake: [:update]
|
45
36
|
end
|
46
37
|
|
47
38
|
settings default: { hash_field: '', occurrences_field: '' }, partial: 'settings/airbrake'
|
48
39
|
end
|
49
40
|
end
|
41
|
+
|
42
|
+
def add_redmine_patch(clazz_name)
|
43
|
+
require_dependency "redmine_airbrake_backend/patches/#{clazz_name.underscore}"
|
44
|
+
|
45
|
+
clazz = "::#{clazz_name}".constantize
|
46
|
+
patch_clazz = "::RedmineAirbrakeBackend::Patches::#{clazz_name}".constantize
|
47
|
+
|
48
|
+
return if clazz.included_modules.include?(patch_clazz)
|
49
|
+
|
50
|
+
clazz.send(:include, patch_clazz)
|
51
|
+
end
|
50
52
|
end
|
51
53
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'redmine_airbrake_backend/notice'
|
2
4
|
|
3
5
|
module RedmineAirbrakeBackend
|
@@ -6,16 +8,15 @@ module RedmineAirbrakeBackend
|
|
6
8
|
def initialize(options)
|
7
9
|
error, application, attachments = self.class.parse(options[:report])
|
8
10
|
|
9
|
-
super(
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
super(
|
12
|
+
errors: [Error.new(error)],
|
13
|
+
context: options[:context],
|
14
|
+
application: application,
|
15
|
+
attachments: attachments
|
16
|
+
)
|
15
17
|
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
+
# rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
19
20
|
def self.parse(data)
|
20
21
|
error = { backtrace: [] }
|
21
22
|
application = {}
|
@@ -27,7 +28,9 @@ module RedmineAirbrakeBackend
|
|
27
28
|
indicent_identifier = nil
|
28
29
|
|
29
30
|
data.split("\n").each do |line|
|
30
|
-
|
31
|
+
if line.match?(/^(Application Specific Information|Last Exception Backtrace|Thread \d+( Crashed)?):$/)
|
32
|
+
header_finished = true
|
33
|
+
end
|
31
34
|
|
32
35
|
unless header_finished
|
33
36
|
ii = parse_header_line(line, error, application)
|
@@ -47,26 +50,22 @@ module RedmineAirbrakeBackend
|
|
47
50
|
error[:backtrace] << backtrace if backtrace
|
48
51
|
end
|
49
52
|
|
50
|
-
|
53
|
+
if error[:backtrace].compact.blank? && line.match?(/^(Last Exception Backtrace|Thread \d+ Crashed):$/)
|
54
|
+
crashed_thread = true
|
55
|
+
end
|
51
56
|
|
52
57
|
next_line_is_message = true if line =~ /^Application Specific Information:$/
|
53
58
|
end
|
54
59
|
|
55
60
|
return nil if error.blank?
|
56
61
|
|
57
|
-
attachments << {
|
58
|
-
filename: "#{indicent_identifier}.crash",
|
59
|
-
data: data
|
60
|
-
} if indicent_identifier.present?
|
62
|
+
attachments << { filename: "#{indicent_identifier}.crash", data: data } if indicent_identifier.present?
|
61
63
|
|
62
64
|
[error, application, attachments]
|
63
65
|
end
|
66
|
+
# rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength
|
64
67
|
|
65
|
-
def self.
|
66
|
-
key, value = line.split(':', 2).map { |s| s.strip }
|
67
|
-
|
68
|
-
return nil if key.blank? || value.blank?
|
69
|
-
|
68
|
+
def self.handle_header(key, value, error, application)
|
70
69
|
case key
|
71
70
|
when 'Exception Type'
|
72
71
|
error[:type] = value
|
@@ -83,6 +82,14 @@ module RedmineAirbrakeBackend
|
|
83
82
|
nil
|
84
83
|
end
|
85
84
|
|
85
|
+
def self.parse_header_line(line, error, application)
|
86
|
+
key, value = line.split(':', 2).map(&:strip)
|
87
|
+
|
88
|
+
return nil if key.blank? || value.blank?
|
89
|
+
|
90
|
+
handle_header(key, value, error, application)
|
91
|
+
end
|
92
|
+
|
86
93
|
def self.parse_message(line, error)
|
87
94
|
error[:message] = line
|
88
95
|
|
@@ -95,15 +102,13 @@ module RedmineAirbrakeBackend
|
|
95
102
|
end
|
96
103
|
|
97
104
|
def self.parse_backtrace_element(line)
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
nil
|
106
|
-
end
|
105
|
+
return nil unless line =~ /^(\d+)\s+([^\s]+)\s+(0x[0-9a-f]+)\s+(.+) \+ (\d+)$/
|
106
|
+
|
107
|
+
{
|
108
|
+
file: Regexp.last_match(2),
|
109
|
+
function: Regexp.last_match(4),
|
110
|
+
line: Regexp.last_match(5)
|
111
|
+
}
|
107
112
|
end
|
108
113
|
end
|
109
114
|
end
|