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.
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