What React Native libraries do you use?
Expo (mobile only), Expo Router, Hermes, RN New Architecture
Are you using sentry.io or on-premise?
sentry.io (SaS)
Are you using any other error monitoring solution alongside Sentry?
No
@sentry/react-native SDK Version
7.11.0, also reproduced on 8.28.0 (the code below is unchanged on main at 0d58e97)
How does your development environment look like?
Expo SDK 57.0.26, React Native 0.86.3, Jest 29.7.0 (jest-expo 57.0.5), Node 24.21.0, macOS
Sentry.init()
Not needed: importing the SDK is enough to reproduce.
Steps to Reproduce
In a Jest project (jest-expo preset), create a test file containing only:
import '@sentry/react-native';
test('noop', () => {});
Run it with at least two test files or projects, so that Jest uses workers.
Expected Result
Importing the SDK starts no timer, and Jest exits cleanly.
Actual Result
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
With a single test file (no workers), Jest prints Jest did not exit one second after the test run has completed. instead.
On 8.28.0 without Jest, importing dist/js/tracing/timeToDisplayFallback.js in plain Node keeps the process alive for ~5,000 ms.
Cause. timeToDisplayFallback.ts creates an AsyncExpiringMap at module scope. Its constructor calls startCleanup(), which starts a 5-second setInterval without .unref(). The interval stops at its first run, but the test process has already finished by then.
Related bug: cleanup never restarts. stopCleanup() clears the interval but does not reset _cleanupInterval to undefined. set() restarts cleanup only when _cleanupInterval is falsy, so after the first cleanup of an empty map, entries added later are never removed by the interval. Only an explicit get(), has() or pop() made after expiry removes them.
Verified against the 7.11.0 and 8.28.0 builds:
const map = new AsyncExpiringMap({ cleanupInterval: 50, ttl: 10 });
await sleep(120); // first cleanup ran on the empty map and stopped the interval
map.set('span-1', 123);
await sleep(300);
map._map.size; // 1: the entry expired ~290 ms ago and is still there
Suggested fix.
- Don't start the interval in the constructor.
set() already starts it on demand.
- Reset
_cleanupInterval = undefined in stopCleanup() and clear().
- Optionally, call
.unref?.() on the interval, so it never keeps a Node process alive.
What React Native libraries do you use?
Expo (mobile only), Expo Router, Hermes, RN New Architecture
Are you using sentry.io or on-premise?
sentry.io (SaS)
Are you using any other error monitoring solution alongside Sentry?
No
@sentry/react-native SDK Version
7.11.0, also reproduced on 8.28.0 (the code below is unchanged on
mainat 0d58e97)How does your development environment look like?
Sentry.init()
Not needed: importing the SDK is enough to reproduce.
Steps to Reproduce
In a Jest project (jest-expo preset), create a test file containing only:
Run it with at least two test files or projects, so that Jest uses workers.
Expected Result
Importing the SDK starts no timer, and Jest exits cleanly.
Actual Result
With a single test file (no workers), Jest prints
Jest did not exit one second after the test run has completed.instead.On 8.28.0 without Jest, importing
dist/js/tracing/timeToDisplayFallback.jsin plain Node keeps the process alive for ~5,000 ms.Cause.
timeToDisplayFallback.tscreates anAsyncExpiringMapat module scope. Its constructor callsstartCleanup(), which starts a 5-secondsetIntervalwithout.unref(). The interval stops at its first run, but the test process has already finished by then.Related bug: cleanup never restarts.
stopCleanup()clears the interval but does not reset_cleanupIntervaltoundefined.set()restarts cleanup only when_cleanupIntervalis falsy, so after the first cleanup of an empty map, entries added later are never removed by the interval. Only an explicitget(),has()orpop()made after expiry removes them.Verified against the 7.11.0 and 8.28.0 builds:
Suggested fix.
set()already starts it on demand._cleanupInterval = undefinedinstopCleanup()andclear()..unref?.()on the interval, so it never keeps a Node process alive.