Skip to content

Commit 15d7bc5

Browse files
feat: Report fatal app hangs for V2 (#4889)
This PR reports fatal app hangs without session updates, only when the experimental option enableAppHangTrackingV2 is enabled. Updating the session will be done in a follow-up PR. Co-authored-by: Philip Niedertscheider <[email protected]>
1 parent b1783fd commit 15d7bc5

File tree

6 files changed

+383
-39
lines changed

6 files changed

+383
-39
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Report fatal app hangs (#4889) only when enabling the option `enableAppHangTrackingV2`
78
- New user feedback API and Widget (#4874)
89

910
### Improvements

Sentry.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
622C08DB29E554B9002571D4 /* SentrySpanContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 622C08D929E554B9002571D4 /* SentrySpanContext+Private.h */; };
9999
62375FB92B47F9F000CC55F1 /* SentryDependencyContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */; };
100100
623C45B02A651D8200D9E88B /* SentryCoreDataTracker+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = 623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */; };
101+
623E16C32D6C57C000CF1625 /* SentryANRTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623E16C22D6C57C000CF1625 /* SentryANRTypeTests.swift */; };
101102
623FD9022D3FA5E000803EDA /* SentryFrameCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */; };
102103
623FD9042D3FA92700803EDA /* NSNumberDecodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */; };
103104
623FD9062D3FA9C800803EDA /* NSNumberDecodableWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */; };
@@ -1162,6 +1163,7 @@
11621163
62375FB82B47F9F000CC55F1 /* SentryDependencyContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryDependencyContainerTests.swift; sourceTree = "<group>"; };
11631164
623C45AE2A651C4500D9E88B /* SentryCoreDataTracker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCoreDataTracker+Test.h"; sourceTree = "<group>"; };
11641165
623C45AF2A651D8200D9E88B /* SentryCoreDataTracker+Test.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SentryCoreDataTracker+Test.m"; sourceTree = "<group>"; };
1166+
623E16C22D6C57C000CF1625 /* SentryANRTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTypeTests.swift; sourceTree = "<group>"; };
11651167
623FD9012D3FA5DA00803EDA /* SentryFrameCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryFrameCodable.swift; sourceTree = "<group>"; };
11661168
623FD9032D3FA90900803EDA /* NSNumberDecodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapper.swift; sourceTree = "<group>"; };
11671169
623FD9052D3FA9BA00803EDA /* NSNumberDecodableWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNumberDecodableWrapperTests.swift; sourceTree = "<group>"; };
@@ -3053,6 +3055,7 @@
30533055
7B2A70D727D5F07F008B0D15 /* SentryANRTrackerV1Tests.swift */,
30543056
621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */,
30553057
7BFA69F527E0840400233199 /* SentryANRTrackingIntegrationTests.swift */,
3058+
623E16C22D6C57C000CF1625 /* SentryANRTypeTests.swift */,
30563059
);
30573060
path = ANR;
30583061
sourceTree = "<group>";
@@ -5359,6 +5362,7 @@
53595362
63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */,
53605363
7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */,
53615364
843FB3432D156B9900558F18 /* SentryFeedbackTests.swift in Sources */,
5365+
623E16C32D6C57C000CF1625 /* SentryANRTypeTests.swift in Sources */,
53625366
D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */,
53635367
7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */,
53645368
8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */,

Sources/Sentry/SentryANRTrackingIntegration.m

+107-30
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
#import "SentryLog.h"
1212
#import "SentryMechanism.h"
1313
#import "SentrySDK+Private.h"
14+
#import "SentryScope+Private.h"
1415
#import "SentryStacktrace.h"
1516
#import "SentrySwift.h"
1617
#import "SentryThread.h"
1718
#import "SentryThreadInspector.h"
1819
#import "SentryThreadWrapper.h"
1920
#import "SentryUIApplication.h"
21+
#import <SentryCrashWrapper.h>
2022
#import <SentryOptions+Private.h>
2123

2224
#if SENTRY_HAS_UIKIT
@@ -25,12 +27,15 @@
2527

2628
NS_ASSUME_NONNULL_BEGIN
2729

30+
static NSString *const SentryANRMechanismDataAppHangDuration = @"app_hang_duration";
31+
2832
@interface SentryANRTrackingIntegration ()
2933

3034
@property (nonatomic, strong) id<SentryANRTracker> tracker;
3135
@property (nonatomic, strong) SentryOptions *options;
3236
@property (nonatomic, strong) SentryFileManager *fileManager;
3337
@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper;
38+
@property (nonatomic, strong) SentryCrashWrapper *crashWrapper;
3439
@property (atomic, assign) BOOL reportAppHangs;
3540
@property (atomic, assign) BOOL enableReportNonFullyBlockingAppHangs;
3641

@@ -55,6 +60,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options
5560
#endif // SENTRY_HAS_UIKIT
5661
self.fileManager = SentryDependencyContainer.sharedInstance.fileManager;
5762
self.dispatchQueueWrapper = SentryDependencyContainer.sharedInstance.dispatchQueueWrapper;
63+
self.crashWrapper = SentryDependencyContainer.sharedInstance.crashWrapper;
5864
[self.tracker addListener:self];
5965
self.options = options;
6066
self.reportAppHangs = YES;
@@ -64,28 +70,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options
6470
return YES;
6571
}
6672

67-
/**
68-
* It can happen that an app crashes while waiting for the app hang to stop. Therefore, we send the
69-
* app hang without a duration as it was stored.
70-
*/
71-
- (void)captureStoredAppHangEvent
72-
{
73-
__weak SentryANRTrackingIntegration *weakSelf = self;
74-
[self.dispatchQueueWrapper dispatchAsyncWithBlock:^{
75-
if (weakSelf == nil) {
76-
return;
77-
}
78-
79-
SentryEvent *event = [weakSelf.fileManager readAppHangEvent];
80-
if (event == nil) {
81-
return;
82-
}
83-
84-
[weakSelf.fileManager deleteAppHangEvent];
85-
[SentrySDK captureEvent:event];
86-
}];
87-
}
88-
8973
- (SentryIntegrationOption)integrationOptions
9074
{
9175
return kIntegrationOptionEnableAppHangTracking | kIntegrationOptionDebuggerNotAttached;
@@ -142,15 +126,17 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
142126
return;
143127
}
144128

145-
NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.",
146-
(long)(self.options.appHangTimeoutInterval * 1000)];
129+
NSString *appHangDurationInfo = [NSString
130+
stringWithFormat:@"at least %li ms", (long)(self.options.appHangTimeoutInterval * 1000)];
131+
NSString *message = [NSString stringWithFormat:@"App hanging for %@.", appHangDurationInfo];
147132
SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError];
148133

149134
NSString *exceptionType = [SentryAppHangTypeMapper getExceptionTypeWithAnrType:type];
150135
SentryException *sentryException = [[SentryException alloc] initWithValue:message
151136
type:exceptionType];
152137

153-
sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"];
138+
SentryMechanism *mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"];
139+
sentryException.mechanism = mechanism;
154140
sentryException.stacktrace = [threads[0] stacktrace];
155141
sentryException.stacktrace.snapshot = @(YES);
156142

@@ -164,6 +150,20 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
164150
// We only measure app hang duration for V2.
165151
// For V1, we directly capture the app hang event.
166152
if (self.options.enableAppHangTrackingV2) {
153+
// We only temporarily store the app hang duration info, so we can change the error message
154+
// when either sending a normal or fatal app hang event. Otherwise, we would have to rely on
155+
// string parsing to retrieve the app hang duration info from the error message.
156+
mechanism.data = @{ SentryANRMechanismDataAppHangDuration : appHangDurationInfo };
157+
158+
// We need to apply the scope now because if the app hang turns into a fatal one,
159+
// we would lose the scope. Furthermore, we want to know in which state the app was when the
160+
// app hang started.
161+
SentryScope *scope = [SentrySDK currentHub].scope;
162+
SentryOptions *options = SentrySDK.options;
163+
if (scope != nil && options != nil) {
164+
[scope applyToEvent:event maxBreadcrumb:options.maxBreadcrumbs];
165+
}
166+
167167
[self.fileManager storeAppHangEvent:event];
168168
} else {
169169
#endif // SENTRY_HAS_UIKIT
@@ -195,15 +195,92 @@ - (void)anrStoppedWithResult:(SentryANRStoppedResult *_Nullable)result
195195
[self.fileManager deleteAppHangEvent];
196196

197197
// We round to 0.1 seconds accuracy because we can't precicely measure the app hand duration.
198-
NSString *errorMessage =
199-
[NSString stringWithFormat:@"App hanging between %.1f and %.1f seconds.",
200-
result.minDuration, result.maxDuration];
201-
198+
NSString *appHangDurationInfo = [NSString
199+
stringWithFormat:@"between %.1f and %.1f seconds", result.minDuration, result.maxDuration];
200+
NSString *errorMessage = [NSString stringWithFormat:@"App hanging %@.", appHangDurationInfo];
202201
event.exceptions.firstObject.value = errorMessage;
203-
[SentrySDK captureEvent:event];
202+
203+
if (event.exceptions.firstObject.mechanism.data == nil) {
204+
SENTRY_LOG_WARN(@"Mechanism data of the stored app hang event was nil. This is unexpected, "
205+
@"so it's likely that the app hang event is corrupted. Therefore, dropping "
206+
@"the stored app hang event.");
207+
return;
208+
}
209+
210+
NSMutableDictionary *mechanismData = [event.exceptions.firstObject.mechanism.data mutableCopy];
211+
[mechanismData removeObjectForKey:SentryANRMechanismDataAppHangDuration];
212+
event.exceptions.firstObject.mechanism.data = mechanismData;
213+
214+
// We already applied the scope. We use an empty scope to avoid overwriting exising fields on
215+
// the event.
216+
[SentrySDK captureEvent:event withScope:[[SentryScope alloc] init]];
204217
#endif // SENTRY_HAS_UIKIT
205218
}
206219

220+
- (void)captureStoredAppHangEvent
221+
{
222+
__weak SentryANRTrackingIntegration *weakSelf = self;
223+
[self.dispatchQueueWrapper dispatchAsyncWithBlock:^{
224+
if (weakSelf == nil) {
225+
return;
226+
}
227+
228+
SentryEvent *event = [weakSelf.fileManager readAppHangEvent];
229+
if (event == nil) {
230+
return;
231+
}
232+
233+
[weakSelf.fileManager deleteAppHangEvent];
234+
235+
if (weakSelf.crashWrapper.crashedLastLaunch) {
236+
// The app crashed during an ongoing app hang. Capture the stored app hang as it is.
237+
// We already applied the scope. We use an empty scope to avoid overwriting exising
238+
// fields on the event.
239+
[SentrySDK captureEvent:event withScope:[[SentryScope alloc] init]];
240+
} else {
241+
// Fatal App Hang
242+
// We can't differ if the watchdog or the user terminated the app, because when the main
243+
// thread is blocked we don't receive the applicationWillTerminate notification. Further
244+
// investigations are required to validate if we somehow can differ between watchdog or
245+
// user terminations; see https://github.com/getsentry/sentry-cocoa/issues/4845.
246+
247+
if (event.exceptions.count != 1) {
248+
SENTRY_LOG_WARN(@"The stored app hang event is expected to have exactly one "
249+
@"exception, so we don't capture it.");
250+
return;
251+
}
252+
253+
event.level = kSentryLevelFatal;
254+
255+
SentryException *exception = event.exceptions.firstObject;
256+
257+
NSString *exceptionType = exception.type;
258+
NSString *fatalExceptionType =
259+
[SentryAppHangTypeMapper getFatalExceptionTypeWithNonFatalErrorType:exceptionType];
260+
261+
event.exceptions.firstObject.type = fatalExceptionType;
262+
263+
NSMutableDictionary *mechanismData =
264+
[event.exceptions.firstObject.mechanism.data mutableCopy];
265+
NSString *appHangDurationInfo
266+
= exception.mechanism.data[SentryANRMechanismDataAppHangDuration];
267+
268+
[mechanismData removeObjectForKey:SentryANRMechanismDataAppHangDuration];
269+
event.exceptions.firstObject.mechanism.data = mechanismData;
270+
271+
NSString *exceptionValue =
272+
[NSString stringWithFormat:@"The user or the OS watchdog terminated your app while "
273+
@"it blocked the main thread for %@.",
274+
appHangDurationInfo];
275+
event.exceptions.firstObject.value = exceptionValue;
276+
277+
// We already applied the scope. We use an empty scope to avoid overwriting exising
278+
// fields on the event.
279+
[SentrySDK captureEvent:event withScope:[[SentryScope alloc] init]];
280+
}
281+
}];
282+
}
283+
207284
@end
208285

209286
NS_ASSUME_NONNULL_END

Sources/Swift/Integrations/ANR/SentryANRType.swift

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@objc
22
enum SentryANRType: Int {
3+
case fatalFullyBlocking
4+
case fatalNonFullyBlocking
35
case fullyBlocking
46
case nonFullyBlocking
57
case unknown
@@ -9,14 +11,20 @@ enum SentryANRType: Int {
911
class SentryAppHangTypeMapper: NSObject {
1012

1113
private enum ExceptionType: String {
12-
case fullyBlocking = "App Hanging Fully Blocked"
13-
case nonFullyBlocking = "App Hanging Non Fully Blocked"
14+
case fatalFullyBlocking = "Fatal App Hang Fully Blocked"
15+
case fatalNonFullyBlocking = "Fatal App Hang Non Fully Blocked"
16+
case fullyBlocking = "App Hang Fully Blocked"
17+
case nonFullyBlocking = "App Hang Non Fully Blocked"
1418
case unknown = "App Hanging"
1519
}
1620

1721
@objc
1822
static func getExceptionType(anrType: SentryANRType) -> String {
1923
switch anrType {
24+
case .fatalFullyBlocking:
25+
return ExceptionType.fatalFullyBlocking.rawValue
26+
case .fatalNonFullyBlocking:
27+
return ExceptionType.fatalNonFullyBlocking.rawValue
2028
case .fullyBlocking:
2129
return ExceptionType.fullyBlocking.rawValue
2230
case .nonFullyBlocking:
@@ -25,6 +33,15 @@ class SentryAppHangTypeMapper: NSObject {
2533
return ExceptionType.unknown.rawValue
2634
}
2735
}
36+
37+
@objc
38+
static func getFatalExceptionType(nonFatalErrorType: String) -> String {
39+
if nonFatalErrorType == ExceptionType.nonFullyBlocking.rawValue {
40+
return ExceptionType.fatalNonFullyBlocking.rawValue
41+
}
42+
43+
return ExceptionType.fatalFullyBlocking.rawValue
44+
}
2845

2946
@objc
3047
static func isExceptionTypeAppHang(exceptionType: String) -> Bool {

0 commit comments

Comments
 (0)