Skip to content

Commit 187edbf

Browse files
feat: AppHangsV2 (#4379)
Expose the AppHangsV2 algorithm with the options enableAppHangTrackingV2 and enableReportNonFullyBlockingAppHangs. Fixes GH-3492
1 parent 2d2068d commit 187edbf

17 files changed

+317
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Added breadcrumb.origin private field (#4358)
88
- Custom redact modifier for SwiftUI (#4362)
9+
- AppHangV2 detection (#4379) Add a new algorithm for detecting app hangs that differentiates between fully blocking and non-fully blocking app hangs. Read more in-depth in our [docs](https://docs.sentry.io/platforms/apple/guides/ios/configuration/app-hangs/#app-hangs-v2).
910
- Add support for arm64e (#3398)
1011
- Add mergeable libraries support to dynamic libraries (#4381)
1112

Sentry.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; };
8181
621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; };
8282
621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; };
83+
6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6221BBC92CAA932100C627CA /* SentryANRType.swift */; };
8384
62262B862BA1C46D004DA3DD /* SentryStatsdClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */; };
8485
62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 62262B872BA1C490004DA3DD /* SentryStatsdClient.m */; };
8586
62262B8B2BA1C4C1004DA3DD /* EncodeMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */; };
@@ -1088,6 +1089,7 @@
10881089
621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = "<group>"; };
10891090
621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = "<group>"; };
10901091
621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = "<group>"; };
1092+
6221BBC92CAA932100C627CA /* SentryANRType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRType.swift; sourceTree = "<group>"; };
10911093
62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStatsdClient.h; path = include/SentryStatsdClient.h; sourceTree = "<group>"; };
10921094
62262B872BA1C490004DA3DD /* SentryStatsdClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStatsdClient.m; sourceTree = "<group>"; };
10931095
62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodeMetrics.swift; sourceTree = "<group>"; };
@@ -2199,6 +2201,7 @@
21992201
children = (
22002202
6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */,
22012203
62FC18AE2C9D5FAC008803CD /* SentryANRTracker.swift */,
2204+
6221BBC92CAA932100C627CA /* SentryANRType.swift */,
22022205
);
22032206
path = ANR;
22042207
sourceTree = "<group>";
@@ -4634,6 +4637,7 @@
46344637
D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */,
46354638
63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */,
46364639
63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */,
4640+
6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */,
46374641
7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */,
46384642
D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */,
46394643
63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */,

Sources/Sentry/Public/SentryOptions.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,40 @@ NS_SWIFT_NAME(Options)
559559
*/
560560
@property (nonatomic, assign) BOOL enableAppHangTracking;
561561

562+
#if SENTRY_UIKIT_AVAILABLE
563+
564+
/**
565+
* AppHangTrackingV2 can differentiate between fully-blocking and non-fully blocking app hangs.
566+
* fully-blocking app hang is when the main thread is stuck completely, and the app can't render a
567+
* single frame. A non-fully-blocking app hang is when the app appears stuck to the user but can
568+
still
569+
* render a few frames. Fully-blocking app hangs are more actionable because the stacktrace shows
570+
the
571+
* exact blocking location on the main thread. As the main thread isn't completely blocked,
572+
* non-fully-blocking app hangs can have a stacktrace that doesn't highlight the exact blocking
573+
* location.
574+
*
575+
* You can use @c enableReportNonFullyBlockingAppHangs to ignore non-fully-blocking app hangs.
576+
*
577+
* @note This flag wins over enableAppHangTracking. When enabling both enableAppHangTracking and
578+
enableAppHangTrackingV2, the SDK only enables enableAppHangTrackingV2 and disables
579+
enableAppHangTracking.
580+
*
581+
* @warning This is an experimental feature and may still have bugs.
582+
*/
583+
@property (nonatomic, assign) BOOL enableAppHangTrackingV2;
584+
585+
/**
586+
* When enabled the SDK reports non-fully-blocking app hangs. A non-fully-blocking app hang is when
587+
* the app appears stuck to the user but can still render a few frames. For more information see @c
588+
* enableAppHangTrackingV2.
589+
*
590+
* @note The default is @c YES. This feature only works when @c enableAppHangTrackingV2 is enabled.
591+
*/
592+
@property (nonatomic, assign) BOOL enableReportNonFullyBlockingAppHangs;
593+
594+
#endif // SENTRY_UIKIT_AVAILABLE
595+
562596
/**
563597
* The minimum amount of time an app should be unresponsive to be classified as an App Hanging.
564598
* @note The actual amount may be a little longer.

Sources/Sentry/SentryANRTrackingIntegration.m

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ @interface SentryANRTrackingIntegration ()
2929
@property (nonatomic, strong) id<SentryANRTracker> tracker;
3030
@property (nonatomic, strong) SentryOptions *options;
3131
@property (atomic, assign) BOOL reportAppHangs;
32+
@property (atomic, assign) BOOL enableReportNonFullyBlockingAppHangs;
3233

3334
@end
3435

@@ -40,9 +41,15 @@ - (BOOL)installWithOptions:(SentryOptions *)options
4041
return NO;
4142
}
4243

44+
#if SENTRY_HAS_UIKIT
45+
self.tracker =
46+
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval
47+
isV2Enabled:options.enableAppHangTrackingV2];
48+
#else
4349
self.tracker =
44-
[SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval];
50+
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval];
4551

52+
#endif // SENTRY_HAS_UIKIT
4653
[self.tracker addListener:self];
4754
self.options = options;
4855
self.reportAppHangs = YES;
@@ -83,6 +90,12 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
8390
}
8491

8592
#if SENTRY_HAS_UIKIT
93+
if (type == SentryANRTypeNonFullyBlocking
94+
&& !self.options.enableReportNonFullyBlockingAppHangs) {
95+
SENTRY_LOG_DEBUG(@"Ignoring non fully blocking app hang.")
96+
return;
97+
}
98+
8699
// If the app is not active, the main thread may be blocked or too busy.
87100
// Since there is no UI for the user to interact, there is no need to report app hang.
88101
if (SentryDependencyContainer.sharedInstance.application.applicationState
@@ -103,8 +116,10 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
103116
NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.",
104117
(long)(self.options.appHangTimeoutInterval * 1000)];
105118
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
106-
SentryException *sentryException =
107-
[[SentryException alloc] initWithValue:message type:SentryANRExceptionType];
119+
120+
NSString *exceptionType = [SentryAppHangTypeMapper getExceptionTypeWithAnrType:type];
121+
SentryException *sentryException = [[SentryException alloc] initWithValue:message
122+
type:exceptionType];
108123

109124
sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"];
110125
sentryException.stacktrace = [threads[0] stacktrace];

Sources/Sentry/SentryBaseIntegration.m

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,17 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options
7878
#endif
7979

8080
if (integrationOptions & kIntegrationOptionEnableAppHangTracking) {
81-
if (!options.enableAppHangTracking) {
82-
[self logWithOptionName:@"enableAppHangTracking"];
83-
return NO;
84-
}
85-
86-
if (options.appHangTimeoutInterval == 0) {
87-
[self logWithReason:@"because appHangTimeoutInterval is 0"];
81+
#if SENTRY_HAS_UIKIT
82+
if (!options.enableAppHangTracking && !options.enableAppHangTrackingV2) {
83+
[self logWithOptionName:@"enableAppHangTracking && enableAppHangTrackingV2"];
8884
return NO;
8985
}
90-
}
91-
92-
if (integrationOptions & kIntegrationOptionEnableAppHangTrackingV2) {
93-
if (!options.enableAppHangTrackingV2) {
94-
[self logWithOptionName:@"enableAppHangTrackingV2"];
86+
#else
87+
if (!options.enableAppHangTracking) {
88+
[self logWithOptionName:@"enableAppHangTracking"];
9589
return NO;
9690
}
91+
#endif // SENTRY_HAS_UIKIT
9792

9893
if (options.appHangTimeoutInterval == 0) {
9994
[self logWithReason:@"because appHangTimeoutInterval is 0"];

Sources/Sentry/SentryDependencyContainer.m

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#import "SentryANRTrackerV1.h"
2-
#import "SentryANRTrackerV2.h"
2+
33
#import "SentryBinaryImageCache.h"
44
#import "SentryDispatchFactory.h"
55
#import "SentryDispatchQueueWrapper.h"
@@ -32,6 +32,7 @@
3232
#import <SentryTracer.h>
3333

3434
#if SENTRY_HAS_UIKIT
35+
# import "SentryANRTrackerV2.h"
3536
# import "SentryFramesTracker.h"
3637
# import "SentryUIApplication.h"
3738
# import <SentryScreenshot.h>
@@ -46,6 +47,12 @@
4647
# import "SentryReachability.h"
4748
#endif // !TARGET_OS_WATCH
4849

50+
@interface SentryDependencyContainer ()
51+
52+
@property (nonatomic, strong) id<SentryANRTracker> anrTracker;
53+
54+
@end
55+
4956
@implementation SentryDependencyContainer
5057

5158
static SentryDependencyContainer *instance;
@@ -301,32 +308,6 @@ - (SentryFramesTracker *)framesTracker SENTRY_DISABLE_THREAD_SANITIZER(
301308
# endif // SENTRY_HAS_UIKIT
302309
}
303310

304-
- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout
305-
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
306-
{
307-
# if SENTRY_HAS_UIKIT
308-
if (_anrTrackerV2 == nil) {
309-
@synchronized(sentryDependencyContainerLock) {
310-
if (_anrTrackerV2 == nil) {
311-
_anrTrackerV2 =
312-
[[SentryANRTrackerV2 alloc] initWithTimeoutInterval:timeout
313-
crashWrapper:self.crashWrapper
314-
dispatchQueueWrapper:self.dispatchQueueWrapper
315-
threadWrapper:self.threadWrapper
316-
framesTracker:self.framesTracker];
317-
}
318-
}
319-
}
320-
321-
return _anrTrackerV2;
322-
# else
323-
SENTRY_LOG_DEBUG(
324-
@"SentryDependencyContainer.getANRTrackerV2 only works with UIKit enabled. Ensure you're "
325-
@"using the right configuration of Sentry that links UIKit.");
326-
return nil;
327-
# endif // SENTRY_HAS_UIKIT
328-
}
329-
330311
- (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER(
331312
"double-checked lock produce false alarms")
332313
{
@@ -348,13 +329,13 @@ - (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER(
348329
}
349330
#endif // SENTRY_UIKIT_AVAILABLE
350331

351-
- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout
332+
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout
352333
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
353334
{
354-
if (_anrTrackerV1 == nil) {
335+
if (_anrTracker == nil) {
355336
@synchronized(sentryDependencyContainerLock) {
356-
if (_anrTrackerV1 == nil) {
357-
_anrTrackerV1 =
337+
if (_anrTracker == nil) {
338+
_anrTracker =
358339
[[SentryANRTrackerV1 alloc] initWithTimeoutInterval:timeout
359340
crashWrapper:self.crashWrapper
360341
dispatchQueueWrapper:self.dispatchQueueWrapper
@@ -363,8 +344,34 @@ - (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout
363344
}
364345
}
365346

366-
return _anrTrackerV1;
347+
return _anrTracker;
348+
}
349+
350+
#if SENTRY_HAS_UIKIT
351+
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout
352+
isV2Enabled:(BOOL)isV2Enabled
353+
SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms")
354+
{
355+
if (isV2Enabled) {
356+
if (_anrTracker == nil) {
357+
@synchronized(sentryDependencyContainerLock) {
358+
if (_anrTracker == nil) {
359+
_anrTracker = [[SentryANRTrackerV2 alloc]
360+
initWithTimeoutInterval:timeout
361+
crashWrapper:self.crashWrapper
362+
dispatchQueueWrapper:self.dispatchQueueWrapper
363+
threadWrapper:self.threadWrapper
364+
framesTracker:self.framesTracker];
365+
}
366+
}
367+
}
368+
369+
return _anrTracker;
370+
} else {
371+
return [self getANRTracker:timeout];
372+
}
367373
}
374+
#endif // SENTRY_HAS_UIKIT
368375

369376
- (SentryNSProcessInfoWrapper *)processInfoWrapper SENTRY_DISABLE_THREAD_SANITIZER(
370377
"double-checked lock produce false alarms")

Sources/Sentry/SentryEvent.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ - (BOOL)isMetricKitEvent
204204
- (BOOL)isAppHangEvent
205205
{
206206
return self.exceptions.count == 1 &&
207-
[self.exceptions.firstObject.type isEqualToString:SentryANRExceptionType];
207+
[SentryAppHangTypeMapper
208+
isExceptionTypeAppHangWithExceptionType:self.exceptions.firstObject.type];
208209
}
209210

210211
@end

Sources/Sentry/SentryOptions.m

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,11 @@ - (instancetype)init
118118
self.enableUserInteractionTracing = YES;
119119
self.idleTimeout = SentryTracerDefaultTimeout;
120120
self.enablePreWarmedAppStartTracing = NO;
121+
self.enableAppHangTrackingV2 = NO;
122+
self.enableReportNonFullyBlockingAppHangs = YES;
121123
#endif // SENTRY_HAS_UIKIT
122124
self.enableAppHangTracking = YES;
123125
self.appHangTimeoutInterval = 2.0;
124-
self.enableAppHangTrackingV2 = NO;
125126
self.enableAutoBreadcrumbTracking = YES;
126127
self.enableNetworkTracking = YES;
127128
self.enableFileIOTracing = YES;
@@ -435,6 +436,12 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
435436
[self setBool:options[@"enablePreWarmedAppStartTracing"]
436437
block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }];
437438

439+
[self setBool:options[@"enableAppHangTrackingV2"]
440+
block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }];
441+
442+
[self setBool:options[@"enableReportNonFullyBlockingAppHangs"]
443+
block:^(BOOL value) { self->_enableReportNonFullyBlockingAppHangs = value; }];
444+
438445
#endif // SENTRY_HAS_UIKIT
439446

440447
[self setBool:options[@"enableAppHangTracking"]
@@ -444,9 +451,6 @@ - (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
444451
self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue];
445452
}
446453

447-
[self setBool:options[@"enableAppHangTrackingV2"]
448-
block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }];
449-
450454
[self setBool:options[@"enableNetworkTracking"]
451455
block:^(BOOL value) { self->_enableNetworkTracking = value; }];
452456

Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options
7474
[self.tracker start];
7575

7676
self.anrTracker =
77-
[SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval];
77+
[SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval
78+
isV2Enabled:options.enableAppHangTrackingV2];
7879
[self.anrTracker addListener:self];
7980

8081
self.appStateManager = appStateManager;

Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#import "SentryDefines.h"
22

3-
@class SentryANRTrackerV1;
4-
@class SentryANRTrackerV2;
3+
@protocol SentryANRTracker;
54
@class SentryAppStateManager;
65
@class SentryBinaryImageCache;
76
@class SentryCrash;
@@ -63,8 +62,6 @@ SENTRY_NO_INIT
6362
@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper;
6463
@property (nonatomic, strong) SentryNSNotificationCenterWrapper *notificationCenterWrapper;
6564
@property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider;
66-
@property (nonatomic, strong) SentryANRTrackerV1 *anrTrackerV1;
67-
@property (nonatomic, strong) SentryANRTrackerV2 *anrTrackerV2;
6865
@property (nonatomic, strong) SentryNSProcessInfoWrapper *processInfoWrapper;
6966
@property (nonatomic, strong) SentrySystemWrapper *systemWrapper;
7067
@property (nonatomic, strong) SentryDispatchFactory *dispatchFactory;
@@ -90,10 +87,10 @@ SENTRY_NO_INIT
9087
@property (nonatomic, strong) SentryReachability *reachability;
9188
#endif // !TARGET_OS_WATCH
9289

93-
- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout;
94-
#if SENTRY_UIKIT_AVAILABLE
95-
- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout;
96-
#endif // SENTRY_UIKIT_AVAILABLE
90+
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout;
91+
#if SENTRY_HAS_UIKIT
92+
- (id<SentryANRTracker>)getANRTracker:(NSTimeInterval)timeout isV2Enabled:(BOOL)isV2Enabled;
93+
#endif // SENTRY_HAS_UIKIT
9794

9895
#if SENTRY_HAS_METRIC_KIT
9996
@property (nonatomic, strong) SentryMXManager *metricKitManager API_AVAILABLE(

0 commit comments

Comments
 (0)