diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c8fd3070..aa04e130c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added breadcrumb.origin private field (#4358) - Custom redact modifier for SwiftUI (#4362) +- 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). - Add support for arm64e (#3398) - Add mergeable libraries support to dynamic libraries (#4381) diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 9a77b69818..c64e8f922a 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -80,6 +80,7 @@ 621AE74D2C626C510012E730 /* SentryANRTrackerV2.m in Sources */ = {isa = PBXBuildFile; fileRef = 621AE74C2C626C510012E730 /* SentryANRTrackerV2.m */; }; 621D9F2F2B9B0320003D94DE /* SentryCurrentDateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */; }; 621F61F12BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */; }; + 6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6221BBC92CAA932100C627CA /* SentryANRType.swift */; }; 62262B862BA1C46D004DA3DD /* SentryStatsdClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */; }; 62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 62262B872BA1C490004DA3DD /* SentryStatsdClient.m */; }; 62262B8B2BA1C4C1004DA3DD /* EncodeMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */; }; @@ -1088,6 +1089,7 @@ 621AE74E2C626CF70012E730 /* SentryANRTrackerV2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRTrackerV2Tests.swift; sourceTree = ""; }; 621D9F2E2B9B0320003D94DE /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; 621F61F02BEA073A005E654F /* SentryEnabledFeaturesBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnabledFeaturesBuilder.swift; sourceTree = ""; }; + 6221BBC92CAA932100C627CA /* SentryANRType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryANRType.swift; sourceTree = ""; }; 62262B852BA1C46D004DA3DD /* SentryStatsdClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStatsdClient.h; path = include/SentryStatsdClient.h; sourceTree = ""; }; 62262B872BA1C490004DA3DD /* SentryStatsdClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStatsdClient.m; sourceTree = ""; }; 62262B8A2BA1C4C1004DA3DD /* EncodeMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodeMetrics.swift; sourceTree = ""; }; @@ -2199,6 +2201,7 @@ children = ( 6294774B2C6F255F00846CBC /* SentryANRTrackerV2Delegate.swift */, 62FC18AE2C9D5FAC008803CD /* SentryANRTracker.swift */, + 6221BBC92CAA932100C627CA /* SentryANRType.swift */, ); path = ANR; sourceTree = ""; @@ -4634,6 +4637,7 @@ D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */, 63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */, 63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */, + 6221BBCA2CAA932100C627CA /* SentryANRType.swift in Sources */, 7BA61CCA247D128B00C130A8 /* SentryThreadInspector.m in Sources */, D8CA12952C203E71005894F4 /* SentrySessionListener.swift in Sources */, 63FE718D20DA4C1100CDBAE8 /* SentryCrashReportStore.c in Sources */, diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index a98d8d64cc..7914b54437 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -559,6 +559,40 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enableAppHangTracking; +#if SENTRY_UIKIT_AVAILABLE + +/** + * AppHangTrackingV2 can differentiate between fully-blocking and non-fully blocking app hangs. + * fully-blocking app hang is when the main thread is stuck completely, and the app can't render a + * single frame. A non-fully-blocking app hang is when the app appears stuck to the user but can + still + * render a few frames. Fully-blocking app hangs are more actionable because the stacktrace shows + the + * exact blocking location on the main thread. As the main thread isn't completely blocked, + * non-fully-blocking app hangs can have a stacktrace that doesn't highlight the exact blocking + * location. + * + * You can use @c enableReportNonFullyBlockingAppHangs to ignore non-fully-blocking app hangs. + * + * @note This flag wins over enableAppHangTracking. When enabling both enableAppHangTracking and + enableAppHangTrackingV2, the SDK only enables enableAppHangTrackingV2 and disables + enableAppHangTracking. + * + * @warning This is an experimental feature and may still have bugs. + */ +@property (nonatomic, assign) BOOL enableAppHangTrackingV2; + +/** + * When enabled the SDK reports non-fully-blocking app hangs. A non-fully-blocking app hang is when + * the app appears stuck to the user but can still render a few frames. For more information see @c + * enableAppHangTrackingV2. + * + * @note The default is @c YES. This feature only works when @c enableAppHangTrackingV2 is enabled. + */ +@property (nonatomic, assign) BOOL enableReportNonFullyBlockingAppHangs; + +#endif // SENTRY_UIKIT_AVAILABLE + /** * The minimum amount of time an app should be unresponsive to be classified as an App Hanging. * @note The actual amount may be a little longer. diff --git a/Sources/Sentry/SentryANRTrackingIntegration.m b/Sources/Sentry/SentryANRTrackingIntegration.m index 35fa65cb1f..c1d6793ed3 100644 --- a/Sources/Sentry/SentryANRTrackingIntegration.m +++ b/Sources/Sentry/SentryANRTrackingIntegration.m @@ -29,6 +29,7 @@ @interface SentryANRTrackingIntegration () @property (nonatomic, strong) id tracker; @property (nonatomic, strong) SentryOptions *options; @property (atomic, assign) BOOL reportAppHangs; +@property (atomic, assign) BOOL enableReportNonFullyBlockingAppHangs; @end @@ -40,9 +41,15 @@ - (BOOL)installWithOptions:(SentryOptions *)options return NO; } +#if SENTRY_HAS_UIKIT + self.tracker = + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval + isV2Enabled:options.enableAppHangTrackingV2]; +#else self.tracker = - [SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval]; + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval]; +#endif // SENTRY_HAS_UIKIT [self.tracker addListener:self]; self.options = options; self.reportAppHangs = YES; @@ -83,6 +90,12 @@ - (void)anrDetectedWithType:(enum SentryANRType)type } #if SENTRY_HAS_UIKIT + if (type == SentryANRTypeNonFullyBlocking + && !self.options.enableReportNonFullyBlockingAppHangs) { + SENTRY_LOG_DEBUG(@"Ignoring non fully blocking app hang.") + return; + } + // 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. if (SentryDependencyContainer.sharedInstance.application.applicationState @@ -103,8 +116,10 @@ - (void)anrDetectedWithType:(enum SentryANRType)type NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.", (long)(self.options.appHangTimeoutInterval * 1000)]; SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError]; - SentryException *sentryException = - [[SentryException alloc] initWithValue:message type:SentryANRExceptionType]; + + NSString *exceptionType = [SentryAppHangTypeMapper getExceptionTypeWithAnrType:type]; + SentryException *sentryException = [[SentryException alloc] initWithValue:message + type:exceptionType]; sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"]; sentryException.stacktrace = [threads[0] stacktrace]; diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index f6f7f6698d..35ea398867 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -78,22 +78,17 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options #endif if (integrationOptions & kIntegrationOptionEnableAppHangTracking) { - if (!options.enableAppHangTracking) { - [self logWithOptionName:@"enableAppHangTracking"]; - return NO; - } - - if (options.appHangTimeoutInterval == 0) { - [self logWithReason:@"because appHangTimeoutInterval is 0"]; +#if SENTRY_HAS_UIKIT + if (!options.enableAppHangTracking && !options.enableAppHangTrackingV2) { + [self logWithOptionName:@"enableAppHangTracking && enableAppHangTrackingV2"]; return NO; } - } - - if (integrationOptions & kIntegrationOptionEnableAppHangTrackingV2) { - if (!options.enableAppHangTrackingV2) { - [self logWithOptionName:@"enableAppHangTrackingV2"]; +#else + if (!options.enableAppHangTracking) { + [self logWithOptionName:@"enableAppHangTracking"]; return NO; } +#endif // SENTRY_HAS_UIKIT if (options.appHangTimeoutInterval == 0) { [self logWithReason:@"because appHangTimeoutInterval is 0"]; diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 7d32483cad..19c9580584 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -1,5 +1,5 @@ #import "SentryANRTrackerV1.h" -#import "SentryANRTrackerV2.h" + #import "SentryBinaryImageCache.h" #import "SentryDispatchFactory.h" #import "SentryDispatchQueueWrapper.h" @@ -32,6 +32,7 @@ #import #if SENTRY_HAS_UIKIT +# import "SentryANRTrackerV2.h" # import "SentryFramesTracker.h" # import "SentryUIApplication.h" # import @@ -46,6 +47,12 @@ # import "SentryReachability.h" #endif // !TARGET_OS_WATCH +@interface SentryDependencyContainer () + +@property (nonatomic, strong) id anrTracker; + +@end + @implementation SentryDependencyContainer static SentryDependencyContainer *instance; @@ -301,32 +308,6 @@ - (SentryFramesTracker *)framesTracker SENTRY_DISABLE_THREAD_SANITIZER( # endif // SENTRY_HAS_UIKIT } -- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout - SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms") -{ -# if SENTRY_HAS_UIKIT - if (_anrTrackerV2 == nil) { - @synchronized(sentryDependencyContainerLock) { - if (_anrTrackerV2 == nil) { - _anrTrackerV2 = - [[SentryANRTrackerV2 alloc] initWithTimeoutInterval:timeout - crashWrapper:self.crashWrapper - dispatchQueueWrapper:self.dispatchQueueWrapper - threadWrapper:self.threadWrapper - framesTracker:self.framesTracker]; - } - } - } - - return _anrTrackerV2; -# else - SENTRY_LOG_DEBUG( - @"SentryDependencyContainer.getANRTrackerV2 only works with UIKit enabled. Ensure you're " - @"using the right configuration of Sentry that links UIKit."); - return nil; -# endif // SENTRY_HAS_UIKIT -} - - (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER( "double-checked lock produce false alarms") { @@ -348,13 +329,13 @@ - (SentrySwizzleWrapper *)swizzleWrapper SENTRY_DISABLE_THREAD_SANITIZER( } #endif // SENTRY_UIKIT_AVAILABLE -- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout +- (id)getANRTracker:(NSTimeInterval)timeout SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms") { - if (_anrTrackerV1 == nil) { + if (_anrTracker == nil) { @synchronized(sentryDependencyContainerLock) { - if (_anrTrackerV1 == nil) { - _anrTrackerV1 = + if (_anrTracker == nil) { + _anrTracker = [[SentryANRTrackerV1 alloc] initWithTimeoutInterval:timeout crashWrapper:self.crashWrapper dispatchQueueWrapper:self.dispatchQueueWrapper @@ -363,8 +344,34 @@ - (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout } } - return _anrTrackerV1; + return _anrTracker; +} + +#if SENTRY_HAS_UIKIT +- (id)getANRTracker:(NSTimeInterval)timeout + isV2Enabled:(BOOL)isV2Enabled + SENTRY_DISABLE_THREAD_SANITIZER("double-checked lock produce false alarms") +{ + if (isV2Enabled) { + if (_anrTracker == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_anrTracker == nil) { + _anrTracker = [[SentryANRTrackerV2 alloc] + initWithTimeoutInterval:timeout + crashWrapper:self.crashWrapper + dispatchQueueWrapper:self.dispatchQueueWrapper + threadWrapper:self.threadWrapper + framesTracker:self.framesTracker]; + } + } + } + + return _anrTracker; + } else { + return [self getANRTracker:timeout]; + } } +#endif // SENTRY_HAS_UIKIT - (SentryNSProcessInfoWrapper *)processInfoWrapper SENTRY_DISABLE_THREAD_SANITIZER( "double-checked lock produce false alarms") diff --git a/Sources/Sentry/SentryEvent.m b/Sources/Sentry/SentryEvent.m index 2408c73a7e..c5478226b1 100644 --- a/Sources/Sentry/SentryEvent.m +++ b/Sources/Sentry/SentryEvent.m @@ -204,7 +204,8 @@ - (BOOL)isMetricKitEvent - (BOOL)isAppHangEvent { return self.exceptions.count == 1 && - [self.exceptions.firstObject.type isEqualToString:SentryANRExceptionType]; + [SentryAppHangTypeMapper + isExceptionTypeAppHangWithExceptionType:self.exceptions.firstObject.type]; } @end diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index b0da7a1908..554eb37b43 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -118,10 +118,11 @@ - (instancetype)init self.enableUserInteractionTracing = YES; self.idleTimeout = SentryTracerDefaultTimeout; self.enablePreWarmedAppStartTracing = NO; + self.enableAppHangTrackingV2 = NO; + self.enableReportNonFullyBlockingAppHangs = YES; #endif // SENTRY_HAS_UIKIT self.enableAppHangTracking = YES; self.appHangTimeoutInterval = 2.0; - self.enableAppHangTrackingV2 = NO; self.enableAutoBreadcrumbTracking = YES; self.enableNetworkTracking = YES; self.enableFileIOTracing = YES; @@ -435,6 +436,12 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; + [self setBool:options[@"enableAppHangTrackingV2"] + block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }]; + + [self setBool:options[@"enableReportNonFullyBlockingAppHangs"] + block:^(BOOL value) { self->_enableReportNonFullyBlockingAppHangs = value; }]; + #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] @@ -444,9 +451,6 @@ - (BOOL)validateOptions:(NSDictionary *)options self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue]; } - [self setBool:options[@"enableAppHangTrackingV2"] - block:^(BOOL value) { self->_enableAppHangTrackingV2 = value; }]; - [self setBool:options[@"enableNetworkTracking"] block:^(BOOL value) { self->_enableNetworkTracking = value; }]; diff --git a/Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m b/Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m index 850530b12d..65a8126cbd 100644 --- a/Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m +++ b/Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m @@ -74,7 +74,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options [self.tracker start]; self.anrTracker = - [SentryDependencyContainer.sharedInstance getANRTrackerV1:options.appHangTimeoutInterval]; + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval + isV2Enabled:options.enableAppHangTrackingV2]; [self.anrTracker addListener:self]; self.appStateManager = appStateManager; diff --git a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h index e9e817f1aa..2a2cf867c4 100644 --- a/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h +++ b/Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h @@ -1,7 +1,6 @@ #import "SentryDefines.h" -@class SentryANRTrackerV1; -@class SentryANRTrackerV2; +@protocol SentryANRTracker; @class SentryAppStateManager; @class SentryBinaryImageCache; @class SentryCrash; @@ -63,8 +62,6 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper; @property (nonatomic, strong) SentryNSNotificationCenterWrapper *notificationCenterWrapper; @property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; -@property (nonatomic, strong) SentryANRTrackerV1 *anrTrackerV1; -@property (nonatomic, strong) SentryANRTrackerV2 *anrTrackerV2; @property (nonatomic, strong) SentryNSProcessInfoWrapper *processInfoWrapper; @property (nonatomic, strong) SentrySystemWrapper *systemWrapper; @property (nonatomic, strong) SentryDispatchFactory *dispatchFactory; @@ -90,10 +87,10 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentryReachability *reachability; #endif // !TARGET_OS_WATCH -- (SentryANRTrackerV1 *)getANRTrackerV1:(NSTimeInterval)timeout; -#if SENTRY_UIKIT_AVAILABLE -- (SentryANRTrackerV2 *)getANRTrackerV2:(NSTimeInterval)timeout; -#endif // SENTRY_UIKIT_AVAILABLE +- (id)getANRTracker:(NSTimeInterval)timeout; +#if SENTRY_HAS_UIKIT +- (id)getANRTracker:(NSTimeInterval)timeout isV2Enabled:(BOOL)isV2Enabled; +#endif // SENTRY_HAS_UIKIT #if SENTRY_HAS_METRIC_KIT @property (nonatomic, strong) SentryMXManager *metricKitManager API_AVAILABLE( diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h index fa803291fc..c8adad48fc 100644 --- a/Sources/Sentry/include/SentryBaseIntegration.h +++ b/Sources/Sentry/include/SentryBaseIntegration.h @@ -23,7 +23,6 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) { kIntegrationOptionEnableCrashHandler = 1 << 16, kIntegrationOptionEnableMetricKit = 1 << 17, kIntegrationOptionEnableReplay = 1 << 18, - kIntegrationOptionEnableAppHangTrackingV2 = 1 << 19, }; @class SentryOptions; diff --git a/Sources/Sentry/include/SentryOptions+Private.h b/Sources/Sentry/include/SentryOptions+Private.h index 1b6db177db..b9c74a20e2 100644 --- a/Sources/Sentry/include/SentryOptions+Private.h +++ b/Sources/Sentry/include/SentryOptions+Private.h @@ -32,8 +32,6 @@ FOUNDATION_EXPORT NSString *const kSentryDefaultEnvironment; SENTRY_EXTERN BOOL sentry_isValidSampleRate(NSNumber *sampleRate); -@property (nonatomic, assign) BOOL enableAppHangTrackingV2; - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift b/Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift index 670c65df7f..92849ff581 100644 --- a/Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift +++ b/Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift @@ -6,10 +6,3 @@ protocol SentryANRTrackerDelegate { func anrDetected(type: SentryANRType) func anrStopped() } - -@objc -enum SentryANRType: Int { - case fullyBlocking - case nonFullyBlocking - case unknown -} diff --git a/Sources/Swift/Integrations/ANR/SentryANRType.swift b/Sources/Swift/Integrations/ANR/SentryANRType.swift new file mode 100644 index 0000000000..46ee99d65f --- /dev/null +++ b/Sources/Swift/Integrations/ANR/SentryANRType.swift @@ -0,0 +1,33 @@ +@objc +enum SentryANRType: Int { + case fullyBlocking + case nonFullyBlocking + case unknown +} + +@objc +class SentryAppHangTypeMapper: NSObject { + + private enum ExceptionType: String { + case fullyBlocking = "App Hanging Fully Blocked" + case nonFullyBlocking = "App Hanging Non Fully Blocked" + case unknown = "App Hanging" + } + + @objc + static func getExceptionType(anrType: SentryANRType) -> String { + switch anrType { + case .fullyBlocking: + return ExceptionType.fullyBlocking.rawValue + case .nonFullyBlocking: + return ExceptionType.nonFullyBlocking.rawValue + default: + return ExceptionType.unknown.rawValue + } + } + + @objc + static func isExceptionTypeAppHang(exceptionType: String) -> Bool { + return ExceptionType(rawValue: exceptionType) != nil + } +} diff --git a/Tests/SentryTests/Helper/SentryDependencyContainerTests.swift b/Tests/SentryTests/Helper/SentryDependencyContainerTests.swift index db16f40793..148c594e94 100644 --- a/Tests/SentryTests/Helper/SentryDependencyContainerTests.swift +++ b/Tests/SentryTests/Helper/SentryDependencyContainerTests.swift @@ -1,8 +1,8 @@ import XCTest -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) final class SentryDependencyContainerTests: XCTestCase { - + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) func testReset_CallsFramesTrackerStop() throws { let framesTracker = SentryDependencyContainer.sharedInstance().framesTracker framesTracker.start() @@ -10,5 +10,49 @@ final class SentryDependencyContainerTests: XCTestCase { XCTAssertFalse(framesTracker.isRunning) } -} + + func testGetANRTrackerV2() { + let instance = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: true) + XCTAssertTrue(instance is SentryANRTrackerV2) + + SentryDependencyContainer.reset() + + } + + func testGetANRTrackerV1() { + let instance = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: false) + XCTAssertTrue(instance is SentryANRTrackerV1) + + SentryDependencyContainer.reset() + } + + func testGetANRTrackerV2AndThenV1_FirstCalledVersionStaysTheSame() { + let instance1 = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: true) + XCTAssertTrue(instance1 is SentryANRTrackerV2) + + let instance2 = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: false) + XCTAssertTrue(instance2 is SentryANRTrackerV2) + + SentryDependencyContainer.reset() + } + + func testGetANRTrackerV1AndThenV2_FirstCalledVersionStaysTheSame() { + let instance1 = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: false) + XCTAssertTrue(instance1 is SentryANRTrackerV1) + + let instance2 = SentryDependencyContainer.sharedInstance().getANRTracker(2.0, isV2Enabled: true) + XCTAssertTrue(instance2 is SentryANRTrackerV1) + + SentryDependencyContainer.reset() + } + #endif + + func testGetANRTracker_ReturnsV1() { + + let instance = SentryDependencyContainer.sharedInstance().getANRTracker(2.0) + XCTAssertTrue(instance is SentryANRTrackerV1) + + SentryDependencyContainer.reset() + } +} diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift index 63602ec816..9c15e6606c 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift @@ -41,7 +41,10 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { func testWhenNoDebuggerAttached_TrackerInitialized() { givenInitializedTracker() - XCTAssertNotNil(Dynamic(sut).tracker.asAnyObject) + + let tracker = Dynamic(sut).tracker.asAnyObject + XCTAssertNotNil(tracker) + XCTAssertTrue(tracker is SentryANRTrackerV1) } func test_enableAppHangsTracking_Disabled() { @@ -65,6 +68,22 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { XCTAssertFalse(result) } +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + func test_enableAppHangTrackingV2_UsesV2Tracker() { + let options = Options() + options.enableAppHangTracking = true + options.enableAppHangTrackingV2 = true + + sut = SentryANRTrackingIntegration() + let result = sut.install(with: options) + XCTAssertTrue(result) + + let tracker = Dynamic(sut).tracker.asAnyObject + XCTAssertNotNil(tracker) + XCTAssertTrue(tracker is SentryANRTrackerV2) + } +#endif + func testANRDetected_EventCaptured() throws { givenInitializedTracker() setUpThreadInspector() @@ -102,6 +121,92 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { } } + func testANRDetected_FullyBlocking_EventCaptured() throws { + givenInitializedTracker() + setUpThreadInspector() + + Dynamic(sut).anrDetectedWithType(SentryANRType.fullyBlocking) + + try assertEventWithScopeCaptured { event, _, _ in + XCTAssertNotNil(event) + guard let ex = event?.exceptions?.first else { + XCTFail("ANR Exception not found") + return + } + + XCTAssertEqual(ex.mechanism?.type, "AppHang") + XCTAssertEqual(ex.type, "App Hanging Fully Blocked") + XCTAssertEqual(ex.value, "App hanging for at least 4500 ms.") + XCTAssertNotNil(ex.stacktrace) + XCTAssertEqual(ex.stacktrace?.frames.first?.function, "main") + XCTAssertEqual(ex.stacktrace?.snapshot?.boolValue, true) + XCTAssertEqual(try XCTUnwrap(event?.threads?.first).current?.boolValue, true) + XCTAssertEqual(event?.isAppHangEvent, true) + + guard let threads = event?.threads else { + XCTFail("ANR Exception not found") + return + } + + // Sometimes during tests its possible to have one thread without frames + // We just need to make sure we retrieve frame information for at least one other thread than the main thread + let threadsWithFrames = threads.filter { + ($0.stacktrace?.frames.count ?? 0) >= 1 + }.count + + XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames") + } + } + + func testANRDetected_NonFullyBlocked_EventCaptured() throws { + givenInitializedTracker() + setUpThreadInspector() + + Dynamic(sut).anrDetectedWithType(SentryANRType.nonFullyBlocking) + + try assertEventWithScopeCaptured { event, _, _ in + XCTAssertNotNil(event) + guard let ex = event?.exceptions?.first else { + XCTFail("ANR Exception not found") + return + } + + XCTAssertEqual(ex.mechanism?.type, "AppHang") + XCTAssertEqual(ex.type, "App Hanging Non Fully Blocked") + XCTAssertEqual(ex.value, "App hanging for at least 4500 ms.") + XCTAssertNotNil(ex.stacktrace) + XCTAssertEqual(ex.stacktrace?.frames.first?.function, "main") + XCTAssertEqual(ex.stacktrace?.snapshot?.boolValue, true) + XCTAssertEqual(try XCTUnwrap(event?.threads?.first).current?.boolValue, true) + XCTAssertEqual(event?.isAppHangEvent, true) + + guard let threads = event?.threads else { + XCTFail("ANR Exception not found") + return + } + + // Sometimes during tests its possible to have one thread without frames + // We just need to make sure we retrieve frame information for at least one other thread than the main thread + let threadsWithFrames = threads.filter { + ($0.stacktrace?.frames.count ?? 0) >= 1 + }.count + + XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames") + } + } + +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + func testANRDetected_NonFullyBlockedDisabled_EventNotCaptured() throws { + fixture.options.enableReportNonFullyBlockingAppHangs = false + givenInitializedTracker() + setUpThreadInspector() + + Dynamic(sut).anrDetectedWithType(SentryANRType.nonFullyBlocking) + + assertNoEventCaptured() + } +#endif + func testANRDetected_DetectingPaused_NoEventCaptured() { givenInitializedTracker() setUpThreadInspector() @@ -151,6 +256,7 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { assertNoEventCaptured() } + #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) func testANRDetected_ButBackground_EventNotCaptured() { @@ -180,7 +286,7 @@ class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { initIntegration() - let tracker = SentryDependencyContainer.sharedInstance().getANRTrackerV1(self.options.appHangTimeoutInterval) + let tracker = SentryDependencyContainer.sharedInstance().getANRTracker(self.options.appHangTimeoutInterval) let listeners = Dynamic(tracker).listeners.asObject as? NSHashTable diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 07bc316848..14925b4a48 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -916,11 +916,20 @@ - (void)testEnableAppHangTracking [self testBooleanField:@"enableAppHangTracking" defaultValue:YES]; } +#if SENTRY_UIKIT_AVAILABLE + - (void)testEnableAppHangTrackingV2 { [self testBooleanField:@"enableAppHangTrackingV2" defaultValue:NO]; } +- (void)testEnableReportNonFullyBlockingAppHangs +{ + [self testBooleanField:@"enableReportNonFullyBlockingAppHangs" defaultValue:YES]; +} + +#endif // SENTRY_UIKIT_AVAILABLE + - (void)testDefaultAppHangsTimeout { SentryOptions *options = [self getValidOptions:@{}];