Skip to content

Commit 276b655

Browse files
authored
Merge pull request rails#55222 from rails/fxn/first_clean_frame
Implement ActiveSupport::BacktraceCleaner#first_clean_frame
2 parents 53a9a31 + ea66b1c commit 276b655

File tree

6 files changed

+79
-15
lines changed

6 files changed

+79
-15
lines changed

activejob/lib/active_job/log_subscriber.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,7 @@ def log_enqueue_source
256256
end
257257

258258
def enqueue_source_location
259-
Thread.each_caller_location do |location|
260-
frame = backtrace_cleaner.clean_frame(location)
261-
return frame if frame
262-
end
263-
nil
259+
backtrace_cleaner.first_clean_frame
264260
end
265261

266262
def enqueued_jobs_message(adapter, enqueued_jobs)

activerecord/lib/active_record/log_subscriber.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,7 @@ def log_query_source
127127
end
128128

129129
def query_source_location
130-
Thread.each_caller_location do |location|
131-
frame = backtrace_cleaner.clean_frame(location)
132-
return frame if frame
133-
end
134-
nil
130+
backtrace_cleaner.first_clean_frame
135131
end
136132

137133
def filter(name, value)

activerecord/lib/active_record/query_logs.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,7 @@ def clear_cache # :nodoc:
153153
end
154154

155155
def query_source_location # :nodoc:
156-
Thread.each_caller_location do |location|
157-
frame = LogSubscriber.backtrace_cleaner.clean_frame(location)
158-
return frame if frame
159-
end
160-
nil
156+
LogSubscriber.backtrace_cleaner.first_clean_frame
161157
end
162158

163159
ActiveSupport::ExecutionContext.after_change { ActiveRecord::QueryLogs.clear_cache }

activesupport/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
* The new method `ActiveSupport::BacktraceCleaner#first_clean_frame` returns
2+
the first clean frame of the caller's backtrace, or `nil`. Useful when you
3+
want to report the application-level location where something happened.
4+
5+
*Xavier Noria*
6+
17
* Always clear `CurrentAttribute` instances.
28

39
Previously `CurrentAttribute` instance would be reset at the end of requests.

activesupport/lib/active_support/backtrace_cleaner.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,32 @@ def clean_frame(frame, kind = :silent)
7474
end
7575
end
7676

77+
# Thread.each_caller_location does not accept a start in Ruby < 3.4.
78+
if Thread.method(:each_caller_location).arity == 0
79+
# Returns the first clean frame of the caller's backtrace, or +nil+.
80+
def first_clean_frame(kind = :silent)
81+
caller_location_skipped = false
82+
83+
Thread.each_caller_location do |location|
84+
unless caller_location_skipped
85+
caller_location_skipped = true
86+
next
87+
end
88+
89+
frame = clean_frame(location, kind)
90+
return frame if frame
91+
end
92+
end
93+
else
94+
# Returns the first clean frame of the caller's backtrace, or +nil+.
95+
def first_clean_frame(kind = :silent)
96+
Thread.each_caller_location(2) do |location|
97+
frame = clean_frame(location, kind)
98+
return frame if frame
99+
end
100+
end
101+
end
102+
77103
# Adds a filter from the block provided. Each line in the backtrace will be
78104
# mapped against this filter.
79105
#

activesupport/test/backtrace_cleaner_test.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,47 @@ def setup
136136
assert_equal backtrace, @bc.clean(backtrace)
137137
end
138138
end
139+
140+
class BacktraceCleanerFirstCleanFrame < ActiveSupport::TestCase
141+
def setup
142+
@bc = ActiveSupport::BacktraceCleaner.new
143+
end
144+
145+
def invoke_first_clean_frame_defaults
146+
-> do
147+
@bc.first_clean_frame.tap { @line = __LINE__ + 1 }
148+
end.call
149+
end
150+
151+
def invoke_first_clean_frame(kind = :silent)
152+
-> do
153+
@bc.first_clean_frame(kind).tap { @line = __LINE__ + 1 }
154+
end.call
155+
end
156+
157+
test "returns the first clean frame (defaults)" do
158+
result = invoke_first_clean_frame_defaults
159+
assert_match(/\A#{__FILE__}:#@line:in [`'](#{self.class}#)?invoke_first_clean_frame_defaults[`']\z/, result)
160+
end
161+
162+
test "returns the first clean frame (:silent)" do
163+
result = invoke_first_clean_frame(:silent)
164+
assert_match(/\A#{__FILE__}:#@line:in [`'](#{self.class}#)?invoke_first_clean_frame[`']\z/, result)
165+
end
166+
167+
test "returns the first clean frame (:noise)" do
168+
@bc.add_silencer { true }
169+
result = invoke_first_clean_frame(:noise)
170+
assert_match(/\A#{__FILE__}:#@line:in [`'](#{self.class}#)?invoke_first_clean_frame[`']\z/, result)
171+
end
172+
173+
test "returns the first clean frame (:any)" do
174+
result = invoke_first_clean_frame(:any) # fallback of the case statement
175+
assert_match(/\A#{__FILE__}:#@line:in [`'](#{self.class}#)?invoke_first_clean_frame[`']\z/, result)
176+
end
177+
178+
test "returns nil if there is no clean frame" do
179+
@bc.add_silencer { true }
180+
assert_nil invoke_first_clean_frame_defaults
181+
end
182+
end

0 commit comments

Comments
 (0)