redmine_airbrake_backend 1.2.1 → 1.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|