feat: Add integration for App Hangs V3#8302
Conversation
Introduce a layered design for hang detection: - RunLoopDelayTracker (renamed from DefaultHangTracker): raw ~25ms runloop polling via CFRunLoopObserver + semaphore - HangTracker (new middle layer): debounces delays into hangs with per-observer thresholds, notifies once on threshold crossing and once on hang end - WatchdogTerminationTrackingIntegration: now uses HangTracker.addObserver(threshold:) instead of manually filtering by duration
Gives the middle layer a distinct name so git sees the original HangTracker.swift → RunLoopDelayTracker.swift as a clean rename instead of a full rewrite.
Backport of Synchronization.Mutex API (iOS 18+) using os_unfair_lock for older deployment targets.
Document SentryMutex as preferred locking primitive for new Swift code. Expose isSessionPaused as computed property for test access after State struct migration.
Move sentrycrashbic callback registration back inside the mutex to prevent a race between start() and stop(), matching the original behavior.
This reverts commit 3bf7671.
Register callbacks outside the lock since sentrycrashbic_registerAddedCallback synchronously replays loaded images, each re-entering the lock. Unregistering stays inside the lock as it does not replay.
…observer-threshold
Add doc comments to SentryRunLoopDelay and SentryAppHang structs. Document the conditional compilation protocol/typealias pattern and the CFRunLoop function type abstractions. Fix class doc referencing ongoing=true/false instead of .started/.ended. Tighten addObserver doc, fix typos, remove redundant inline comments that restate what the code already expresses.
Clarifies which tracker the token belongs to and fixes trailing whitespace.
- Use UnsafeMutablePointer for os_unfair_lock to guarantee a stable heap-allocated address - Make SentryMutex internal (not public API) - Drop @unchecked Sendable from Storage class - Update doc comment to reference OSAllocatedUnfairLock
The threshold check used > which meant a delay exactly equal to the configured threshold would not trigger a notification.
AppHangTracker: mid-hang observer add/remove, re-add during hang, double-remove, stop/restart lifecycle, duration assertions. RunLoopDelayTracker: partial observer removal, observer added during active hang, double-remove same token.
Replace NSRecursiveLock with SentryMutex<T> for both trackers. The lock is non-recursive but safe here because handlers are called from the background queue while add/remove are main-queue-only, so re-entrance cannot occur.
The threshold check should use strict greater-than (>) to match the watchdog termination tracking behavior. A delay exactly equal to the threshold should not trigger a notification.
Replace MockAppHangTracker (which duplicated threshold logic) with a MockRunLoopDelayTracker injected into the real SentryDefaultAppHangTracker. This ensures tests exercise the actual production comparison logic instead of a mock copy.
…observer-threshold
Replace standalone SentryRunLoopDelayTrackerDependencies protocol with a typealias composing DateProviderProvider & ApplicationProvider. Update all tests to use MockDependencies instead of the removed dateProvider: init parameter.
- Rename addOngoingHangObserver to addObserver - Rename parameter id to token for observer tokens - Rename hangNotifyThreshold to pollingInterval - Replace NSRecursiveLock with SentryMutex - Reorganize class layout with MARK sections - Convert docs from comments to doc comments - Add tests for multi-observer edge cases
- Rename addOngoingHangObserver to addObserver - Rename parameter id to token for observer tokens - Rename hangNotifyThreshold to pollingInterval - Replace NSRecursiveLock with SentryMutex - Reorganize class layout with MARK sections - Convert docs from comments to doc comments - Add tests for multi-observer edge cases
…cker-per-observer-threshold
…cker-per-observer-threshold
…r-refactoring-5 # Conflicts: # Sources/Swift/Integrations/AppHangTracking/SentryRunLoopDelayTracker.swift # Sources/Swift/Integrations/WatchdogTerminations/SentryWatchdogTerminationTrackingIntegration.swift # Tests/SentryTests/Integrations/AppHangTracking/SentryRunLoopDelayTrackerTests.swift # Tests/SentryTests/Integrations/WatchdogTerminations/SentryWatchdogTerminationTrackingIntegrationTests.swift
- Clarify comment about deferred deallocation outside lock - Use inverted XCTestExpectation for thread-safe removed-observer check - Update values before fulfilling expectation to avoid race condition
…cker-per-observer-threshold
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
### Features
- Add integration for App Hangs V3 ([#8302](https://github.com/getsentry/sentry-cocoa/pull/8302))If none of the above apply, you can opt out of this check by adding |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.
| guard options.experimental.appHangs.enableV3 else { | ||
| SentrySDKLog.debug("Not enabled, skipping installation") | ||
| return nil | ||
| } |
There was a problem hiding this comment.
V3 ignores app hang disable
Medium Severity
Installation only checks experimental.appHangs.enableV3, not enableAppHangTracking or isAppHangTrackingDisabled(). With V3 on, hang events can still be captured after the user disables app hang tracking (e.g. sample --io.sentry.app-hangs.disable-tracking or appHangTimeoutInterval ≤ 0).
Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.
|
|
||
| SentrySDKLog.debug("Capturing hang event (eventId=\(event.eventId.sentryIdString))") | ||
| client.capture(event: event) | ||
| } |
There was a problem hiding this comment.
Pause API ineffective with V3
Medium Severity
When V3 is enabled, SentryHangTrackerIntegrationObjC is not installed, so SentrySDK.pauseAppHangTracking() / resumeAppHangTracking() no longer affect any integration. The V3 observer keeps calling client.capture(event:) with no pause gate like V2’s reportAppHangs flag.
Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.
|
|
||
| SentrySDKLog.debug("Installing with threshold=\(options.experimental.appHangs.threshold)s") | ||
| self.appHangTracker = dependencies.appHangTracker | ||
| observer = appHangTracker.addObserver(threshold: options.experimental.appHangs.threshold) { hang in |
There was a problem hiding this comment.
No threshold validation in V3
Low Severity
V3 installs using experimental.appHangs.threshold without requiring it to be > 0. A zero or negative threshold makes SentryDefaultAppHangTracker treat almost any run-loop delay as a hang and can produce excessive .ended capture callbacks.
Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.
| event.exceptions = [exception] | ||
|
|
||
| SentrySDKLog.debug("Capturing hang event (eventId=\(event.eventId.sentryIdString))") | ||
| client.capture(event: event) |
There was a problem hiding this comment.
Bug: The V3 hang tracking integration bypasses the Sentry Hub, causing hang events to be sent without user data, breadcrumbs, or tags from the current scope.
Severity: HIGH
Suggested Fix
Instead of calling client.capture(event: event) directly, use the HubProvider to access the current hub and capture the event through it. This can be done by calling a method like hub.capture(event: event), which will automatically apply the hub's scope to the event before sending it. This will ensure all context is included.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
Sources/Swift/Integrations/AppHangTracking/SentryHangTrackingV3Integration.swift#L55
Potential issue: The `SentryHangTrackingV3Integration` captures hang events by calling
`client.capture(event: event)` directly. This method uses a new, empty scope, bypassing
the current hub's scope. As a result, any user data, breadcrumbs, tags, or extra context
configured via `SentrySDK.configureScope` will be missing from the captured hang events.
The `HubProvider` dependency is declared but unused, suggesting that routing through the
hub to apply the scope was intended but not implemented. While `releaseName` and
`environment` are applied by the client, other crucial context is lost.
Did we get this right? 👍 / 👎 to inform future reviews.
|
|
||
| protocol SentryHangTrackingV3IntegrationProtocol {} | ||
|
|
||
| typealias SentryHangTrackingV3IntegrationDependencies = AppHangTrackerProvider & ClientProvider & ThreadInspectorProvider & DebugImageProvider & HubProvider & ExtensionDetectorProvider |
There was a problem hiding this comment.
Bug: The V3 hang tracking integration creates hang events without a stack trace, as the ThreadInspectorProvider and DebugImageProvider dependencies are unused.
Severity: CRITICAL
Suggested Fix
Use the provided dependencies.threadInspector to get the current threads with their stack traces. Assign the captured stack trace to the SentryThread object being created for the hang event. Also, use dependencies.debugImageProvider to attach debug image information, similar to how the V1 hang tracking integration functions. This will ensure hang reports contain actionable debugging information.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
Sources/Swift/Integrations/AppHangTracking/SentryHangTrackingV3Integration.swift#L5
Potential issue: The `SentryHangTrackingV3Integration` creates hang events that lack a
stack trace for the blocked main thread. Although `ThreadInspectorProvider` and
`DebugImageProvider` are included in the integration's dependencies, they are never used
to capture and attach the stack trace to the event. The resulting event contains only a
synthetic `SentryThread` with the name "main" but no stack frames, which severely limits
the utility of the hang report for debugging the cause of the block.
Did we get this right? 👍 / 👎 to inform future reviews.
📲 Install BuildsiOS
|
7a78e14 to
8c2b62a
Compare


📜 Description
Adds new integration for App Hangs V3 based on the options introduced in #8300
💡 Motivation and Context
It uses the
AppHangTrackerto detect delays in the runloop longer than the configured threshold and emits an event for that.This is a limited minimal implementation which will not include a stack trace for now, with follow-up pull request introducing either sampled stack traces or flamegraphs using profiling
Closes #8235
💚 How did you test it?
📝 Checklist
You have to check all boxes before merging:
sendDefaultPIIis enabled.