11
11
#import " SentryLog.h"
12
12
#import " SentryMechanism.h"
13
13
#import " SentrySDK+Private.h"
14
+ #import " SentryScope+Private.h"
14
15
#import " SentryStacktrace.h"
15
16
#import " SentrySwift.h"
16
17
#import " SentryThread.h"
17
18
#import " SentryThreadInspector.h"
18
19
#import " SentryThreadWrapper.h"
19
20
#import " SentryUIApplication.h"
21
+ #import < SentryCrashWrapper.h>
20
22
#import < SentryOptions+Private.h>
21
23
22
24
#if SENTRY_HAS_UIKIT
25
27
26
28
NS_ASSUME_NONNULL_BEGIN
27
29
30
+ static NSString *const SentryANRMechanismDataAppHangDuration = @" app_hang_duration" ;
31
+
28
32
@interface SentryANRTrackingIntegration ()
29
33
30
34
@property (nonatomic , strong ) id <SentryANRTracker> tracker;
31
35
@property (nonatomic , strong ) SentryOptions *options;
32
36
@property (nonatomic , strong ) SentryFileManager *fileManager;
33
37
@property (nonatomic , strong ) SentryDispatchQueueWrapper *dispatchQueueWrapper;
38
+ @property (nonatomic , strong ) SentryCrashWrapper *crashWrapper;
34
39
@property (atomic , assign ) BOOL reportAppHangs;
35
40
@property (atomic , assign ) BOOL enableReportNonFullyBlockingAppHangs;
36
41
@@ -55,6 +60,7 @@ - (BOOL)installWithOptions:(SentryOptions *)options
55
60
#endif // SENTRY_HAS_UIKIT
56
61
self.fileManager = SentryDependencyContainer.sharedInstance .fileManager ;
57
62
self.dispatchQueueWrapper = SentryDependencyContainer.sharedInstance .dispatchQueueWrapper ;
63
+ self.crashWrapper = SentryDependencyContainer.sharedInstance .crashWrapper ;
58
64
[self .tracker addListener: self ];
59
65
self.options = options;
60
66
self.reportAppHangs = YES ;
@@ -64,28 +70,6 @@ - (BOOL)installWithOptions:(SentryOptions *)options
64
70
return YES ;
65
71
}
66
72
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
-
89
73
- (SentryIntegrationOption)integrationOptions
90
74
{
91
75
return kIntegrationOptionEnableAppHangTracking | kIntegrationOptionDebuggerNotAttached ;
@@ -142,15 +126,17 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
142
126
return ;
143
127
}
144
128
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];
147
132
SentryEvent *event = [[SentryEvent alloc ] initWithLevel: kSentryLevelError ];
148
133
149
134
NSString *exceptionType = [SentryAppHangTypeMapper getExceptionTypeWithAnrType: type];
150
135
SentryException *sentryException = [[SentryException alloc ] initWithValue: message
151
136
type: exceptionType];
152
137
153
- sentryException.mechanism = [[SentryMechanism alloc ] initWithType: @" AppHang" ];
138
+ SentryMechanism *mechanism = [[SentryMechanism alloc ] initWithType: @" AppHang" ];
139
+ sentryException.mechanism = mechanism;
154
140
sentryException.stacktrace = [threads[0 ] stacktrace ];
155
141
sentryException.stacktrace .snapshot = @(YES );
156
142
@@ -164,6 +150,20 @@ - (void)anrDetectedWithType:(enum SentryANRType)type
164
150
// We only measure app hang duration for V2.
165
151
// For V1, we directly capture the app hang event.
166
152
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
+
167
167
[self .fileManager storeAppHangEvent: event];
168
168
} else {
169
169
#endif // SENTRY_HAS_UIKIT
@@ -195,15 +195,92 @@ - (void)anrStoppedWithResult:(SentryANRStoppedResult *_Nullable)result
195
195
[self .fileManager deleteAppHangEvent ];
196
196
197
197
// 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];
202
201
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 ]];
204
217
#endif // SENTRY_HAS_UIKIT
205
218
}
206
219
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
+
207
284
@end
208
285
209
286
NS_ASSUME_NONNULL_END
0 commit comments