Skip to content

Commit 0794abb

Browse files
authored
Merge pull request rails#52684 from Shopify/error-subscriber-backtrace
ActiveSupport::ErrorReporter#report assigns a backtrace to unraised exceptions
2 parents ca05b17 + b1d8cf5 commit 0794abb

File tree

3 files changed

+43
-1
lines changed

3 files changed

+43
-1
lines changed

activesupport/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
* `ActiveSupport::ErrorReporter#report` now assigns a backtrace to unraised exceptions.
2+
3+
Previously reporting an un-raised exception would result in an error report without
4+
a backtrace. Now it automatically generates one.
5+
6+
*Jean Boussier*
7+
18
* Add `escape_html_entities` option to `ActiveSupport::JSON.encode`.
29

310
This allows for overriding the global configuration found at

activesupport/lib/active_support/error_reporter.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE
144144
#
145145
def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
146146
error = RuntimeError.new(error) if error.is_a?(String)
147-
error.set_backtrace(caller(1)) if error.backtrace.nil?
148147

149148
if @debug_mode
149+
ensure_backtrace(error)
150150
raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
151151
else
152152
report(error, handled: true, severity: severity, context: context, source: source)
@@ -209,6 +209,7 @@ def set_context(...)
209209
#
210210
def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211211
return if error.instance_variable_defined?(:@__rails_error_reported)
212+
ensure_backtrace(error)
212213

213214
unless SEVERITIES.include?(severity)
214215
raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
@@ -237,5 +238,28 @@ def report(error, handled: true, severity: handled ? :warning : :error, context:
237238

238239
nil
239240
end
241+
242+
private
243+
def ensure_backtrace(error)
244+
return if error.frozen? # re-raising won't add a backtrace
245+
return unless error.backtrace.nil?
246+
247+
begin
248+
# We could use Exception#set_backtrace, but until Ruby 3.4
249+
# it only support setting `Exception#backtrace` and not
250+
# `Exception#backtrace_locations`. So raising the exception
251+
# is a good way to build a real backtrace.
252+
raise error
253+
rescue error.class => error
254+
end
255+
256+
count = 0
257+
while error.backtrace_locations.first&.path == __FILE__
258+
count += 1
259+
error.backtrace_locations.shift
260+
end
261+
262+
error.backtrace.shift(count)
263+
end
240264
end
241265
end

activesupport/test/error_reporter_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,16 @@ class ErrorReporterTest < ActiveSupport::TestCase
161161
assert_equal [[error, false, :error, "application", {}]], @subscriber.events
162162
end
163163

164+
test "#report assigns a backtrace if it's missing" do
165+
error = RuntimeError.new("Oops")
166+
assert_nil error.backtrace
167+
assert_nil error.backtrace_locations
168+
169+
assert_nil @reporter.report(error)
170+
assert_not_predicate error.backtrace, :empty?
171+
assert_not_predicate error.backtrace_locations, :empty?
172+
end
173+
164174
test "#record passes through the return value" do
165175
result = @reporter.record do
166176
2 + 2
@@ -173,6 +183,7 @@ class ErrorReporterTest < ActiveSupport::TestCase
173183
assert_nil @reporter.unexpected(error)
174184
assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
175185
assert_not_predicate error.backtrace, :empty?
186+
assert_not_predicate error.backtrace_locations, :empty?
176187
end
177188

178189
test "#unexpected accepts an error message" do

0 commit comments

Comments
 (0)