Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Pause and resume AppHangTracking API #4077

Merged
merged 2 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
34 changes: 34 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,39 @@ + (void)reportFullyDisplayed
[SentrySDK.currentHub reportFullyDisplayed];
}

+ (void)pauseAppHangTracking
{
SentryANRTrackingIntegration *anrTrackingIntegration = [SentrySDK getANRTrackingIntegration];

if (anrTrackingIntegration == nil) {
return;
}
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved

[anrTrackingIntegration pauseAppHangTracking];
}

+ (void)resumeAppHangTracking
{
SentryANRTrackingIntegration *anrTrackingIntegration = [SentrySDK getANRTrackingIntegration];

if (anrTrackingIntegration == nil) {
return;
}

[anrTrackingIntegration resumeAppHangTracking];
}

+ (nullable SentryANRTrackingIntegration *)getANRTrackingIntegration
{
id<SentryIntegrationProtocol> integration =
[SentrySDK.currentHub getInstalledIntegration:[SentryANRTrackingIntegration class]];
if (integration == nil) {
return nil;
}

return (SentryANRTrackingIntegration *)integration;
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
}

+ (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 @@
}
}

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

Check warning on line 127 in Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift#L126-L127

Added lines #L126 - L127 were not covered by tests
}

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

Check warning on line 713 in Tests/SentryTests/SentrySDKTests.swift

View check run for this annotation

Codecov / codecov/patch

Tests/SentryTests/SentrySDKTests.swift#L713

Added line #L713 was not covered by tests
} 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 func uninstall() {
}
}
// swiftlint:enable file_length
Loading