exception_handling 1.2.1 → 2.2.1
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 +5 -5
- data/.ruby-version +1 -1
- data/Gemfile +17 -0
- data/Gemfile.lock +142 -102
- data/README.md +11 -2
- data/config/exception_filters.yml +2 -0
- data/exception_handling.gemspec +6 -10
- data/lib/exception_handling.rb +222 -313
- data/lib/exception_handling/exception_catalog.rb +8 -6
- data/lib/exception_handling/exception_description.rb +8 -6
- data/lib/exception_handling/exception_info.rb +272 -0
- data/lib/exception_handling/honeybadger_callbacks.rb +42 -0
- data/lib/exception_handling/log_stub_error.rb +18 -3
- data/lib/exception_handling/mailer.rb +14 -0
- data/lib/exception_handling/methods.rb +26 -8
- data/lib/exception_handling/testing.rb +2 -2
- data/lib/exception_handling/version.rb +3 -1
- data/test/helpers/controller_helpers.rb +27 -0
- data/test/helpers/exception_helpers.rb +11 -0
- data/test/test_helper.rb +42 -19
- data/test/unit/exception_handling/exception_catalog_test.rb +19 -0
- data/test/unit/exception_handling/exception_description_test.rb +12 -1
- data/test/unit/exception_handling/exception_info_test.rb +501 -0
- data/test/unit/exception_handling/honeybadger_callbacks_test.rb +85 -0
- data/test/unit/exception_handling/log_error_stub_test.rb +26 -3
- data/test/unit/exception_handling/mailer_test.rb +39 -14
- data/test/unit/exception_handling/methods_test.rb +40 -18
- data/test/unit/exception_handling_test.rb +947 -539
- data/views/exception_handling/mailer/escalate_custom.html.erb +17 -0
- data/views/exception_handling/mailer/exception_notification.html.erb +1 -1
- data/views/exception_handling/mailer/log_parser_exception_notification.html.erb +1 -1
- metadata +28 -60
@@ -0,0 +1,27 @@
|
|
1
|
+
module ControllerHelpers
|
2
|
+
DummyController = Struct.new(:complete_request_uri, :request, :session)
|
3
|
+
DummyRequest = Struct.new(:env, :parameters, :session_options)
|
4
|
+
|
5
|
+
class DummySession
|
6
|
+
def initialize(data)
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
@data[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(key, value)
|
15
|
+
@data[key] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
@data
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_dummy_controller(env, parameters, session, request_uri)
|
24
|
+
request = DummyRequest.new(env, parameters, DummySession.new(session))
|
25
|
+
DummyController.new(request_uri, request, DummySession.new(session))
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module ExceptionHelpers
|
2
|
+
def raise_exception_with_nil_message
|
3
|
+
raise exception_with_nil_message
|
4
|
+
end
|
5
|
+
|
6
|
+
def exception_with_nil_message
|
7
|
+
exception_with_nil_message = RuntimeError.new(nil)
|
8
|
+
stub(exception_with_nil_message).message { nil }
|
9
|
+
exception_with_nil_message
|
10
|
+
end
|
11
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,39 +1,46 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
require 'active_support/time'
|
3
3
|
require 'active_support/test_case'
|
4
|
+
require 'active_model'
|
4
5
|
require 'action_mailer'
|
6
|
+
require 'action_dispatch'
|
5
7
|
require 'hobo_support'
|
6
8
|
require 'shoulda'
|
7
9
|
require 'rr'
|
8
10
|
require 'minitest/autorun'
|
9
11
|
require 'pry'
|
12
|
+
require 'honeybadger'
|
13
|
+
require 'contextual_logger'
|
10
14
|
|
11
15
|
require 'exception_handling'
|
12
16
|
require 'exception_handling/testing'
|
13
17
|
|
14
|
-
|
15
|
-
attr_accessor :logged
|
18
|
+
ActiveSupport::TestCase.test_order = :sorted
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
+
class LoggerStub
|
21
|
+
include ContextualLogger
|
22
|
+
attr_accessor :logged
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
def initialize
|
25
|
+
clear
|
26
|
+
end
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
+
def info(message, **log_context)
|
29
|
+
logged << { message: message, context: log_context }
|
30
|
+
end
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
def warn(message, **log_context)
|
33
|
+
logged << { message: message, context: log_context }
|
34
|
+
end
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
def fatal(message, **log_context)
|
37
|
+
logged << { message: message, context: log_context }
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@logged = []
|
42
|
+
end
|
43
|
+
end
|
37
44
|
|
38
45
|
class SocketStub
|
39
46
|
attr_accessor :sent, :connected
|
@@ -56,7 +63,7 @@ class SocketStub
|
|
56
63
|
end
|
57
64
|
|
58
65
|
def closed?
|
59
|
-
!@
|
66
|
+
!@connected
|
60
67
|
end
|
61
68
|
end
|
62
69
|
|
@@ -79,6 +86,18 @@ class ActiveSupport::TestCase
|
|
79
86
|
raise "Uh-oh! constant_overrides left over: #{@@constant_overrides.inspect}"
|
80
87
|
end
|
81
88
|
|
89
|
+
unless defined?(Rails) && defined?(Rails.env)
|
90
|
+
module ::Rails
|
91
|
+
def self.env
|
92
|
+
@env ||= 'test'
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.env=(mode)
|
96
|
+
@env = mode
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
82
101
|
Time.now_override = nil
|
83
102
|
|
84
103
|
ActionMailer::Base.deliveries.clear
|
@@ -149,6 +168,10 @@ def assert_equal_with_diff arg1, arg2, msg = ''
|
|
149
168
|
end
|
150
169
|
end
|
151
170
|
|
171
|
+
def require_test_helper(helper_path)
|
172
|
+
require_relative "helpers/#{helper_path}"
|
173
|
+
end
|
174
|
+
|
152
175
|
class Time
|
153
176
|
class << self
|
154
177
|
attr_reader :now_override
|
@@ -35,6 +35,16 @@ module ExceptionHandling
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
+
should "write errors loading the yaml file directly to the log file" do
|
39
|
+
@exception_catalog = ExceptionCatalog.new( ExceptionHandling.filter_list_filename )
|
40
|
+
|
41
|
+
mock(ExceptionHandling).log_error.never
|
42
|
+
mock(ExceptionHandling).write_exception_to_log(anything(), "ExceptionCatalog#refresh_filters: ./config/exception_filters.yml", anything())
|
43
|
+
mock(@exception_catalog).load_file { raise "noooooo"}
|
44
|
+
|
45
|
+
@exception_catalog.find({})
|
46
|
+
end
|
47
|
+
|
38
48
|
end
|
39
49
|
|
40
50
|
context "with live yaml content" do
|
@@ -50,8 +60,17 @@ module ExceptionHandling
|
|
50
60
|
assert !@exception_catalog.find( error: "Scott says unlikely to ever match" )
|
51
61
|
assert !@exception_catalog.find( error: "Scott says unlikely to ever match" )
|
52
62
|
end
|
63
|
+
end
|
53
64
|
|
65
|
+
context "with no yaml content" do
|
66
|
+
setup do
|
67
|
+
@exception_catalog = ExceptionCatalog.new(nil)
|
68
|
+
end
|
54
69
|
|
70
|
+
should "not load filter data" do
|
71
|
+
mock(ExceptionHandling).write_exception_to_log.with_any_args.never
|
72
|
+
@exception_catalog.find( error: "Scott says unlikely to ever match" )
|
73
|
+
end
|
55
74
|
end
|
56
75
|
|
57
76
|
private
|
@@ -15,6 +15,11 @@ module ExceptionHandling
|
|
15
15
|
assert @f.match?( :error => "my error message")
|
16
16
|
end
|
17
17
|
|
18
|
+
should "allow wildcards to cross line boundries" do
|
19
|
+
@f = ExceptionDescription.new(:filter1, :error => "my error message.*with multiple lines" )
|
20
|
+
assert @f.match?( :error => "my error message\nwith more than one, with multiple lines")
|
21
|
+
end
|
22
|
+
|
18
23
|
should "complain when no regexps have a value" do
|
19
24
|
assert_raise(ArgumentError, "has all blank regexe") { ExceptionDescription.new(:filter1, error: nil) }
|
20
25
|
end
|
@@ -29,6 +34,12 @@ module ExceptionHandling
|
|
29
34
|
assert !ExceptionDescription.new(:filter1, error: "my error message" ).send_email
|
30
35
|
end
|
31
36
|
|
37
|
+
should "allow send_to_honeybadger to be specified and have it disabled by default" do
|
38
|
+
assert !ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: false).send_to_honeybadger
|
39
|
+
assert ExceptionDescription.new(:filter1, error: "my error message", send_to_honeybadger: true).send_to_honeybadger
|
40
|
+
assert !ExceptionDescription.new(:filter1, error: "my error message").send_to_honeybadger
|
41
|
+
end
|
42
|
+
|
32
43
|
should "allow send_metric to be configured" do
|
33
44
|
assert !ExceptionDescription.new(:filter1, error: "my error message", send_metric: false ).send_metric
|
34
45
|
assert ExceptionDescription.new(:filter1, error: "my error message", send_email: true ).send_metric
|
@@ -46,7 +57,7 @@ module ExceptionHandling
|
|
46
57
|
end
|
47
58
|
|
48
59
|
should "allow notes to be recorded" do
|
49
|
-
|
60
|
+
assert_nil ExceptionDescription.new(:filter1, error: "my error message" ).notes
|
50
61
|
assert_equal "a long string", ExceptionDescription.new(:filter1, error: "my error message", notes: "a long string" ).notes
|
51
62
|
end
|
52
63
|
|
@@ -0,0 +1,501 @@
|
|
1
|
+
require File.expand_path('../../../test_helper', __FILE__)
|
2
|
+
require_test_helper 'controller_helpers'
|
3
|
+
require_test_helper 'exception_helpers'
|
4
|
+
|
5
|
+
module ExceptionHandling
|
6
|
+
class ExceptionInfoTest < ActiveSupport::TestCase
|
7
|
+
include ControllerHelpers
|
8
|
+
include ExceptionHelpers
|
9
|
+
|
10
|
+
context "initialize" do
|
11
|
+
setup do
|
12
|
+
@exception = StandardError.new("something went wrong")
|
13
|
+
@timestamp = Time.now
|
14
|
+
@controller = Object.new
|
15
|
+
end
|
16
|
+
|
17
|
+
context "controller_from_context" do
|
18
|
+
should "extract controller from context when not specified explicitly" do
|
19
|
+
exception_context = {
|
20
|
+
"action_controller.instance" => @controller
|
21
|
+
}
|
22
|
+
exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
|
23
|
+
assert_equal @controller, exception_info.controller
|
24
|
+
end
|
25
|
+
|
26
|
+
should "prefer the explicit controller over the one from context" do
|
27
|
+
exception_context = {
|
28
|
+
"action_controller.instance" => Object.new
|
29
|
+
}
|
30
|
+
exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp, @controller)
|
31
|
+
assert_equal @controller, exception_info.controller
|
32
|
+
assert_not_equal exception_context["action_controller.instance"], exception_info.controller
|
33
|
+
end
|
34
|
+
|
35
|
+
should "leave controller unset when not included in the context hash" do
|
36
|
+
exception_info = ExceptionInfo.new(@exception, {}, @timestamp)
|
37
|
+
assert_nil exception_info.controller
|
38
|
+
end
|
39
|
+
|
40
|
+
should "leave controller unset when context is not in hash format" do
|
41
|
+
exception_info = ExceptionInfo.new(@exception, "string context", @timestamp)
|
42
|
+
assert_nil exception_info.controller
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "data" do
|
48
|
+
setup do
|
49
|
+
@exception = StandardError.new("something went wrong")
|
50
|
+
@timestamp = Time.now
|
51
|
+
end
|
52
|
+
|
53
|
+
should "return a hash with exception specific data including context hash" do
|
54
|
+
exception_context = {
|
55
|
+
"rack.session" => {
|
56
|
+
user_id: 23,
|
57
|
+
user_name: "John"
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
|
62
|
+
expected_data = {
|
63
|
+
"error_class" => "StandardError",
|
64
|
+
"error_string" => "StandardError: something went wrong",
|
65
|
+
"timestamp" => @timestamp,
|
66
|
+
"backtrace" => ["<no backtrace>"],
|
67
|
+
"error" => "StandardError: something went wrong",
|
68
|
+
"session" => { "user_id" => 23, "user_name" => "John" },
|
69
|
+
"environment" => {
|
70
|
+
"rack.session" => { "user_id" => 23, "user_name" => "John" }
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
assert_equal_with_diff expected_data, exception_info.data
|
75
|
+
end
|
76
|
+
|
77
|
+
should "generate exception data appropriately if exception message is nil" do
|
78
|
+
exception_info = ExceptionInfo.new(exception_with_nil_message, "custom context data", @timestamp)
|
79
|
+
exception_data = exception_info.data
|
80
|
+
assert_equal "RuntimeError: ", exception_data["error_string"]
|
81
|
+
assert_equal "RuntimeError: : custom context data", exception_data["error"]
|
82
|
+
end
|
83
|
+
|
84
|
+
should "return a hash with exception specific data including context string" do
|
85
|
+
exception_context = "custom context data"
|
86
|
+
exception_info = ExceptionInfo.new(@exception, exception_context, @timestamp)
|
87
|
+
expected_data = {
|
88
|
+
"error_class" => "StandardError",
|
89
|
+
"error_string" => "StandardError: something went wrong",
|
90
|
+
"timestamp" => @timestamp,
|
91
|
+
"backtrace" => ["<no backtrace>"],
|
92
|
+
"error" => "StandardError: something went wrong: custom context data",
|
93
|
+
"environment" => {
|
94
|
+
"message" => "custom context data"
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
assert_equal_with_diff expected_data, exception_info.data
|
99
|
+
end
|
100
|
+
|
101
|
+
should "not include enhanced data from controller or custom data callback" do
|
102
|
+
env = { server: "fe98" }
|
103
|
+
parameters = { advertiser_id: 435 }
|
104
|
+
session = { username: "jsmith" }
|
105
|
+
request_uri = "host/path"
|
106
|
+
controller = create_dummy_controller(env, parameters, session, request_uri)
|
107
|
+
data_callback = ->(data) { data[:custom_section] = "value" }
|
108
|
+
exception_info = ExceptionInfo.new(@exception, "custom context data", @timestamp, controller, data_callback)
|
109
|
+
|
110
|
+
dont_allow(exception_info).extract_and_merge_controller_data
|
111
|
+
dont_allow(exception_info).customize_from_data_callback
|
112
|
+
expected_data = {
|
113
|
+
"error_class" => "StandardError",
|
114
|
+
"error_string" => "StandardError: something went wrong",
|
115
|
+
"timestamp" => @timestamp,
|
116
|
+
"backtrace" => ["<no backtrace>"],
|
117
|
+
"error" => "StandardError: something went wrong: custom context data",
|
118
|
+
"environment" => {
|
119
|
+
"message" => "custom context data"
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
assert_equal_with_diff expected_data, exception_info.data
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "enhanced_data" do
|
128
|
+
setup do
|
129
|
+
@exception = StandardError.new("something went wrong")
|
130
|
+
@timestamp = Time.now
|
131
|
+
@exception_context = {
|
132
|
+
"rack.session" => {
|
133
|
+
user_id: 23,
|
134
|
+
user_name: "John"
|
135
|
+
},
|
136
|
+
"SERVER_NAME" => "exceptional.com"
|
137
|
+
}
|
138
|
+
env = { server: "fe98" }
|
139
|
+
parameters = { advertiser_id: 435, controller: "dummy", action: "fail" }
|
140
|
+
session = { username: "jsmith", id: "session_key" }
|
141
|
+
request_uri = "host/path"
|
142
|
+
@controller = create_dummy_controller(env, parameters, session, request_uri)
|
143
|
+
@data_callback = ->(data) { data[:custom_section] = "check this out" }
|
144
|
+
end
|
145
|
+
|
146
|
+
should "not return a mutable object for the session" do
|
147
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
|
148
|
+
exception_info.enhanced_data["session"]["hello"] = "world"
|
149
|
+
assert_nil @controller.session["hello"]
|
150
|
+
end
|
151
|
+
|
152
|
+
should "return a hash with generic exception attributes as well as context data" do
|
153
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
|
154
|
+
expected_data = {
|
155
|
+
"error_class" => "StandardError",
|
156
|
+
"error_string" => "StandardError: something went wrong",
|
157
|
+
"timestamp" => @timestamp,
|
158
|
+
"backtrace" => ["<no backtrace>"],
|
159
|
+
"error" => "StandardError: something went wrong",
|
160
|
+
"session" => { "user_id" => 23, "user_name" => "John" },
|
161
|
+
"environment" => { "SERVER_NAME" => "exceptional.com" },
|
162
|
+
"location" => { "file" => "<no backtrace>", "line" => nil }
|
163
|
+
}
|
164
|
+
|
165
|
+
assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
|
166
|
+
end
|
167
|
+
|
168
|
+
should "generate exception data appropriately if exception message is nil" do
|
169
|
+
exception_with_nil_message = RuntimeError.new(nil)
|
170
|
+
stub(exception_with_nil_message).message { nil }
|
171
|
+
exception_info = ExceptionInfo.new(exception_with_nil_message, @exception_context, @timestamp)
|
172
|
+
exception_data = exception_info.enhanced_data
|
173
|
+
assert_equal "RuntimeError: ", exception_data["error_string"]
|
174
|
+
assert_equal "RuntimeError: ", exception_data["error"]
|
175
|
+
end
|
176
|
+
|
177
|
+
should "include controller data when available" do
|
178
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
|
179
|
+
expected_data = {
|
180
|
+
"error_class" => "StandardError",
|
181
|
+
"error_string" => "StandardError: something went wrong",
|
182
|
+
"timestamp" => @timestamp,
|
183
|
+
"backtrace" => ["<no backtrace>"],
|
184
|
+
"error" => "StandardError: something went wrong",
|
185
|
+
"session" => { "key" => "session_key", "data" => { "username" => "jsmith", "id" => "session_key" } },
|
186
|
+
"environment" => { "SERVER_NAME" => "exceptional.com" },
|
187
|
+
"request" => {
|
188
|
+
"params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
|
189
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
190
|
+
"url" => "host/path"
|
191
|
+
},
|
192
|
+
"location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
|
193
|
+
}
|
194
|
+
|
195
|
+
assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
|
196
|
+
end
|
197
|
+
|
198
|
+
should "extract controller from rack specific exception context when not provided explicitly" do
|
199
|
+
@exception_context["action_controller.instance"] = @controller
|
200
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
|
201
|
+
expected_data = {
|
202
|
+
"error_class" => "StandardError",
|
203
|
+
"error_string" => "StandardError: something went wrong",
|
204
|
+
"timestamp" => @timestamp,
|
205
|
+
"backtrace" => ["<no backtrace>"],
|
206
|
+
"error" => "StandardError: something went wrong",
|
207
|
+
"session" => { "key" => "session_key", "data" => { "username" => "jsmith", "id" => "session_key" } },
|
208
|
+
"environment" => { "SERVER_NAME" => "exceptional.com" },
|
209
|
+
"request" => {
|
210
|
+
"params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
|
211
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
212
|
+
"url" => "host/path"
|
213
|
+
},
|
214
|
+
"location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
|
215
|
+
}
|
216
|
+
|
217
|
+
assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
|
218
|
+
end
|
219
|
+
|
220
|
+
should "add to_s attribute to specific sections that have their content in hash format" do
|
221
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
|
222
|
+
expected_data = {
|
223
|
+
"error_class" => "StandardError",
|
224
|
+
"error_string" => "StandardError: something went wrong",
|
225
|
+
"timestamp" => @timestamp,
|
226
|
+
"backtrace" => ["<no backtrace>"],
|
227
|
+
"error" => "StandardError: something went wrong",
|
228
|
+
"session" => {
|
229
|
+
"key" => "session_key",
|
230
|
+
"data" => { "username" => "jsmith", "id" => "session_key" },
|
231
|
+
"to_s" => "data:\n id: session_key\n username: jsmith\nkey: session_key\n" },
|
232
|
+
"environment" => {
|
233
|
+
"SERVER_NAME" => "exceptional.com",
|
234
|
+
"to_s" => "SERVER_NAME: exceptional.com\n" },
|
235
|
+
"request" => {
|
236
|
+
"params" => { "advertiser_id" => 435, "controller" => "dummy", "action" => "fail" },
|
237
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
238
|
+
"url" => "host/path",
|
239
|
+
"to_s" => "params:\n action: fail\n advertiser_id: 435\n controller: dummy\nrails_root: Rails.root not defined. Is this a test environment?\nurl: host/path\n"
|
240
|
+
},
|
241
|
+
"location" => { "controller" => "dummy", "action" => "fail", "file" => "<no backtrace>", "line" => nil }
|
242
|
+
}
|
243
|
+
|
244
|
+
assert_equal_with_diff expected_data, exception_info.enhanced_data
|
245
|
+
end
|
246
|
+
|
247
|
+
should "filter out sensitive parameters like passwords" do
|
248
|
+
@controller.request.parameters[:password] = "super_secret"
|
249
|
+
@controller.request.parameters[:user] = { "password" => "also super secret", "password_confirmation" => "also super secret" }
|
250
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, @controller)
|
251
|
+
expected_params = {
|
252
|
+
"password" => "[FILTERED]",
|
253
|
+
"advertiser_id" => 435, "controller" => "dummy",
|
254
|
+
"action" => "fail",
|
255
|
+
"user" => {
|
256
|
+
"password" => "[FILTERED]",
|
257
|
+
"password_confirmation" => "[FILTERED]"
|
258
|
+
}
|
259
|
+
}
|
260
|
+
assert_equal_with_diff expected_params, exception_info.enhanced_data["request"]["params"]
|
261
|
+
end
|
262
|
+
|
263
|
+
should "include the changes from the custom data callback" do
|
264
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp, nil, @data_callback)
|
265
|
+
expected_data = {
|
266
|
+
"error_class" => "StandardError",
|
267
|
+
"error_string" => "StandardError: something went wrong",
|
268
|
+
"timestamp" => @timestamp,
|
269
|
+
"backtrace" => ["<no backtrace>"],
|
270
|
+
"error" => "StandardError: something went wrong",
|
271
|
+
"session" => { "user_id" => 23, "user_name" => "John" },
|
272
|
+
"environment" => { "SERVER_NAME" => "exceptional.com" },
|
273
|
+
"custom_section" => "check this out",
|
274
|
+
"location" => { "file" => "<no backtrace>", "line" => nil }
|
275
|
+
}
|
276
|
+
|
277
|
+
assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
|
278
|
+
end
|
279
|
+
|
280
|
+
should "apply the custom_data_hook results" do
|
281
|
+
stub(ExceptionHandling).custom_data_hook { ->(data) { data[:custom_hook] = "changes from custom hook" } }
|
282
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
|
283
|
+
expected_data = {
|
284
|
+
"error_class" => "StandardError",
|
285
|
+
"error_string" => "StandardError: something went wrong",
|
286
|
+
"timestamp" => @timestamp,
|
287
|
+
"backtrace" => ["<no backtrace>"],
|
288
|
+
"error" => "StandardError: something went wrong",
|
289
|
+
"session" => { "user_id" => 23, "user_name" => "John" },
|
290
|
+
"environment" => { "SERVER_NAME" => "exceptional.com" },
|
291
|
+
"custom_hook" => "changes from custom hook",
|
292
|
+
"location" => { "file" => "<no backtrace>", "line" => nil }
|
293
|
+
}
|
294
|
+
|
295
|
+
assert_equal_with_diff expected_data, prepare_data(exception_info.enhanced_data)
|
296
|
+
end
|
297
|
+
|
298
|
+
should "log info if the custom data hook results in a nil message exception" do
|
299
|
+
ExceptionHandling.custom_data_hook = lambda do |data|
|
300
|
+
raise_exception_with_nil_message
|
301
|
+
end
|
302
|
+
log_info_messages = []
|
303
|
+
stub(ExceptionHandling.logger).info.with_any_args do |message, _|
|
304
|
+
log_info_messages << message
|
305
|
+
end
|
306
|
+
|
307
|
+
exception_info = ExceptionInfo.new(@exception, @exception_context, @timestamp)
|
308
|
+
exception_info.enhanced_data
|
309
|
+
assert log_info_messages.find { |message| message =~ /Unable to execute custom custom_data_hook callback/ }
|
310
|
+
ExceptionHandling.custom_data_hook = nil
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
context "exception_description" do
|
315
|
+
should "return the exception description from the global exception filter list" do
|
316
|
+
exception = StandardError.new("No route matches")
|
317
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
318
|
+
description = exception_info.exception_description
|
319
|
+
assert_not_nil description
|
320
|
+
assert_equal :NoRoute, description.filter_name
|
321
|
+
end
|
322
|
+
|
323
|
+
should "find the description when filter criteria includes section in hash format" do
|
324
|
+
env = { server: "fe98" }
|
325
|
+
parameters = { advertiser_id: 435, controller: "sessions", action: "fail" }
|
326
|
+
session = { username: "jsmith", id: "session_key" }
|
327
|
+
request_uri = "host/path"
|
328
|
+
controller = create_dummy_controller(env, parameters, session, request_uri)
|
329
|
+
exception = StandardError.new("Request to click domain rejected")
|
330
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now, controller)
|
331
|
+
assert_equal true, exception_info.enhanced_data[:request].is_a?(Hash)
|
332
|
+
description = exception_info.exception_description
|
333
|
+
assert_not_nil description
|
334
|
+
assert_equal :"Click Request Rejected", description.filter_name
|
335
|
+
end
|
336
|
+
|
337
|
+
should "return same description object for related errors (avoid reloading exception catalog from disk)" do
|
338
|
+
exception = StandardError.new("No route matches")
|
339
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
340
|
+
description = exception_info.exception_description
|
341
|
+
|
342
|
+
repeat_ex = StandardError.new("No route matches 2")
|
343
|
+
repeat_ex_info = ExceptionInfo.new(repeat_ex, {}, Time.now)
|
344
|
+
assert_equal description.object_id, repeat_ex_info.exception_description.object_id
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
context "send_to_honeybadger?" do
|
349
|
+
should "be enabled when Honeybadger is defined and exception is not in the filter list" do
|
350
|
+
stub(ExceptionHandling).honeybadger? { true }
|
351
|
+
exception = StandardError.new("something went wrong")
|
352
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
353
|
+
assert_nil exception_info.exception_description
|
354
|
+
assert_equal true, exception_info.send_to_honeybadger?
|
355
|
+
end
|
356
|
+
|
357
|
+
should "be enabled when Honeybadger is defined and exception is on the filter list with the flag turned on" do
|
358
|
+
stub(ExceptionHandling).honeybadger? { true }
|
359
|
+
exception = StandardError.new("No route matches")
|
360
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
361
|
+
assert_not_nil exception_info.exception_description
|
362
|
+
assert_equal true, exception_info.exception_description.send_to_honeybadger
|
363
|
+
assert_equal true, exception_info.send_to_honeybadger?
|
364
|
+
end
|
365
|
+
|
366
|
+
should "be disabled when Honeybadger is defined and exception is on the filter list with the flag turned off" do
|
367
|
+
stub(ExceptionHandling).honeybadger? { true }
|
368
|
+
exception = StandardError.new("No route matches")
|
369
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
370
|
+
assert_not_nil exception_info.exception_description
|
371
|
+
stub(exception_info.exception_description).send_to_honeybadger { false }
|
372
|
+
assert_equal false, exception_info.send_to_honeybadger?
|
373
|
+
end
|
374
|
+
|
375
|
+
should "be disabled when Honeybadger is not defined" do
|
376
|
+
stub(ExceptionHandling).honeybadger? { false }
|
377
|
+
exception = StandardError.new("something went wrong")
|
378
|
+
exception_info = ExceptionInfo.new(exception, {}, Time.now)
|
379
|
+
assert_nil exception_info.exception_description
|
380
|
+
assert_equal false, exception_info.send_to_honeybadger?
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
context "honeybadger_context_data" do
|
385
|
+
should "return the error details and relevant context data to be used as honeybadger notification context while filtering sensitive data" do
|
386
|
+
env = { server: "fe98" }
|
387
|
+
parameters = { advertiser_id: 435 }
|
388
|
+
session = { username: "jsmith" }
|
389
|
+
request_uri = "host/path"
|
390
|
+
controller = create_dummy_controller(env, parameters, session, request_uri)
|
391
|
+
stub(ExceptionHandling).server_name { "invoca_fe98" }
|
392
|
+
|
393
|
+
exception = StandardError.new("Some BS")
|
394
|
+
exception.set_backtrace([
|
395
|
+
"test/unit/exception_handling_test.rb:847:in `exception_1'",
|
396
|
+
"test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"])
|
397
|
+
exception_context = { "SERVER_NAME" => "exceptional.com" }
|
398
|
+
data_callback = ->(data) do
|
399
|
+
data[:scm_revision] = "5b24eac37aaa91f5784901e9aabcead36fd9df82"
|
400
|
+
data[:user_details] = { username: "jsmith" }
|
401
|
+
data[:event_response] = "Event successfully received"
|
402
|
+
data[:other_section] = "This should not be included in the response"
|
403
|
+
end
|
404
|
+
timestamp = Time.now
|
405
|
+
exception_info = ExceptionInfo.new(exception, exception_context, timestamp, controller, data_callback)
|
406
|
+
|
407
|
+
expected_data = {
|
408
|
+
timestamp: timestamp,
|
409
|
+
error_class: "StandardError",
|
410
|
+
exception_context: { "SERVER_NAME" => "exceptional.com" },
|
411
|
+
server: "invoca_fe98",
|
412
|
+
scm_revision: "5b24eac37aaa91f5784901e9aabcead36fd9df82",
|
413
|
+
notes: "this is used by a test",
|
414
|
+
user_details: { "username" => "jsmith" },
|
415
|
+
request: {
|
416
|
+
"params" => { "advertiser_id" => 435 },
|
417
|
+
"rails_root" => "Rails.root not defined. Is this a test environment?",
|
418
|
+
"url" => "host/path" },
|
419
|
+
session: {
|
420
|
+
"key" => nil,
|
421
|
+
"data" => { "username" => "jsmith" } },
|
422
|
+
environment: {
|
423
|
+
"SERVER_NAME" => "exceptional.com" },
|
424
|
+
backtrace: [
|
425
|
+
"test/unit/exception_handling_test.rb:847:in `exception_1'",
|
426
|
+
"test/unit/exception_handling_test.rb:455:in `block (4 levels) in <class:ExceptionHandlingTest>'"],
|
427
|
+
event_response: "Event successfully received"
|
428
|
+
}
|
429
|
+
assert_equal_with_diff expected_data, exception_info.honeybadger_context_data
|
430
|
+
end
|
431
|
+
|
432
|
+
[ ['Hash', { 'cookie' => 'cookie_context' } ],
|
433
|
+
['String', 'Entering Error State' ],
|
434
|
+
['Array', ['Error1', 'Error2'] ]].each do |klass, value|
|
435
|
+
|
436
|
+
should "extract context from exception_context when it is a #{klass}" do
|
437
|
+
exception = StandardError.new("Exception")
|
438
|
+
exception_context = value
|
439
|
+
exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
|
440
|
+
|
441
|
+
assert_equal klass, value.class.name
|
442
|
+
assert_equal value, exception_info.honeybadger_context_data[:exception_context]
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
should "filter out sensitive data from exception context such as [password, password_confirmation, oauth_token]" do
|
447
|
+
sensitive_data = {
|
448
|
+
"password" => "super_secret",
|
449
|
+
"password_confirmation" => "super_secret_confirmation",
|
450
|
+
"oauth_token" => "super_secret_oauth_token"
|
451
|
+
}
|
452
|
+
|
453
|
+
exception = StandardError.new("boom")
|
454
|
+
exception_context = {
|
455
|
+
"SERVER_NAME" => "exceptional.com",
|
456
|
+
"one_layer" => sensitive_data,
|
457
|
+
"two_layers" => {
|
458
|
+
"sensitive_data" => sensitive_data
|
459
|
+
},
|
460
|
+
"rack.request.form_vars" => "username=investor%40invoca.com&password=my_special_password&commit=Log+In",
|
461
|
+
"example_without_password" => {
|
462
|
+
"rack.request.form_vars" => "username=investor%40invoca.com"
|
463
|
+
}
|
464
|
+
}.merge(sensitive_data)
|
465
|
+
|
466
|
+
exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
|
467
|
+
|
468
|
+
expected_sensitive_data = ["password", "password_confirmation", "oauth_token"].build_hash { |key| [key, "[FILTERED]"] }
|
469
|
+
expected_exception_context = {
|
470
|
+
"SERVER_NAME" => "exceptional.com",
|
471
|
+
"one_layer" => expected_sensitive_data,
|
472
|
+
"two_layers" => {
|
473
|
+
"sensitive_data" => expected_sensitive_data
|
474
|
+
},
|
475
|
+
"rack.request.form_vars" => "username=investor%40invoca.com&password=[FILTERED]&commit=Log+In",
|
476
|
+
"example_without_password" => {
|
477
|
+
"rack.request.form_vars" => "username=investor%40invoca.com"
|
478
|
+
}
|
479
|
+
}.merge(expected_sensitive_data)
|
480
|
+
|
481
|
+
assert_equal_with_diff expected_exception_context, exception_info.honeybadger_context_data[:exception_context]
|
482
|
+
end
|
483
|
+
|
484
|
+
should "omit context if exception_context is empty" do
|
485
|
+
exception = StandardError.new("Exception")
|
486
|
+
exception_context = ""
|
487
|
+
exception_info = ExceptionInfo.new(exception, exception_context, Time.now)
|
488
|
+
|
489
|
+
assert_nil exception_info.honeybadger_context_data[:exception_context]
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def prepare_data(data)
|
494
|
+
data.each do |key, section|
|
495
|
+
if section.is_a?(Hash)
|
496
|
+
section.delete(:to_s)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|