Description
setup_sentry_test modifies whichever hub Sentry.get_current_hub returns (the thread-local hub), but in Rails request/integration specs, Sentry::Rack::CaptureExceptions middleware calls Sentry.clone_hub_to_current_thread, which always clones @main_hub — not the thread-local hub that setup_sentry_test modified. This overwrites the test setup entirely.
After the first setup_sentry_test call sets a DummyTransport on @main_hub (because in the main thread, thread-local IS @main_hub), teardown_sentry_test pops the scope but the base layer with DummyTransport remains. Subsequent requests in unrelated tests then capture events to this leftover DummyTransport, and those stale events leak into later tests that use sentry_events.
Since RSpec uses random test order, this manifests as intermittent failures.
Reproduction
require "sentry-ruby"
require "sentry/test_helper"
include Sentry::TestHelper
Sentry.init do |config|
config.dsn = "http://key@sentry.localdomain/42"
config.environment = "test"
config.enabled_environments = ["test"]
end
# Test A: first sentry test
setup_sentry_test
teardown_sentry_test
# Test B: intermediate request (no sentry helper)
Sentry.clone_hub_to_current_thread
Sentry.capture_message("leaked event from test B")
puts "main_hub transport events: #{Sentry.get_main_hub.current_client.transport.events.count}"
# => 1
# Test C: new sentry test (should start clean)
setup_sentry_test
puts "sentry_events (thread-local): #{sentry_events.count}"
# => 0 (looks clean)
# Simulate Rack middleware
Sentry.clone_hub_to_current_thread
puts "sentry_events (after clone): #{Sentry.get_current_client.transport.events.count}"
# => 1 (BUG: stale event from Test B leaked through!)
teardown_sentry_test
Output:
main_hub transport events: 1
sentry_events (thread-local): 0
sentry_events (after clone): 1
Expected Behavior
After setup_sentry_test, sentry_events should always be empty, even after clone_hub_to_current_thread is called.
Root Cause
setup_sentry_test calls get_current_hub which returns the thread-local hub (not necessarily @main_hub)
- It modifies this hub with
DummyTransport
clone_hub_to_current_thread always clones @main_hub, discarding the thread-local hub
Hub#clone creates Hub.new(layer.client, scope.dup) — sharing the same client (and transport) as @main_hub
- Stale events on
@main_hub's DummyTransport become visible through the clone
Workaround
DummyTransport (as of sentry-ruby 6.5.0) does not implement clear, so events and envelopes arrays must be cleared directly on both the current hub and @main_hub transports:
def clear_sentry_transport(transport)
return unless transport
transport.events.clear if transport.respond_to?(:events)
transport.envelopes.clear if transport.respond_to?(:envelopes)
end
# After setup_sentry_test:
clear_sentry_transport(Sentry.get_current_client&.transport)
clear_sentry_transport(Sentry.get_main_hub&.current_client&.transport)
Both hubs must be cleared because clone_hub_to_current_thread shares the same client (and transport) instance between @main_hub and its clones via Hub#clone.
SDK Version
sentry-ruby 6.5.0
Ruby Version
3.4.9
Description
setup_sentry_testmodifies whichever hubSentry.get_current_hubreturns (the thread-local hub), but in Rails request/integration specs,Sentry::Rack::CaptureExceptionsmiddleware callsSentry.clone_hub_to_current_thread, which always clones@main_hub— not the thread-local hub thatsetup_sentry_testmodified. This overwrites the test setup entirely.After the first
setup_sentry_testcall sets aDummyTransporton@main_hub(because in the main thread, thread-local IS@main_hub),teardown_sentry_testpops the scope but the base layer withDummyTransportremains. Subsequent requests in unrelated tests then capture events to this leftoverDummyTransport, and those stale events leak into later tests that usesentry_events.Since RSpec uses random test order, this manifests as intermittent failures.
Reproduction
Output:
Expected Behavior
After
setup_sentry_test,sentry_eventsshould always be empty, even afterclone_hub_to_current_threadis called.Root Cause
setup_sentry_testcallsget_current_hubwhich returns the thread-local hub (not necessarily@main_hub)DummyTransportclone_hub_to_current_threadalways clones@main_hub, discarding the thread-local hubHub#clonecreatesHub.new(layer.client, scope.dup)— sharing the same client (and transport) as@main_hub@main_hub'sDummyTransportbecome visible through the cloneWorkaround
DummyTransport(as of sentry-ruby 6.5.0) does not implementclear, soeventsandenvelopesarrays must be cleared directly on both the current hub and@main_hubtransports:Both hubs must be cleared because
clone_hub_to_current_threadshares the same client (and transport) instance between@main_huband its clones viaHub#clone.SDK Version
sentry-ruby 6.5.0
Ruby Version
3.4.9