Skip to content

Commit

Permalink
feat: Pause and resume AppHangTracking API (#4077)
Browse files Browse the repository at this point in the history
Add two methods pauseAppHangTracking and
resumeAppHangTracking to ignore reported AppHangs.

Fixes GH-3472
  • Loading branch information
philipphofmann authored Jun 17, 2024
1 parent e19cca3 commit a8f7a1b
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Add pause and resume AppHangTracking API (#4077). You can now pause and resume app hang tracking with `SentrySDK.pauseAppHangTracking()` and `SentrySDK.resumeAppHangTracking()`.

### Fixes

- Fix potential deadlock in app hang detection (#4063)
Expand Down
21 changes: 15 additions & 6 deletions Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
Expand Down Expand Up @@ -943,32 +943,41 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ckT-1E-GWZ">
<rect key="frame" x="0.0" y="32.5" width="152" height="28"/>
<rect key="frame" x="0.0" y="28" width="152" height="28"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<state key="normal" title="Close SDK"/>
<connections>
<action selector="close:" destination="VqS-l1-kwe" eventType="touchUpInside" id="UwB-2M-pCr"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rpD-Rf-xbz">
<rect key="frame" x="0.0" y="65.5" width="152" height="28"/>
<rect key="frame" x="0.0" y="56" width="152" height="28"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<state key="normal" title="ANR fully blocking"/>
<connections>
<action selector="anrFullyBlocking:" destination="VqS-l1-kwe" eventType="touchUpInside" id="PLh-oH-8oF"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2e4-48-rLl">
<rect key="frame" x="0.0" y="98" width="152" height="28"/>
<rect key="frame" x="0.0" y="84" width="152" height="28"/>
<accessibility key="accessibilityConfiguration" identifier="anrFillingRunLoopExtra"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<state key="normal" title="ANR filling run loop"/>
<connections>
<action selector="anrFillingRunLoop:" destination="VqS-l1-kwe" eventType="touchUpInside" id="ON6-DV-3Tz"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6Jr-19-VhC">
<rect key="frame" x="0.0" y="112" width="152" height="28"/>
<accessibility key="accessibilityConfiguration" identifier="anrFillingRunLoopExtra"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<state key="normal" title="Pasteboard Contents"/>
<connections>
<action selector="getPasteBoardString:" destination="VqS-l1-kwe" eventType="touchUpInside" id="RXr-u0-uBD"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F0l-xf-cQd">
<rect key="frame" x="0.0" y="130.5" width="152" height="28"/>
<rect key="frame" x="0.0" y="140" width="152" height="28"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="Start 100 threads"/>
Expand All @@ -977,7 +986,7 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Evt-B9-zEC">
<rect key="frame" x="0.0" y="163.5" width="152" height="28"/>
<rect key="frame" x="0.0" y="168" width="152" height="28"/>
<accessibility key="accessibilityConfiguration" identifier="breadcrumbInfoButton"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
Expand Down
15 changes: 15 additions & 0 deletions Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ class ExtraViewController: UIViewController {
triggerANRFillingRunLoop(button: self.anrFillingRunLoopButton)
}

@IBAction func getPasteBoardString(_ sender: Any) {
SentrySDK.pauseAppHangTracking()

// Getting the pasteboard string asks for permission
// and the SDK would detect an ANR if we don't pause it.
// Make sure to copy something into the pasteboard, cause
// iOS only opens the system permission dialog if you do.

if let clipboard = UIPasteboard.general.string {
SentrySDK.capture(message: clipboard)
}

SentrySDK.resumeAppHangTracking()
}

@IBAction func start100Threads(_ sender: UIButton) {
highlightButton(sender)
for _ in 0..<100 {
Expand Down
13 changes: 13 additions & 0 deletions Sources/Sentry/Public/SentrySDK.h
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,19 @@ SENTRY_NO_INIT
*/
+ (void)reportFullyDisplayed;

/**
* Pauses sending detected app hangs to Sentry.
*
* @discussion This method doesn't close the detection of app hangs. Instead, the app hang detection
* will ignore detected app hangs until you call @c resumeAppHangTracking.
*/
+ (void)pauseAppHangTracking;

/**
* Resumes sending detected app hangs to Sentry.
*/
+ (void)resumeAppHangTracking;

/**
* Waits synchronously for the SDK to flush out all queued and cached items for up to the specified
* timeout in seconds. If there is no internet connection, the function returns immediately. The SDK
Expand Down
17 changes: 17 additions & 0 deletions Sources/Sentry/SentryANRTrackingIntegration.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

@property (nonatomic, strong) SentryANRTracker *tracker;
@property (nonatomic, strong) SentryOptions *options;
@property (atomic, assign) BOOL reportAppHangs;

@end

Expand All @@ -45,6 +46,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options

[self.tracker addListener:self];
self.options = options;
self.reportAppHangs = YES;

return YES;
}
Expand All @@ -54,6 +56,16 @@ - (SentryIntegrationOption)integrationOptions
return kIntegrationOptionEnableAppHangTracking | kIntegrationOptionDebuggerNotAttached;
}

- (void)pauseAppHangTracking
{
self.reportAppHangs = NO;
}

- (void)resumeAppHangTracking
{
self.reportAppHangs = YES;
}

- (void)uninstall
{
[self.tracker removeListener:self];
Expand All @@ -66,6 +78,11 @@ - (void)dealloc

- (void)anrDetected
{
if (self.reportAppHangs == NO) {
SENTRY_LOG_DEBUG(@"AppHangTracking paused. Ignoring reported app hang.")
return;
}

#if SENTRY_HAS_UIKIT
// If the app is not active, the main thread may be blocked or too busy.
// Since there is no UI for the user to interact, there is no need to report app hang.
Expand Down
12 changes: 12 additions & 0 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,18 @@ - (BOOL)isIntegrationInstalled:(Class)integrationClass
}
}

- (nullable id<SentryIntegrationProtocol>)getInstalledIntegration:(Class)integrationClass
{
@synchronized(_integrationsLock) {
for (id<SentryIntegrationProtocol> item in _installedIntegrations) {
if ([item isKindOfClass:integrationClass]) {
return item;
}
}
return nil;
}
}

- (BOOL)hasIntegration:(NSString *)integrationName
{
// installedIntegrations and installedIntegrationNames share the same lock.
Expand Down
19 changes: 19 additions & 0 deletions Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "SentrySDK.h"
#import "PrivateSentrySDKOnly.h"
#import "SentryANRTrackingIntegration.h"
#import "SentryAppStartMeasurement.h"
#import "SentryAppStateManager.h"
#import "SentryBinaryImageCache.h"
Expand Down Expand Up @@ -471,6 +472,24 @@ + (void)reportFullyDisplayed
[SentrySDK.currentHub reportFullyDisplayed];
}

+ (void)pauseAppHangTracking
{
SentryANRTrackingIntegration *anrTrackingIntegration
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
getInstalledIntegration:[SentryANRTrackingIntegration class]];

[anrTrackingIntegration pauseAppHangTracking];
}

+ (void)resumeAppHangTracking
{
SentryANRTrackingIntegration *anrTrackingIntegration
= (SentryANRTrackingIntegration *)[SentrySDK.currentHub
getInstalledIntegration:[SentryANRTrackingIntegration class]];

[anrTrackingIntegration resumeAppHangTracking];
}

+ (void)flush:(NSTimeInterval)timeout
{
[SentrySDK.currentHub flush:timeout];
Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/include/SentryANRTrackingIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ static NSString *const SentryANRExceptionType = @"App Hanging";
@interface SentryANRTrackingIntegration
: SentryBaseIntegration <SentryIntegrationProtocol, SentryANRTrackerDelegate>

- (void)pauseAppHangTracking;
- (void)resumeAppHangTracking;

@end

NS_ASSUME_NONNULL_END
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryHub+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ SentryHub ()

- (void)captureEnvelope:(SentryEnvelope *)envelope;

- (nullable id<SentryIntegrationProtocol>)getInstalledIntegration:(Class)integrationClass;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,47 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase {
}
}

func testANRDetected_DetectingPaused_NoEventCaptured() {
givenInitializedTracker()
setUpThreadInspector()
sut.pauseAppHangTracking()

Dynamic(sut).anrDetected()

assertNoEventCaptured()
}

func testANRDetected_DetectingPausedResumed_EventCaptured() {
givenInitializedTracker()
setUpThreadInspector()
sut.pauseAppHangTracking()
sut.resumeAppHangTracking()

Dynamic(sut).anrDetected()

assertEventWithScopeCaptured { event, _, _ in
XCTAssertNotNil(event)
guard let ex = event?.exceptions?.first else {
XCTFail("ANR Exception not found")
return
}

expect(ex.mechanism?.type) == "AppHang"
}
}

func testCallPauseResumeOnMultipleThreads_DoesNotCrash() {
givenInitializedTracker()

testConcurrentModifications(asyncWorkItems: 100, writeLoopCount: 10, writeWork: {_ in
self.sut.pauseAppHangTracking()
Dynamic(self.sut).anrDetected()
}, readWork: {
self.sut.resumeAppHangTracking()
Dynamic(self.sut).anrDetected()
})
}

func testANRDetected_ButNoThreads_EventNotCaptured() {
givenInitializedTracker()
setUpThreadInspector(addThreads: false)
Expand Down
18 changes: 18 additions & 0 deletions Tests/SentryTests/SentryHubTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@ class SentryHubTests: XCTestCase {
let integrationName = "Integration\(i)\(j)"
sut.addInstalledIntegration(EmptyIntegration(), name: integrationName)
XCTAssertTrue(sut.hasIntegration(integrationName))
XCTAssertNotNil(sut.getInstalledIntegration(EmptyIntegration.self))
}
group.leave()
}
Expand Down Expand Up @@ -940,6 +941,7 @@ class SentryHubTests: XCTestCase {
sut.addInstalledIntegration(EmptyIntegration(), name: integrationName)
sut.hasIntegration(integrationName)
sut.isIntegrationInstalled(EmptyIntegration.self)
sut.getInstalledIntegration(EmptyIntegration.self)
}
XCTAssertLessThanOrEqual(0, sut.installedIntegrations().count)
sut.installedIntegrations().forEach { XCTAssertNotNil($0) }
Expand All @@ -955,6 +957,22 @@ class SentryHubTests: XCTestCase {
group.wait()
}

func testGetInstalledIntegration() {
let integration = EmptyIntegration()
sut.addInstalledIntegration(integration, name: "EmptyIntegration")

let installedIntegration = sut.getInstalledIntegration(EmptyIntegration.self)

XCTAssert(integration === installedIntegration)
}

func testGetInstalledIntegration_ReturnsNilIfNotFound() {
let integration = EmptyIntegration()
sut.addInstalledIntegration(integration, name: "EmptyIntegration")

XCTAssertNil(sut.getInstalledIntegration(SentryANRTrackingIntegration.self))
}

func testEventContainsOnlyHandledErrors() {
let sut = fixture.getSut()
XCTAssertFalse(sut.eventContainsOnlyHandledErrors(["exception":
Expand Down
41 changes: 41 additions & 0 deletions Tests/SentryTests/SentrySDKTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Nimble
import SentryTestUtils
import XCTest

// swiftlint:disable file_length
class SentrySDKTests: XCTestCase {

private static let dsnAsString = TestConstants.dsnAsString(username: "SentrySDKTests")
Expand Down Expand Up @@ -689,6 +690,45 @@ class SentrySDKTests: XCTestCase {
XCTAssertFalse(deviceWrapper.started)
}
#endif

func testResumeAndPauseAppHangTracking() {
SentrySDK.start { options in
options.dsn = SentrySDKTests.dsnAsString
options.setIntegrations([SentryANRTrackingIntegration.self])
}

let client = fixture.client
SentrySDK.currentHub().bindClient(client)

let anrTrackingIntegration = SentrySDK.currentHub().getInstalledIntegration(SentryANRTrackingIntegration.self)

SentrySDK.pauseAppHangTracking()
Dynamic(anrTrackingIntegration).anrDetected()
XCTAssertEqual(0, client.captureEventWithScopeInvocations.count)

SentrySDK.resumeAppHangTracking()
Dynamic(anrTrackingIntegration).anrDetected()

if SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced() {
XCTAssertEqual(0, client.captureEventWithScopeInvocations.count)
} else {
XCTAssertEqual(1, client.captureEventWithScopeInvocations.count)
}
}

func testResumeAndPauseAppHangTracking_ANRTrackingNotInstalled() {
SentrySDK.start { options in
options.dsn = SentrySDKTests.dsnAsString
options.removeAllIntegrations()
}

let client = fixture.client
SentrySDK.currentHub().bindClient(client)

// Both invocations do nothing
SentrySDK.pauseAppHangTracking()
SentrySDK.resumeAppHangTracking()
}

func testClose_SetsClientToNil() {
SentrySDK.start { options in
Expand Down Expand Up @@ -980,3 +1020,4 @@ public class MainThreadTestIntegration: NSObject, SentryIntegrationProtocol {
public func uninstall() {
}
}
// swiftlint:enable file_length

0 comments on commit a8f7a1b

Please sign in to comment.