redmine_airbrake_backend 0.4.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +91 -0
- data/Rakefile +3 -8
- data/app/controllers/airbrake_controller.rb +107 -127
- data/app/controllers/airbrake_notice_controller.rb +25 -0
- data/app/controllers/airbrake_project_settings_controller.rb +5 -4
- data/app/controllers/airbrake_report_controller.rb +15 -0
- data/app/helpers/airbrake_helper.rb +40 -15
- data/app/models/airbrake_project_setting.rb +1 -0
- data/app/views/airbrake/issue_description/default.erb +52 -0
- data/config/routes.rb +5 -1
- data/db/migrate/20130708102357_create_airbrake_project_settings.rb +0 -2
- data/db/migrate/20131003151228_add_reopen_repeat_description.rb +0 -2
- data/lib/redmine_airbrake_backend.rb +0 -2
- data/lib/redmine_airbrake_backend/engine.rb +10 -10
- data/lib/redmine_airbrake_backend/error.rb +78 -0
- data/lib/redmine_airbrake_backend/patches/projects_helper.rb +8 -7
- data/lib/redmine_airbrake_backend/report/ios.rb +71 -0
- data/lib/redmine_airbrake_backend/request.rb +132 -0
- data/lib/redmine_airbrake_backend/request/json.rb +73 -0
- data/lib/redmine_airbrake_backend/request/xml.rb +121 -0
- data/lib/redmine_airbrake_backend/version.rb +2 -1
- data/redmine_airbrake_backend.gemspec +3 -3
- data/test/unit/notice_test.rb +1 -3
- metadata +11 -4
- data/app/views/airbrake/_issue_description.erb +0 -50
- data/lib/redmine_airbrake_backend/notice.rb +0 -109
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'redmine_airbrake_backend/request'
|
2
|
+
|
3
|
+
module RedmineAirbrakeBackend
|
4
|
+
class Request
|
5
|
+
# Represents a JSON request received by airbrake
|
6
|
+
class JSON < ::RedmineAirbrakeBackend::Request
|
7
|
+
# Creates a request from a parsed json request
|
8
|
+
def self.parse(parsed_json_data)
|
9
|
+
raise Invalid if parsed_json_data.blank?
|
10
|
+
|
11
|
+
context = parse_plain_section(parsed_json_data[:context])
|
12
|
+
params = parse_plain_section(parsed_json_data[:params])
|
13
|
+
notifier = parse_plain_section(parsed_json_data[:notifier])
|
14
|
+
request = parse_plain_section(parsed_json_data[:request])
|
15
|
+
env = parse_plain_section(parsed_json_data[:environment])
|
16
|
+
config = parsed_json_data[:key]
|
17
|
+
|
18
|
+
errors = []
|
19
|
+
|
20
|
+
(parsed_json_data[:errors] || []).each do |error_data|
|
21
|
+
error = parse_error(error_data)
|
22
|
+
errors << ::RedmineAirbrakeBackend::Error.new(error) if error.present?
|
23
|
+
end
|
24
|
+
|
25
|
+
report = parse_report(parsed_json_data[:type], parsed_json_data[:report])
|
26
|
+
errors << ::RedmineAirbrakeBackend::Error.new(report) if report.present?
|
27
|
+
|
28
|
+
errors.compact!
|
29
|
+
|
30
|
+
raise Invalid.new('No error or report found') if errors.blank?
|
31
|
+
|
32
|
+
new(config, context: context, params: params, notifier: notifier, errors: errors, request: request, env: env)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def self.parse_plain_section(section_data)
|
38
|
+
return {} if section_data.blank?
|
39
|
+
|
40
|
+
section_data.symbolize_keys
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.parse_error(error_data)
|
44
|
+
return nil if error_data.blank?
|
45
|
+
|
46
|
+
error = {}
|
47
|
+
error[:type] = error_data[:type]
|
48
|
+
error[:message] = error_data[:message]
|
49
|
+
error[:backtrace] = error_data[:backtrace].map do |backtrace_element|
|
50
|
+
{
|
51
|
+
line: backtrace_element[:line].to_i,
|
52
|
+
file: backtrace_element[:file],
|
53
|
+
method: (backtrace_element[:method] || backtrace_element[:function])
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
error
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse_report(type, report_data)
|
61
|
+
return nil if report_data.blank?
|
62
|
+
|
63
|
+
require "redmine_airbrake_backend/report/#{type}"
|
64
|
+
|
65
|
+
clazz = "RedmineAirbrakeBackend::Report::#{type.to_s.camelize}".constantize rescue nil
|
66
|
+
|
67
|
+
return nil if clazz.blank?
|
68
|
+
|
69
|
+
clazz.parse(report_data)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'hpricot'
|
3
|
+
require 'redmine_airbrake_backend/request'
|
4
|
+
|
5
|
+
module RedmineAirbrakeBackend
|
6
|
+
class Request
|
7
|
+
# Represents a XML request received by airbrake
|
8
|
+
class XML < ::RedmineAirbrakeBackend::Request
|
9
|
+
# Supported airbrake api versions
|
10
|
+
SUPPORTED_API_VERSIONS = %w(2.4)
|
11
|
+
|
12
|
+
class UnsupportedVersion < Error; end
|
13
|
+
|
14
|
+
# Creates a notice from an airbrake xml request
|
15
|
+
def self.parse(xml_data)
|
16
|
+
doc = Hpricot::XML(xml_data) rescue nil
|
17
|
+
raise Invalid if doc.blank?
|
18
|
+
|
19
|
+
notice = doc.at('notice')
|
20
|
+
raise Invalid if notice.blank?
|
21
|
+
|
22
|
+
# Version
|
23
|
+
version = notice.attributes['version']
|
24
|
+
raise Invalid.new('No version') if version.blank?
|
25
|
+
raise UnsupportedVersion.new(version) unless SUPPORTED_API_VERSIONS.include?(version)
|
26
|
+
|
27
|
+
error_data = parse_error(notice)
|
28
|
+
raise Invalid.new('No error found') if error_data.blank?
|
29
|
+
error = RedmineAirbrakeBackend::Error.new(error_data)
|
30
|
+
|
31
|
+
notifier = convert_element(notice.at('notifier'))
|
32
|
+
request = parse_request(notice)
|
33
|
+
env = convert_element(notice.at('server-environment'))
|
34
|
+
config = notice.at('api-key').inner_text
|
35
|
+
|
36
|
+
new(config, notifier: notifier, errors: [error], request: request, env: env)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def self.parse_error(notice_doc)
|
42
|
+
error = convert_element(notice_doc.at('error'))
|
43
|
+
|
44
|
+
# map class to type
|
45
|
+
error[:type] ||= error[:class]
|
46
|
+
error.delete(:class)
|
47
|
+
|
48
|
+
error[:backtrace] = format_backtrace(error[:backtrace])
|
49
|
+
|
50
|
+
error
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.parse_request(notice_doc)
|
54
|
+
request = convert_element(notice_doc.at('request'))
|
55
|
+
|
56
|
+
if request.present? && request[:session].present?
|
57
|
+
request[:session][:log] = request[:session][:log].present? ? format_session_log(request[:session][:log]) : nil
|
58
|
+
end
|
59
|
+
|
60
|
+
request
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.convert_element(elem)
|
64
|
+
return nil if elem.nil?
|
65
|
+
return elem.children.first.inner_text if !elem.children.nil? && elem.children.count == 1 && elem.children.first.is_a?(Hpricot::Text)
|
66
|
+
return elem.attributes.to_hash.symbolize_keys if elem.children.nil?
|
67
|
+
return convert_var_elements(elem.children) if elem.children.count == elem.children.select { |c| c.name == 'var' }.count
|
68
|
+
|
69
|
+
h = {}
|
70
|
+
elem.children.each do |e|
|
71
|
+
key = format_hash_key(e.name)
|
72
|
+
if h.key?(key)
|
73
|
+
h[key] = [h[key]] unless h[key].is_a?(Array)
|
74
|
+
h[key] << convert_element(e)
|
75
|
+
else
|
76
|
+
h[key] = convert_element(e)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
h.delete_if { |k, v| k.strip.blank? }
|
80
|
+
h.symbolize_keys
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.convert_var_elements(elements)
|
84
|
+
vars = {}
|
85
|
+
elements.each do |elem|
|
86
|
+
vars[format_hash_key(elem.attributes['key'])] = elem.inner_text
|
87
|
+
end
|
88
|
+
vars.delete_if { |k, v| k.strip.blank? }
|
89
|
+
vars.symbolize_keys
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.format_hash_key(key)
|
93
|
+
key.to_s.gsub(/-/, '_')
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.ensure_hash_array(data)
|
97
|
+
return nil if data.blank?
|
98
|
+
|
99
|
+
d = (data.is_a?(Array) ? data : [data]).compact
|
100
|
+
d.reject! { |e| !e.is_a?(Hash) }
|
101
|
+
d.blank? ? nil : d
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.format_backtrace(backtrace)
|
105
|
+
ensure_hash_array(backtrace).first[:line] rescue nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.format_session_log(log)
|
109
|
+
log = ::JSON.parse(log) rescue nil
|
110
|
+
|
111
|
+
log = ensure_hash_array(log)
|
112
|
+
return nil if log.blank?
|
113
|
+
|
114
|
+
log.map! { |l| l.symbolize_keys!; l[:time] = (Time.parse(l[:time]) rescue nil); l }
|
115
|
+
log.reject! { |l| l[:time].blank? }
|
116
|
+
|
117
|
+
log.blank? ? nil : log
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
-
|
2
|
+
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
|
3
3
|
|
4
4
|
require 'redmine_airbrake_backend/version'
|
5
5
|
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = RedmineAirbrakeBackend::VERSION
|
9
9
|
spec.authors = ['Florian Schwab']
|
10
10
|
spec.email = ['me@ydkn.de']
|
11
|
-
spec.description = %q
|
12
|
-
spec.summary = %q
|
11
|
+
spec.description = %q(Plugin which adds Airbrake support to Redmine)
|
12
|
+
spec.summary = %q(This plugin provides the necessary API to use Redmine as a Airbrake backend)
|
13
13
|
spec.homepage = 'https://github.com/ydkn/redmine_airbrake_backend'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
data/test/unit/notice_test.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require File.expand_path('../../test_helper', __FILE__)
|
2
2
|
|
3
3
|
class NoticeTest < ActiveSupport::TestCase
|
4
|
-
|
5
4
|
test 'parse' do
|
6
5
|
api_key = {
|
7
6
|
api_key: 'foobar',
|
@@ -48,7 +47,6 @@ class NoticeTest < ActiveSupport::TestCase
|
|
48
47
|
private
|
49
48
|
|
50
49
|
def prepare_xml(xml_data)
|
51
|
-
xml_data.split("\n").map{|l| l.strip}.join('')
|
50
|
+
xml_data.split("\n").map { |l| l.strip }.join('')
|
52
51
|
end
|
53
|
-
|
54
52
|
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: 0.
|
4
|
+
version: 0.5.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: 2014-
|
11
|
+
date: 2014-10-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -89,14 +89,17 @@ extra_rdoc_files: []
|
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
91
|
- Gemfile
|
92
|
+
- Gemfile.lock
|
92
93
|
- MIT-LICENSE
|
93
94
|
- README.md
|
94
95
|
- Rakefile
|
95
96
|
- app/controllers/airbrake_controller.rb
|
97
|
+
- app/controllers/airbrake_notice_controller.rb
|
96
98
|
- app/controllers/airbrake_project_settings_controller.rb
|
99
|
+
- app/controllers/airbrake_report_controller.rb
|
97
100
|
- app/helpers/airbrake_helper.rb
|
98
101
|
- app/models/airbrake_project_setting.rb
|
99
|
-
- app/views/airbrake/
|
102
|
+
- app/views/airbrake/issue_description/default.erb
|
100
103
|
- app/views/projects/settings/_airbrake.html.erb
|
101
104
|
- app/views/settings/_airbrake.html.erb
|
102
105
|
- bin/rails
|
@@ -107,12 +110,16 @@ files:
|
|
107
110
|
- db/migrate/20131003151228_add_reopen_repeat_description.rb
|
108
111
|
- lib/redmine_airbrake_backend.rb
|
109
112
|
- lib/redmine_airbrake_backend/engine.rb
|
110
|
-
- lib/redmine_airbrake_backend/
|
113
|
+
- lib/redmine_airbrake_backend/error.rb
|
111
114
|
- lib/redmine_airbrake_backend/patches/issue_category.rb
|
112
115
|
- lib/redmine_airbrake_backend/patches/issue_priority.rb
|
113
116
|
- lib/redmine_airbrake_backend/patches/project.rb
|
114
117
|
- lib/redmine_airbrake_backend/patches/projects_helper.rb
|
115
118
|
- lib/redmine_airbrake_backend/patches/tracker.rb
|
119
|
+
- lib/redmine_airbrake_backend/report/ios.rb
|
120
|
+
- lib/redmine_airbrake_backend/request.rb
|
121
|
+
- lib/redmine_airbrake_backend/request/json.rb
|
122
|
+
- lib/redmine_airbrake_backend/request/xml.rb
|
116
123
|
- lib/redmine_airbrake_backend/version.rb
|
117
124
|
- lib/tasks/test.rake
|
118
125
|
- redmine_airbrake_backend.gemspec
|
@@ -1,50 +0,0 @@
|
|
1
|
-
h1. <%= @notice.error[:message] %>
|
2
|
-
|
3
|
-
<% if @notice.error.present? && @notice.error[:backtrace].present? %>
|
4
|
-
h2. Stacktrace:
|
5
|
-
|
6
|
-
p((. <%=raw @notice.error[:backtrace].collect{|e| format_backtrace_element(e)}.join("\n") %>
|
7
|
-
<% end %>
|
8
|
-
|
9
|
-
<% if @notice.request.present? %>
|
10
|
-
h2. Request:
|
11
|
-
|
12
|
-
<%=format_list_item('URL', @notice.request[:url]) %>
|
13
|
-
<%=format_list_item('Component', @notice.request[:component]) %>
|
14
|
-
<%=format_list_item('Action', @notice.request[:action]) %>
|
15
|
-
|
16
|
-
<% if @notice.request[:params].present? %>
|
17
|
-
h2. Parameters:
|
18
|
-
|
19
|
-
<%=format_table(@notice.request[:params]) %>
|
20
|
-
<% end %>
|
21
|
-
|
22
|
-
<% if @notice.request[:cgi_data].present? %>
|
23
|
-
h2. Headers:
|
24
|
-
|
25
|
-
<%=format_table(@notice.request[:cgi_data]) %>
|
26
|
-
<% end %>
|
27
|
-
|
28
|
-
<% log = @notice.request[:session].delete(:log) if @notice.request[:session].present? %>
|
29
|
-
<% if @notice.request[:session].present? %>
|
30
|
-
h2. Session:
|
31
|
-
|
32
|
-
<%=format_table(@notice.request[:session]) %>
|
33
|
-
<% end %>
|
34
|
-
|
35
|
-
<% if log.present? %>
|
36
|
-
h2. Log:
|
37
|
-
|
38
|
-
<pre>
|
39
|
-
<%=format_log(log) %>
|
40
|
-
</pre>
|
41
|
-
<% end %>
|
42
|
-
<% end %>
|
43
|
-
|
44
|
-
<% if @notice.env.present? %>
|
45
|
-
h2. Environment
|
46
|
-
|
47
|
-
<%=format_list_item('Name', @notice.env[:environment_name]) %>
|
48
|
-
<%=format_list_item('Directory', @notice.env[:project_root]) %>
|
49
|
-
<%=format_list_item('Hostname', @notice.env[:hostname]) %>
|
50
|
-
<% end %>
|
@@ -1,109 +0,0 @@
|
|
1
|
-
require 'json'
|
2
|
-
require 'hpricot'
|
3
|
-
require 'htmlentities'
|
4
|
-
|
5
|
-
module RedmineAirbrakeBackend
|
6
|
-
class Notice
|
7
|
-
SUPPORTED_API_VERSIONS = %w(2.4)
|
8
|
-
|
9
|
-
class NoticeInvalid < StandardError; end
|
10
|
-
class UnsupportedVersion < StandardError; end
|
11
|
-
|
12
|
-
attr_reader :version, :params, :notifier, :error, :request, :env
|
13
|
-
|
14
|
-
def initialize(version, params, notifier, options={})
|
15
|
-
@version = version
|
16
|
-
@params = params
|
17
|
-
@notifier = notifier
|
18
|
-
|
19
|
-
@error = options.delete(:error)
|
20
|
-
@request = options.delete(:request)
|
21
|
-
@env = options.delete(:env)
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.parse(xml_data)
|
25
|
-
doc = Hpricot::XML(xml_data)
|
26
|
-
|
27
|
-
raise NoticeInvalid if (notice = doc.at('notice')).blank?
|
28
|
-
raise NoticeInvalid.new('no version') if (version = notice.attributes['version']).blank?
|
29
|
-
raise UnsupportedVersion.new(version) unless SUPPORTED_API_VERSIONS.include?(version)
|
30
|
-
|
31
|
-
params = JSON.parse(notice.at('api-key').inner_text).symbolize_keys rescue nil
|
32
|
-
raise NoticeInvalid.new('no or invalid api-key') if params.blank?
|
33
|
-
|
34
|
-
raise NoticeInvalid.new('no notifier') if (notifier = convert_element(notice.at('notifier'))).blank?
|
35
|
-
|
36
|
-
raise NoticeInvalid.new('no error') if (error = convert_element(notice.at('error'))).blank?
|
37
|
-
raise NoticeInvalid.new('no message') if error[:message].blank?
|
38
|
-
|
39
|
-
error[:backtrace] = format_backtrace(error[:backtrace])
|
40
|
-
|
41
|
-
request = convert_element(notice.at('request'))
|
42
|
-
if request.present? && request[:session].present?
|
43
|
-
request[:session][:log] = request[:session][:log].present? ? format_session_log(request[:session][:log]) : nil
|
44
|
-
end
|
45
|
-
|
46
|
-
env = convert_element(notice.at('server-environment'))
|
47
|
-
|
48
|
-
new(version, params, notifier, error: error, request: request, env: env)
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def self.convert_element(elem)
|
54
|
-
return nil if elem.nil?
|
55
|
-
return elem.children.first.inner_text if !elem.children.nil? && elem.children.count == 1 && elem.children.first.is_a?(Hpricot::Text)
|
56
|
-
return elem.attributes.to_hash.symbolize_keys if elem.children.nil?
|
57
|
-
return convert_var_elements(elem.children) if elem.children.count == elem.children.select{|c| c.name == 'var'}.count
|
58
|
-
|
59
|
-
h = {}
|
60
|
-
elem.children.each do |e|
|
61
|
-
key = format_hash_key(e.name)
|
62
|
-
if h.key?(key)
|
63
|
-
h[key] = [h[key]] unless h[key].is_a?(Array)
|
64
|
-
h[key] << convert_element(e)
|
65
|
-
else
|
66
|
-
h[key] = convert_element(e)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
h.delete_if{|k,v| k.strip.blank?}
|
70
|
-
h.symbolize_keys
|
71
|
-
end
|
72
|
-
|
73
|
-
def self.convert_var_elements(elements)
|
74
|
-
vars = {}
|
75
|
-
elements.each do |elem|
|
76
|
-
vars[format_hash_key(elem.attributes['key'])] = elem.inner_text
|
77
|
-
end
|
78
|
-
vars.delete_if{|k,v| k.strip.blank?}
|
79
|
-
vars.symbolize_keys
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.format_hash_key(key)
|
83
|
-
key.to_s.gsub(/-/, '_')
|
84
|
-
end
|
85
|
-
|
86
|
-
def self.ensure_hash_array(data)
|
87
|
-
return nil if data.blank?
|
88
|
-
|
89
|
-
d = (data.is_a?(Array) ? data : [data]).compact
|
90
|
-
d.reject!{|e| !e.is_a?(Hash)}
|
91
|
-
d.blank? ? nil : d
|
92
|
-
end
|
93
|
-
|
94
|
-
def self.format_backtrace(backtrace)
|
95
|
-
ensure_hash_array(backtrace).first[:line] rescue nil
|
96
|
-
end
|
97
|
-
|
98
|
-
def self.format_session_log(log)
|
99
|
-
log = JSON.parse(log) rescue nil
|
100
|
-
return nil unless log = ensure_hash_array(log)
|
101
|
-
|
102
|
-
log.map!{|l| l.symbolize_keys!; l[:time] = (Time.parse(l[:time]) rescue nil); l}
|
103
|
-
log.reject!{|l| l[:time].blank?}
|
104
|
-
|
105
|
-
log.blank? ? nil : log
|
106
|
-
end
|
107
|
-
|
108
|
-
end
|
109
|
-
end
|