Skip to content

fix: dispatch UIApplication.applicationState read to main thread on SDK start#8047

Closed
tsushanth wants to merge 3 commits into
getsentry:mainfrom
tsushanth:fix/sdk-start-application-state-main-thread
Closed

fix: dispatch UIApplication.applicationState read to main thread on SDK start#8047
tsushanth wants to merge 3 commits into
getsentry:mainfrom
tsushanth:fix/sdk-start-application-state-main-thread

Conversation

@tsushanth

Copy link
Copy Markdown
Contributor

Summary

SentryThreadsafeApplication.init reads UIApplication.applicationState synchronously from the calling thread. When SentrySDK.start runs on a background thread or a non-main actor (a documented but discouraged pattern — see #6591), Xcode emits a runtime warning on every launch:

-[UIApplication applicationState] must be used from main thread only

The issue author's use case is a non-main-isolated error-reporting service that wraps SentrySDK.start. A maintainer agreed it's worth fixing.

Root cause

ThreadSafeApplication.swift:13–14:

if let application = applicationProvider() {
    _internalState = application.unsafeApplicationState
}

unsafeApplicationState is a direct read of UIApplication.applicationState, which UIKit gates on Thread.isMainThread and reports via the runtime issue checker.

Fix

Hop to the main thread for that single property read using the existing Dependencies.dispatchQueueWrapper.dispatchSyncOnMainQueue(_:timeout:) helper — the same pattern already used elsewhere in this file (internal_getWindows, internal_getActiveWindowSize, internal_relevantViewControllersNames) with the same 10 ms timeout. On timeout we fall back to the existing .active default that the null-application branch uses, and the notification observers registered immediately afterward correct the state on the next foreground/background transition. The behavior when start is called on the main thread is unchanged.

 if let application = applicationProvider() {
-    _internalState = application.unsafeApplicationState
+    if Thread.isMainThread {
+        _internalState = application.unsafeApplicationState
+    } else {
+        var stateOnMain: UIApplication.State = .active
+        Dependencies.dispatchQueueWrapper.dispatchSyncOnMainQueue({
+            stateOnMain = application.unsafeApplicationState
+        }, timeout: 0.01)
+        _internalState = stateOnMain
+    }
 } else {
     SentrySDKLog.warning("Application is null in SentryThreadsafeApplication")
     _internalState = .active
 }

Test plan

  • Added testInit_OffMainThread_ReadsApplicationStateOnMainThread to SentryThreadsafeApplicationTests. The test dispatches init onto a background queue and asserts (via a new unsafeApplicationStateReadOnMainThread tracker on TestSentryUIApplication) that the underlying property access landed on the main thread, and that the value is correctly propagated to _internalState.
  • xcodebuild build on the Sentry scheme (iOS Simulator, iPhone 17 Pro, iOS 26.4.1) — BUILD SUCCEEDED with the production-code change in place.
  • Local xcodebuild test couldn't run the new test in this PR's checkout — SentryTestUtils/Sources/ClearTestState.swift:52 calls SentryAppStartMeasurementProvider.reset() which Swift can't resolve (confirmed pre-existing on main without my changes). This appears to be a separate test-infra build issue; deferring to CI for the actual run.

Changelog

Added an entry under ## Unreleased per the convention used in the in-flight #8041 PR. (PR number placeholder #PRPENDING will need updating once this PR is assigned a number — happy to amend.)

Notes for reviewer

  • TestSentryUIApplication already had a calledOnMainThread tracker for windows; the new unsafeApplicationStateReadOnMainThread tracker follows the same pattern.
  • The 10 ms timeout is identical to the existing usages in this file. If reviewers prefer no timeout (the no-timeout dispatchSyncOnMainQueue(block:) overload also exists), happy to switch — the timeout guards against the pathological case where the caller holds a lock that main is waiting on.

Closes #6591

@tsushanth

Copy link
Copy Markdown
Contributor Author

Friendly bump — GitHub still shows "Conflicting" but a fresh local merge against current main (HEAD at 7099e93) goes through clean, no conflicting files. Looks like the merge-state check is stale. The PR is otherwise ready for review whenever someone has a window. Thanks

SentryThreadsafeApplication.init reads UIApplication.applicationState
synchronously from the calling thread. When SentrySDK.start runs on a
background thread or a non-main actor (a documented but discouraged
pattern), Xcode emits a `-[UIApplication applicationState] must be used
from main thread only` runtime warning on every launch.

Hop to the main thread for that single property read. The same
dispatchSyncOnMainQueue helper is used by other readers in the file
(internal_getWindows, internal_getActiveWindowSize) with the same 10 ms
timeout — on timeout we fall back to the existing `.active` default and
the notification observers correct the state on the next foreground or
background transition. The current behavior when start is called on
the main thread is unchanged.

Closes getsentry#6591
@tsushanth
tsushanth force-pushed the fix/sdk-start-application-state-main-thread branch from c843354 to 9f8b292 Compare June 14, 2026 16:41

@NinjaLikesCheez NinjaLikesCheez left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your contribution! I have one small nitpick which is to correct a comment - please request a review again when you've addressed it. Thanks!

Comment on lines +21 to +22
// the value safely. The 10ms timeout matches the pattern used elsewhere
// in this file (e.g. internal_getWindows) — if main is contended we fall

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please correct this comment to remove 'in this file'.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.232%. Comparing base (7099e93) to head (9f8b292).
⚠️ Report is 16 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #8047       +/-   ##
=============================================
- Coverage   87.406%   87.232%   -0.174%     
=============================================
  Files          559       559               
  Lines        32167     32183       +16     
  Branches     13157     13134       -23     
=============================================
- Hits         28116     28074       -42     
- Misses        4004      4060       +56     
- Partials        47        49        +2     
Files with missing lines Coverage Δ
Sources/Swift/Helper/ThreadSafeApplication.swift 100.000% <100.000%> (ø)

... and 11 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7099e93...9f8b292. Read the comment docs.

@philprime

Copy link
Copy Markdown
Member

@tsushanth are you still driving this pull request?

@tsushanth

Copy link
Copy Markdown
Contributor Author

Yes, still on it @philprime! Will fix the comment nitpick from @NinjaLikesCheez and re-request review.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 43e4aa4. Configure here.

Dependencies.dispatchQueueWrapper.dispatchSyncOnMainQueue({
stateOnMain = application.unsafeApplicationState
}, timeout: 0.01)
_internalState = stateOnMain

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Late main read ignored after timeout

Medium Severity

When init runs off the main thread, _internalState is assigned from a local default immediately after dispatchSyncOnMainQueue returns, but that helper uses an async main block with a 10 ms wait. If the wait times out while the block still runs later, the real unsafeApplicationState is written only to the local variable, so _internalState can stay at the default .active until a lifecycle notification fires—and not at all if the app was already backgrounded.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 43e4aa4. Configure here.

@NinjaLikesCheez

Copy link
Copy Markdown
Member

Closing this out for now, after another look there's some duplication of logic so I think it's better we discuss a plan in the linked issue first. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SentrySDK start method causes runtime warning when not being used on the main thread

3 participants