Skip to content

feat: Add integration for App Hangs V3#8302

Draft
philprime wants to merge 62 commits into
philprime/app-hangs-v3-1afrom
philprime/app-hangs-v3-1b
Draft

feat: Add integration for App Hangs V3#8302
philprime wants to merge 62 commits into
philprime/app-hangs-v3-1afrom
philprime/app-hangs-v3-1b

Conversation

@philprime

@philprime philprime commented Jun 30, 2026

Copy link
Copy Markdown
Member

📜 Description

Adds new integration for App Hangs V3 based on the options introduced in #8300

💡 Motivation and Context

It uses the AppHangTracker to 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:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.
  • If I added a new public API, I also added it to the SentryObjC wrapper.

philprime added 30 commits June 24, 2026 16:37
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.
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.
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.
Replace standalone SentryRunLoopDelayTrackerDependencies protocol with
a typealias composing DateProviderProvider & ApplicationProvider.
Update all tests to use MockDependencies instead of the removed
dateProvider: init parameter.
philprime added 17 commits June 29, 2026 17:33
- 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
…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
@philprime philprime self-assigned this Jun 30, 2026
@philprime philprime added the ready-to-merge Use this label to trigger all PR workflows label Jun 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

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 #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against 3f097d1

@philprime philprime marked this pull request as draft June 30, 2026 14:04

@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 3 potential issues.

Fix All in Cursor

❌ 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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.


SentrySDKLog.debug("Capturing hang event (eventId=\(event.eventId.sentryIdString))")
client.capture(event: event)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3f097d1. Configure here.

@philprime philprime removed the ready-to-merge Use this label to trigger all PR workflows label Jun 30, 2026
event.exceptions = [exception]

SentrySDKLog.debug("Capturing hang event (eventId=\(event.eventId.sentryIdString))")
client.capture(event: event)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@sentry

sentry Bot commented Jun 30, 2026

Copy link
Copy Markdown

📲 Install Builds

iOS

🔗 App Name App ID Version Configuration
SDK-Size io.sentry.sample.SDK-Size 9.19.0 (1) Release

⚙️ sentry-cocoa Build Distribution Settings

@philprime philprime force-pushed the philprime/app-hangs-v3-1a branch from 7a78e14 to 8c2b62a Compare June 30, 2026 14:12
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.

1 participant