Skip to content

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

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

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

Conversation

@tsushanth

Copy link
Copy Markdown

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
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.

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

2 participants