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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +42 -0
  3. data/.rubocop.yml +13 -0
  4. data/CHANGELOG.md +8 -0
  5. data/Gemfile +3 -1
  6. data/README.md +0 -4
  7. data/Rakefile +2 -1
  8. data/app/controllers/airbrake_controller.rb +32 -137
  9. data/app/controllers/airbrake_notice_controller.rb +8 -6
  10. data/app/controllers/airbrake_project_settings_controller.rb +6 -4
  11. data/app/controllers/airbrake_report_controller.rb +2 -0
  12. data/app/controllers/concerns/airbrake_attachments.rb +56 -0
  13. data/app/controllers/concerns/airbrake_issue_handling.rb +109 -0
  14. data/app/controllers/concerns/airbrake_rendering.rb +16 -0
  15. data/app/helpers/airbrake_helper.rb +35 -23
  16. data/app/models/airbrake_project_setting.rb +2 -0
  17. data/bin/rails +3 -3
  18. data/config/routes.rb +2 -0
  19. data/db/migrate/20130708102357_create_airbrake_project_settings.rb +3 -0
  20. data/db/migrate/20131003151228_add_reopen_repeat_description.rb +3 -0
  21. data/lib/redmine_airbrake_backend.rb +3 -0
  22. data/lib/redmine_airbrake_backend/backtrace_element.rb +6 -4
  23. data/lib/redmine_airbrake_backend/engine.rb +27 -25
  24. data/lib/redmine_airbrake_backend/error.rb +2 -0
  25. data/lib/redmine_airbrake_backend/ios_report.rb +33 -28
  26. data/lib/redmine_airbrake_backend/notice.rb +43 -27
  27. data/lib/redmine_airbrake_backend/patches/issue_category.rb +10 -5
  28. data/lib/redmine_airbrake_backend/patches/issue_priority.rb +10 -5
  29. data/lib/redmine_airbrake_backend/patches/project.rb +10 -5
  30. data/lib/redmine_airbrake_backend/patches/projects_helper.rb +16 -11
  31. data/lib/redmine_airbrake_backend/patches/tracker.rb +10 -5
  32. data/lib/redmine_airbrake_backend/version.rb +3 -1
  33. data/lib/tasks/test.rake +11 -9
  34. data/redmine_airbrake_backend.gemspec +10 -6
  35. data/test/unit/notice_test.rb +1 -1
  36. 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
- if element.line.blank?
41
- markup = "@#{element.file}@"
42
- else
43
- markup = "@#{element.file}:#{element.line}@"
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 @_bactrace_repositories unless @_bactrace_repositories.nil?
69
+ return @bactrace_repositories unless @bactrace_repositories.nil?
76
70
 
77
- if @repository.present?
78
- @_bactrace_repositories = [@repository]
79
- else
80
- @_bactrace_repositories = @project.repositories.to_a
81
- end
71
+ @bactrace_repositories = if @repository.present?
72
+ [@repository]
73
+ else
74
+ @project.repositories.to_a
75
+ end
82
76
 
83
- @_bactrace_repositories
77
+ @bactrace_repositories
84
78
  end
85
79
 
86
80
  def airbrake_filename_for_backtrace_element(element)
87
- return nil if element.file.blank?
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Project-specific settings for airbrake
2
4
  class AirbrakeProjectSetting < ActiveRecord::Base
3
5
  belongs_to :project
data/bin/rails CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
- # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
2
+ # frozen_string_literal: true
3
3
 
4
- ENGINE_ROOT = File.expand_path('../..', __FILE__)
5
- ENGINE_PATH = File.expand_path('../../lib/redmine_airbrake_backend/engine', __FILE__)
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
  Rails.application.routes.draw do
2
4
  # api
3
5
  post '/api/v3/projects/:project_id/notices' => 'airbrake_notice#notices'
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create airbrake project settings
1
4
  class CreateAirbrakeProjectSettings < ActiveRecord::Migration
2
5
  def up
3
6
  create_table :airbrake_project_settings do |t|
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add repeat description configuration to Airbrake project settings
1
4
  class AddReopenRepeatDescription < ActiveRecord::Migration
2
5
  def up
3
6
  add_column :airbrake_project_settings, :reopen_repeat_description, :boolean
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redmine_airbrake_backend/engine'
2
4
 
5
+ # Airbrake backend for Redmine
3
6
  module RedmineAirbrakeBackend
4
7
  def self.directory
5
8
  File.expand_path(File.join(File.dirname(__FILE__), '..'))
@@ -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
- @_checksum ||= Digest::MD5.hexdigest("#{@file}|#{normalize_function_name(@function)}|#{@line}|#{@column}")
25
+ @checksum ||= Digest::MD5.hexdigest("#{@file}|#{normalized_function_name}|#{@line}|#{@column}")
24
26
  end
25
27
 
26
28
  private
27
29
 
28
- def normalize_function_name(function_name)
30
+ def normalized_function_name
29
31
  name = @function
30
- .downcase
31
- .gsub(/([\d_]+$)?/, '') # ruby blocks
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 'redmine_airbrake_backend.register_redmine_plugin', after: 'load_config_initializers' do |app|
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 'redmine_airbrake_backend.apply_patches', after: 'redmine_airbrake_backend.register_redmine_plugin' do |app|
12
+ initializer 'airbrake_backend.apply_patches', after: 'airbrake_backend.register_redmine_plugin' do |_|
10
13
  ActionDispatch::Callbacks.to_prepare do
11
- require_dependency 'redmine_airbrake_backend/patches/project' unless defined?(RedmineAirbrakeBackend::Patches::Project)
12
- require_dependency 'redmine_airbrake_backend/patches/tracker' unless defined?(RedmineAirbrakeBackend::Patches::Tracker)
13
- require_dependency 'redmine_airbrake_backend/patches/issue_category' unless defined?(RedmineAirbrakeBackend::Patches::IssueCategory)
14
- require_dependency 'redmine_airbrake_backend/patches/issue_priority' unless defined?(RedmineAirbrakeBackend::Patches::IssuePriority)
15
- require_dependency 'redmine_airbrake_backend/patches/projects_helper' unless defined?(RedmineAirbrakeBackend::Patches::ProjectsHelper)
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://github.com/ydkn'
26
+ author_url 'https://ydkn.de'
36
27
  description 'Airbrake Backend for Redmine'
37
- url 'https://github.com/ydkn/redmine_airbrake_backend'
28
+ url 'https://gitlab.com/ydkn/redmine_airbrake_backend'
38
29
  version ::RedmineAirbrakeBackend::VERSION
39
- requires_redmine version_or_higher: '3.2.0'
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, { airbrake_notice: [:notices], airbrake_report: [:ios_reports] }
44
- permission :manage_airbrake, { airbrake: [:update] }
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 'digest/md5'
2
4
  require 'redmine_airbrake_backend/backtrace_element'
3
5
 
@@ -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
- errors: [Error.new(error)],
11
- context: options[:context],
12
- application: application,
13
- attachments: attachments
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
- private
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
- header_finished = true if line =~ /^(Application Specific Information|Last Exception Backtrace|Thread \d+( Crashed)?):$/
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
- crashed_thread = true if error[:backtrace].compact.blank? && line =~ /^(Last Exception Backtrace|Thread \d+ Crashed):$/
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.parse_header_line(line, error, application)
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
- if line =~ /^(\d+)\s+([^\s]+)\s+(0x[0-9a-f]+)\s+(.+) \+ (\d+)$/
99
- {
100
- file: Regexp.last_match(2),
101
- function: Regexp.last_match(4),
102
- line: Regexp.last_match(5)
103
- }
104
- else
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