diff --git a/CHANGELOG.md b/CHANGELOG.md index 59811caf062..a99b5e0487f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Features +- Add App Hangs tracking (#1861) - Replace tracestate header with baggage (#1867) - Implement description for SentryBreadcrumb (#1880) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index b6132bc0387..d50b636bcac 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -27,6 +27,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.enableCoreDataTracking = true options.enableProfiling = true options.attachScreenshot = true + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 2 options.enableUserInteractionTracing = true } diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 98768061e99..e2efeb2575a 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -646,9 +646,13 @@ D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */; }; D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */; }; + D8853C842833EABC00700D64 /* SentryANRTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 7BCFA71427D0BAB7008C662C /* SentryANRTracker.h */; }; + D88817D826D7149100BF2251 /* SentryTraceState.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceState.m */; }; + D88817DA26D72AB800BF2251 /* SentryTraceState.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceState.h */; }; D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; D8ACE3C82762187200F5A213 /* SentryNSDataTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */; }; @@ -1382,6 +1386,7 @@ D88817D626D7149100BF2251 /* SentryTraceContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryTraceContext.m; sourceTree = ""; }; D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = include/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; + D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; D8ACE3C52762187200F5A213 /* SentryNSDataTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataTracker.m; sourceTree = ""; }; @@ -1772,6 +1777,7 @@ 7B4260332630315C00B36EDD /* SampleError.swift */, 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */, 8ED3D305264DFE700049393B /* SentryUIViewControllerSanitizerTests.swift */, + D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */, ); path = SentryTests; sourceTree = ""; @@ -2916,6 +2922,7 @@ 7BE1E32824F7AE08009D3AD0 /* SentrySession+Private.h in Headers */, 7B5CAF7127F5953400ED0DB6 /* SentryEnvelope+Private.h in Headers */, 63FE713320DA4C1100CDBAE8 /* SentryCrashCPU.h in Headers */, + D8853C842833EABC00700D64 /* SentryANRTracker.h in Headers */, 63FE715B20DA4C1100CDBAE8 /* SentryCrashSignalInfo.h in Headers */, 63FE70E520DA4C1000CDBAE8 /* SentryCrashMonitor_CPPException.h in Headers */, 7B127B0D27CF6F2300A71ED2 /* SentryANRTrackingIntegration.h in Headers */, @@ -3497,6 +3504,7 @@ 7B59398224AB47650003AAD2 /* SentrySessionTrackerTests.swift in Sources */, 7B05A61824A4D14A00EF211D /* SentrySessionGeneratorTests.swift in Sources */, 63FE720920DA66EC00CDBAE8 /* XCTestCase+SentryCrash.m in Sources */, + D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */, 7B85BD8E24C5C3A6000A4225 /* SentryFileManagerTestExtension.swift in Sources */, 7B0002342477F52D0035FEF1 /* SentrySessionTests.swift in Sources */, ); diff --git a/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme b/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme index 6a92a5ba9f4..8c6597a7afd 100644 --- a/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme +++ b/Sentry.xcodeproj/xcshareddata/xcschemes/Sentry.xcscheme @@ -54,6 +54,11 @@ BlueprintName = "SentryTests" ReferencedContainer = "container:Sentry.xcodeproj"> + + + + diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index 86eba28f2a3..b802fc45858 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -328,6 +328,19 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL sendClientReports; +/** + * When enabled, the SDK tracks when the application stops responding for a specific amount of + * time defined by the `appHangsTimeoutInterval` option. + */ +@property (nonatomic, assign) BOOL enableAppHangTracking; + +/** + * The minimum amount of time an app shoud be unresponsive to be classified as an App Hanging. + * The actual amount may be a little longer. + * Avoid using values lower than 100ms, which may cause a lot of app hangs events being transmitted. + */ +@property (nonatomic, assign) NSTimeInterval appHangTimeoutInterval; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryANRTracker.m b/Sources/Sentry/SentryANRTracker.m index 34b20fdb229..8cc327d132b 100644 --- a/Sources/Sentry/SentryANRTracker.m +++ b/Sources/Sentry/SentryANRTracker.m @@ -6,50 +6,47 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @interface SentryANRTracker () -@property (weak, nonatomic) id delegate; -@property (nonatomic, assign) NSTimeInterval timeoutInterval; @property (nonatomic, strong) id currentDate; @property (nonatomic, strong) SentryCrashWrapper *crashWrapper; @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper; @property (nonatomic, strong) SentryThreadWrapper *threadWrapper; +@property (nonatomic, strong) NSMutableSet> *listeners; +@property (nonatomic, assign) NSTimeInterval timeoutInterval; @property (weak, nonatomic) NSThread *thread; @end -@implementation SentryANRTracker +@implementation SentryANRTracker { + NSObject *threadLock; + BOOL running; +} -- (instancetype)initWithDelegate:(id)delegate - timeoutIntervalMillis:(NSUInteger)timeoutIntervalMillis - currentDateProvider:(id)currentDateProvider - crashWrapper:(SentryCrashWrapper *)crashWrapper - dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper - threadWrapper:(SentryThreadWrapper *)threadWrapper +- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval + currentDateProvider:(id)currentDateProvider + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper + threadWrapper:(SentryThreadWrapper *)threadWrapper { if (self = [super init]) { - self.delegate = delegate; - self.timeoutInterval = (double)timeoutIntervalMillis / 1000; + self.timeoutInterval = timeoutInterval; self.currentDate = currentDateProvider; self.crashWrapper = crashWrapper; self.dispatchQueueWrapper = dispatchQueueWrapper; self.threadWrapper = threadWrapper; + self.listeners = [NSMutableSet new]; + threadLock = [[NSObject alloc] init]; + running = false; } return self; } -- (void)start -{ - [NSThread detachNewThreadSelector:@selector(detectANRs) toTarget:self withObject:nil]; -} - - (void)detectANRs { - NSThread.currentThread.name = @"io.sentry.anr-tracker"; + NSThread.currentThread.name = @"io.sentry.app-hang-tracker"; self.thread = NSThread.currentThread; @@ -68,7 +65,7 @@ - (void)detectANRs if (blockExecutedOnMainThread) { if (wasPreviousANR) { [SentryLog logWithMessage:@"ANR stopped." andLevel:kSentryLevelWarning]; - [self.delegate anrStopped]; + [self ANRStopped]; } wasPreviousANR = NO; @@ -103,18 +100,85 @@ - (void)detectANRs wasPreviousANR = YES; [SentryLog logWithMessage:@"ANR detected." andLevel:kSentryLevelWarning]; - [self.delegate anrDetected]; + [self ANRDetected]; + } +} + +- (void)ANRDetected +{ + NSArray *localListeners; + @synchronized(self.listeners) { + localListeners = [self.listeners allObjects]; + } + + for (id target in localListeners) { + [target anrDetected]; + } +} + +- (void)ANRStopped +{ + NSArray *targets; + @synchronized(self.listeners) { + targets = [self.listeners allObjects]; + } + + for (id target in targets) { + [target anrStopped]; + } +} + +- (void)addListener:(id)listener +{ + @synchronized(self.listeners) { + [self.listeners addObject:listener]; + + if (self.listeners.count > 0 && !running) { + @synchronized(threadLock) { + if (!running) { + [self start]; + } + } + } + } +} + +- (void)removeListener:(id)listener +{ + @synchronized(self.listeners) { + [self.listeners removeObject:listener]; + + if (self.listeners.count == 0) { + [self stop]; + } + } +} + +- (void)clear +{ + @synchronized(self.listeners) { + [self.listeners removeAllObjects]; + [self stop]; + } +} + +- (void)start +{ + @synchronized(threadLock) { + [NSThread detachNewThreadSelector:@selector(detectANRs) toTarget:self withObject:nil]; + running = YES; } } - (void)stop { - [SentryLog logWithMessage:@"Stopping ANR detection" andLevel:kSentryLevelInfo]; - [self.thread cancel]; + @synchronized(threadLock) { + [SentryLog logWithMessage:@"Stopping ANR detection" andLevel:kSentryLevelInfo]; + [self.thread cancel]; + running = NO; + } } @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryANRTrackingIntegration.m b/Sources/Sentry/SentryANRTrackingIntegration.m index dc07b11c9c7..2919fc75907 100644 --- a/Sources/Sentry/SentryANRTrackingIntegration.m +++ b/Sources/Sentry/SentryANRTrackingIntegration.m @@ -1,9 +1,18 @@ #import "SentryANRTrackingIntegration.h" #import "SentryANRTracker.h" +#import "SentryClient+Private.h" +#import "SentryCrashMachineContext.h" #import "SentryCrashWrapper.h" #import "SentryDefaultCurrentDateProvider.h" #import "SentryDispatchQueueWrapper.h" +#import "SentryEvent.h" +#import "SentryException.h" +#import "SentryHub+Private.h" #import "SentryLog.h" +#import "SentryMechanism.h" +#import "SentrySDK+Private.h" +#import "SentryThread.h" +#import "SentryThreadInspector.h" #import "SentryThreadWrapper.h" #import #import @@ -13,20 +22,11 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - -/** - * As we only use the ANR tracking integration for detecting falsely reported OOMs we can use a more - * defensive value, because we are not reporting any ANRs. - */ -static NSUInteger const SENTRY_ANR_TRACKER_TIMEOUT_MILLIS = 2000; - @interface SentryANRTrackingIntegration () @property (nonatomic, strong) SentryANRTracker *tracker; -@property (nonatomic, strong) SentryAppStateManager *appStateManager; -@property (nonatomic, strong) SentryCrashWrapper *crashWrapper; +@property (nonatomic, strong) SentryOptions *options; @end @@ -34,34 +34,36 @@ @implementation SentryANRTrackingIntegration - (void)installWithOptions:(SentryOptions *)options { - SentryDependencyContainer *dependencies = [SentryDependencyContainer sharedInstance]; - self.crashWrapper = dependencies.crashWrapper; - if ([self shouldBeDisabled:options]) { [options removeEnabledIntegration:NSStringFromClass([self class])]; return; } - self.appStateManager = dependencies.appStateManager; - self.tracker = - [[SentryANRTracker alloc] initWithDelegate:self - timeoutIntervalMillis:SENTRY_ANR_TRACKER_TIMEOUT_MILLIS - currentDateProvider:[SentryDefaultCurrentDateProvider sharedInstance] - crashWrapper:dependencies.crashWrapper - dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] - threadWrapper:dependencies.threadWrapper]; - [self.tracker start]; + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval]; + + [self.tracker addListener:self]; + self.options = options; } - (BOOL)shouldBeDisabled:(SentryOptions *)options { - if (!options.enableOutOfMemoryTracking) { + if (!options.enableAppHangTracking) { + [SentryLog logWithMessage:@"Not going to enable App Hanging integration because " + @"enableAppHangsTracking is disabled." + andLevel:kSentryLevelDebug]; + return YES; + } + + if (options.appHangTimeoutInterval == 0) { + [SentryLog logWithMessage:@"Not going to enable App Hanging integration because " + @"appHangsTimeoutInterval is 0." + andLevel:kSentryLevelDebug]; return YES; } // In case the debugger is attached - if ([self.crashWrapper isBeingTraced]) { + if ([SentryDependencyContainer.sharedInstance.crashWrapper isBeingTraced]) { return YES; } @@ -70,23 +72,37 @@ - (BOOL)shouldBeDisabled:(SentryOptions *)options - (void)uninstall { - [self.tracker stop]; + [self.tracker removeListener:self]; } - (void)anrDetected { - [self.appStateManager - updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }]; + SentryThreadInspector *threadInspector = SentrySDK.currentHub.getClient.threadInspector; + + NSString *message = [NSString stringWithFormat:@"App hanging for at least %li ms.", + (long)(self.options.appHangTimeoutInterval * 1000)]; + + NSArray *threads = [threadInspector getCurrentThreadsWithStackTrace:YES]; + + SentryEvent *event = [[SentryEvent alloc] initWithLevel:kSentryLevelError]; + SentryException *sentryException = [[SentryException alloc] initWithValue:message + type:@"App Hanging"]; + sentryException.mechanism = [[SentryMechanism alloc] initWithType:@"AppHang"]; + sentryException.stacktrace = [threads[0] stacktrace]; + [threads enumerateObjectsUsingBlock:^(SentryThread *_Nonnull obj, NSUInteger idx, + BOOL *_Nonnull stop) { obj.current = [NSNumber numberWithBool:idx == 0]; }]; + + event.exceptions = @[ sentryException ]; + event.threads = threads; + + [SentrySDK captureEvent:event]; } - (void)anrStopped { - [self.appStateManager - updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }]; + // We dont report when an ANR ends. } @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 72686b50cbc..c9a42dba3ff 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -477,6 +477,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event || (nil != event.exceptions && [event.exceptions count] > 0); BOOL threadsNotAttached = !(nil != event.threads && event.threads.count > 0); + if (!isCrashEvent && shouldAttachStacktrace && threadsNotAttached) { event.threads = [self.threadInspector getCurrentThreads]; } diff --git a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m index 77001bbb729..b2f04ccb676 100644 --- a/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m +++ b/Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m @@ -12,11 +12,19 @@ #import "SentryThread.h" #import #include +#include NS_ASSUME_NONNULL_BEGIN +SentryCrashThread mainThreadID; + @implementation SentryCrashDefaultMachineContextWrapper ++ (void)initialize +{ + mainThreadID = pthread_mach_thread_np(pthread_self()); +} + - (void)fillContextForCurrentThread:(struct SentryCrashMachineContext *)context { sentrycrashmc_getContextForThread(sentrycrashthread_self(), context, true); @@ -40,6 +48,11 @@ - (void)getThreadName:(const SentryCrashThread)thread sentrycrashthread_getThreadName(thread, buffer, bufLength); } +- (BOOL)isMainThread:(SentryCrashThread)thread +{ + return thread == mainThreadID; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryDependencyContainer.m b/Sources/Sentry/SentryDependencyContainer.m index 8254af846dd..014bacb2a33 100644 --- a/Sources/Sentry/SentryDependencyContainer.m +++ b/Sources/Sentry/SentryDependencyContainer.m @@ -1,3 +1,6 @@ +#import "SentryANRTracker.h" +#import "SentryDefaultCurrentDateProvider.h" +#import "SentryDispatchQueueWrapper.h" #import "SentryUIApplication.h" #import #import @@ -157,4 +160,21 @@ - (SentryDebugImageProvider *)debugImageProvider return _debugImageProvider; } +- (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout +{ + if (_anrTracker == nil) { + @synchronized(sentryDependencyContainerLock) { + if (_anrTracker == nil) { + _anrTracker = [[SentryANRTracker alloc] + initWithTimeoutInterval:timeout + currentDateProvider:[SentryDefaultCurrentDateProvider sharedInstance] + crashWrapper:self.crashWrapper + dispatchQueueWrapper:[[SentryDispatchQueueWrapper alloc] init] + threadWrapper:self.threadWrapper]; + } + } + } + return _anrTracker; +} + @end diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 9f64f77fe2a..0d98fa9e2a8 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -1,4 +1,5 @@ #import "SentryOptions.h" +#import "SentryANRTracker.h" #import "SentryDsn.h" #import "SentryLog.h" #import "SentryMeta.h" @@ -58,6 +59,9 @@ - (instancetype)init self.enableUserInteractionTracing = NO; self.idleTimeout = 3.0; #endif + self.enableAppHangTracking = NO; + self.appHangTimeoutInterval = 2.0; + self.enableNetworkTracking = YES; self.enableFileIOTracking = NO; self.enableNetworkBreadcrumbs = YES; @@ -243,8 +247,16 @@ - (BOOL)validateOptions:(NSDictionary *)options if ([options[@"idleTimeout"] isKindOfClass:[NSNumber class]]) { self.idleTimeout = [options[@"idleTimeout"] doubleValue]; } + #endif + [self setBool:options[@"enableAppHangTracking"] + block:^(BOOL value) { self->_enableAppHangTracking = value; }]; + + if ([options[@"appHangTimeoutInterval"] isKindOfClass:[NSNumber class]]) { + self.appHangTimeoutInterval = [options[@"appHangTimeoutInterval"] doubleValue]; + } + [self setBool:options[@"enableNetworkTracking"] block:^(BOOL value) { self->_enableNetworkTracking = value; }]; diff --git a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m index a0d440b2cca..84bb2209712 100644 --- a/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m +++ b/Sources/Sentry/SentryOutOfMemoryTrackingIntegration.m @@ -1,4 +1,6 @@ +#import "SentryDefines.h" #import +#import #import #import #import @@ -19,7 +21,9 @@ SentryOutOfMemoryTrackingIntegration () @property (nonatomic, strong) SentryOutOfMemoryTracker *tracker; +@property (nonatomic, strong) SentryANRTracker *anrTracker; @property (nullable, nonatomic, copy) NSString *testConfigurationFilePath; +@property (nonatomic, strong) SentryAppStateManager *appStateManager; @end @@ -62,6 +66,12 @@ - (void)installWithOptions:(SentryOptions *)options dispatchQueueWrapper:dispatchQueueWrapper fileManager:fileManager]; [self.tracker start]; + + self.anrTracker = + [SentryDependencyContainer.sharedInstance getANRTracker:options.appHangTimeoutInterval]; + [self.anrTracker addListener:self]; + + self.appStateManager = appStateManager; } - (BOOL)shouldBeDisabled:(SentryOptions *)options @@ -86,6 +96,23 @@ - (void)uninstall if (nil != self.tracker) { [self.tracker stop]; } + [self.anrTracker removeListener:self]; +} + +- (void)anrDetected +{ +#if SENTRY_HAS_UIKIT + [self.appStateManager + updateAppState:^(SentryAppState *appState) { appState.isANROngoing = YES; }]; +#endif +} + +- (void)anrStopped +{ +#if SENTRY_HAS_UIKIT + [self.appStateManager + updateAppState:^(SentryAppState *appState) { appState.isANROngoing = NO; }]; +#endif } @end diff --git a/Sources/Sentry/SentryStacktraceBuilder.m b/Sources/Sentry/SentryStacktraceBuilder.m index fa0ead21ba1..d393863e29b 100644 --- a/Sources/Sentry/SentryStacktraceBuilder.m +++ b/Sources/Sentry/SentryStacktraceBuilder.m @@ -1,5 +1,6 @@ #import "SentryStacktraceBuilder.h" #import "SentryCrashStackCursor.h" +#import "SentryCrashStackCursor_MachineContext.h" #import "SentryCrashStackCursor_SelfThread.h" #import "SentryCrashStackEntryMapper.h" #import "SentryFrame.h" @@ -25,15 +26,9 @@ - (id)initWithCrashStackEntryMapper:(SentryCrashStackEntryMapper *)crashStackEnt return self; } -- (SentryStacktrace *)buildStacktraceForCurrentThread +- (SentryStacktrace *)retrieveStacktraceFromCursor:(SentryCrashStackCursor)stackCursor { NSMutableArray *frames = [NSMutableArray new]; - - SentryCrashStackCursor stackCursor; - // We don't need to skip any frames, because we filter out non sentry frames below. - NSInteger framesToSkip = 0; - sentrycrashsc_initSelfThread(&stackCursor, (int)framesToSkip); - SentryFrame *frame = nil; while (stackCursor.advanceCursor(&stackCursor)) { if (stackCursor.symbolicate(&stackCursor)) { @@ -61,6 +56,26 @@ - (SentryStacktrace *)buildStacktraceForCurrentThread return stacktrace; } +- (SentryStacktrace *)buildStacktraceForThread:(SentryCrashThread)thread +{ + SentryCrashMC_NEW_CONTEXT(machineContext); + sentrycrashmc_getContextForThread(thread, machineContext, false); + SentryCrashStackCursor stackCursor; + sentrycrashsc_initWithMachineContext(&stackCursor, 100, machineContext); + + return [self retrieveStacktraceFromCursor:stackCursor]; +} + +- (SentryStacktrace *)buildStacktraceForCurrentThread +{ + SentryCrashStackCursor stackCursor; + // We don't need to skip any frames, because we filter out non sentry frames below. + NSInteger framesToSkip = 0; + sentrycrashsc_initSelfThread(&stackCursor, (int)framesToSkip); + + return [self retrieveStacktraceFromCursor:stackCursor]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryThreadInspector.m b/Sources/Sentry/SentryThreadInspector.m index e338b226415..3839add1d0d 100644 --- a/Sources/Sentry/SentryThreadInspector.m +++ b/Sources/Sentry/SentryThreadInspector.m @@ -3,6 +3,7 @@ #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" #import "SentryThread.h" +#include @interface SentryThreadInspector () @@ -25,6 +26,11 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder } - (NSArray *)getCurrentThreads +{ + return [self getCurrentThreadsWithStackTrace:NO]; +} + +- (NSArray *)getCurrentThreadsWithStackTrace:(BOOL)getAllStacktraces { NSMutableArray *threads = [NSMutableArray new]; @@ -33,6 +39,12 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder int threadCount = [self.machineContextWrapper getThreadCount:context]; + thread_act_array_t suspendedThreads = nil; + mach_msg_type_number_t numSuspendedThreads = 0; + if (getAllStacktraces) { + sentrycrashmc_suspendEnvironment(&suspendedThreads, &numSuspendedThreads); + } + for (int i = 0; i < threadCount; i++) { SentryCrashThread thread = [self.machineContextWrapper getThread:context withIndex:i]; SentryThread *sentryThread = [[SentryThread alloc] initWithThreadId:@(i)]; @@ -43,12 +55,21 @@ - (id)initWithStacktraceBuilder:(SentryStacktraceBuilder *)stacktraceBuilder bool isCurrent = thread == sentrycrashthread_self(); sentryThread.current = @(isCurrent); - // For now we can only retrieve the stack trace of the current thread. if (isCurrent) { sentryThread.stacktrace = [self.stacktraceBuilder buildStacktraceForCurrentThread]; + } else if (getAllStacktraces) { + sentryThread.stacktrace = [self.stacktraceBuilder buildStacktraceForThread:thread]; } - [threads addObject:sentryThread]; + // We need to make sure the main thread is always the first thread in the result + if ([self.machineContextWrapper isMainThread:thread]) + [threads insertObject:sentryThread atIndex:0]; + else + [threads addObject:sentryThread]; + } + + if (numSuspendedThreads > 0) { + sentrycrashmc_resumeEnvironment(suspendedThreads, numSuspendedThreads); } return threads; diff --git a/Sources/Sentry/include/SentryANRTracker.h b/Sources/Sentry/include/SentryANRTracker.h index e43fa8b3def..2f2779cad23 100644 --- a/Sources/Sentry/include/SentryANRTracker.h +++ b/Sources/Sentry/include/SentryANRTracker.h @@ -5,8 +5,6 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @protocol SentryANRTrackerDelegate; /** @@ -26,16 +24,18 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryANRTracker : NSObject SENTRY_NO_INIT -- (instancetype)initWithDelegate:(id)delegate - timeoutIntervalMillis:(NSUInteger)timeoutIntervalMillis - currentDateProvider:(id)currentDateProvider - crashWrapper:(SentryCrashWrapper *)crashWrapper - dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper - threadWrapper:(SentryThreadWrapper *)threadWrapper; +- (instancetype)initWithTimeoutInterval:(NSTimeInterval)timeoutInterval + currentDateProvider:(id)currentDateProvider + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueueWrapper:(SentryDispatchQueueWrapper *)dispatchQueueWrapper + threadWrapper:(SentryThreadWrapper *)threadWrapper; + +- (void)addListener:(id)listener; -- (void)start; +- (void)removeListener:(id)listener; -- (void)stop; +// Function used for tests +- (void)clear; @end @@ -47,6 +47,4 @@ SENTRY_NO_INIT @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryANRTrackingIntegration.h b/Sources/Sentry/include/SentryANRTrackingIntegration.h index 6d4cce6baa6..803963e1b6a 100644 --- a/Sources/Sentry/include/SentryANRTrackingIntegration.h +++ b/Sources/Sentry/include/SentryANRTrackingIntegration.h @@ -4,13 +4,9 @@ NS_ASSUME_NONNULL_BEGIN -#if SENTRY_HAS_UIKIT - @interface SentryANRTrackingIntegration : NSObject @end -#endif - NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index aa65ee48a1d..c6a85a98b49 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -3,7 +3,7 @@ #import "SentryDiscardReason.h" #import -@class SentryEnvelopeItem, SentryId, SentryAttachment; +@class SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector; NS_ASSUME_NONNULL_BEGIN @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN SentryClient (Private) @property (nonatomic, weak) id attachmentProcessor; +@property (nonatomic, strong) SentryThreadInspector *threadInspector; - (SentryFileManager *)fileManager; diff --git a/Sources/Sentry/include/SentryCrashMachineContextWrapper.h b/Sources/Sentry/include/SentryCrashMachineContextWrapper.h index 96f8e5adf17..96138f9fb32 100644 --- a/Sources/Sentry/include/SentryCrashMachineContextWrapper.h +++ b/Sources/Sentry/include/SentryCrashMachineContextWrapper.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN andBuffer:(char *const)buffer andBufLength:(int)bufLength; +- (BOOL)isMainThread:(SentryCrashThread)thread; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryDependencyContainer.h b/Sources/Sentry/include/SentryDependencyContainer.h index 4f063925263..ad5778eb7dd 100644 --- a/Sources/Sentry/include/SentryDependencyContainer.h +++ b/Sources/Sentry/include/SentryDependencyContainer.h @@ -2,8 +2,8 @@ #import "SentryRandom.h" #import -@class SentryAppStateManager, SentryCrashWrapper, SentryThreadWrapper, SentryDispatchQueueWrapper, - SentrySwizzleWrapper, SentryDebugImageProvider; +@class SentryAppStateManager, SentryCrashWrapper, SentryThreadWrapper, SentrySwizzleWrapper, + SentryDispatchQueueWrapper, SentryDebugImageProvider, SentryANRTracker; #if SENTRY_HAS_UIKIT @class SentryScreenshot, SentryUIApplication; @@ -28,12 +28,15 @@ SENTRY_NO_INIT @property (nonatomic, strong) SentrySwizzleWrapper *swizzleWrapper; @property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper; @property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; +@property (nonatomic, strong) SentryANRTracker *anrTracker; #if SENTRY_HAS_UIKIT @property (nonatomic, strong) SentryScreenshot *screenshot; @property (nonatomic, strong) SentryUIApplication *application; #endif +- (SentryANRTracker *)getANRTracker:(NSTimeInterval)timeout; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h b/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h index f84e60f6e32..8940f647909 100644 --- a/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h +++ b/Sources/Sentry/include/SentryOutOfMemoryTrackingIntegration.h @@ -1,9 +1,11 @@ +#import "SentryANRTracker.h" #import "SentryIntegrationProtocol.h" #import NS_ASSUME_NONNULL_BEGIN -@interface SentryOutOfMemoryTrackingIntegration : NSObject +@interface SentryOutOfMemoryTrackingIntegration + : NSObject @end diff --git a/Sources/Sentry/include/SentryStacktraceBuilder.h b/Sources/Sentry/include/SentryStacktraceBuilder.h index 706ea50596d..0231030af26 100644 --- a/Sources/Sentry/include/SentryStacktraceBuilder.h +++ b/Sources/Sentry/include/SentryStacktraceBuilder.h @@ -1,3 +1,4 @@ +#include "SentryCrashThread.h" #import "SentryDefines.h" #import @@ -20,6 +21,13 @@ SENTRY_NO_INIT */ - (SentryStacktrace *)buildStacktraceForCurrentThread; +/** + * Builds the stacktrace for given thread removing frames from the SentrySDK until frames from + * a different package are found. When including Sentry via the Swift Package Manager the package is + * the same as the application that includes Sentry. In this case the full stacktrace is returned + * without skipping frames. + */ +- (SentryStacktrace *)buildStacktraceForThread:(SentryCrashThread)thread; @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryThreadInspector.h b/Sources/Sentry/include/SentryThreadInspector.h index 258421a907a..4ce241f79d0 100644 --- a/Sources/Sentry/include/SentryThreadInspector.h +++ b/Sources/Sentry/include/SentryThreadInspector.h @@ -15,9 +15,19 @@ SENTRY_NO_INIT /** * Gets current threads with the stacktrace only for the current thread. Frames from the SentrySDK * are not included. For more details checkout SentryStacktraceBuilder. + * The first thread in the result is always the main thread. */ - (NSArray *)getCurrentThreads; +/** + * Gets current threads. The calling thread always has the stacktrace information. + * It is possible to get stacktrace for all threads passing YES in `getAllStacktraces`, + * this will pause every thread in order to be possible to retrieve this information. + * Frames from the SentrySDK are not included. For more details checkout SentryStacktraceBuilder. + * The first thread in the result is always the main thread. + */ +- (NSArray *)getCurrentThreadsWithStackTrace:(BOOL)getAllStacktraces; + @end NS_ASSUME_NONNULL_END diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift index 0e5faeb9755..f23098b0753 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackerTests.swift @@ -7,7 +7,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { private var fixture: Fixture! private var anrDetectedExpectation: XCTestExpectation! private var anrStoppedExpectation: XCTestExpectation! - private let waitTimeout: TimeInterval = 0.05 + private let waitTimeout: TimeInterval = 0.1 private class Fixture { let timeoutInterval: TimeInterval = 5 @@ -30,17 +30,21 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { fixture = Fixture() - sut = SentryANRTracker(delegate: self, - timeoutIntervalMillis: UInt(fixture.timeoutInterval) * 1_000, - currentDateProvider: fixture.currentDate, - crashWrapper: fixture.crashWrapper, - dispatchQueueWrapper: fixture.dispatchQueue, - threadWrapper: fixture.threadWrapper) + sut = SentryANRTracker( + timeoutInterval: fixture.timeoutInterval, + currentDateProvider: fixture.currentDate, + crashWrapper: fixture.crashWrapper, + dispatchQueueWrapper: fixture.dispatchQueue, + threadWrapper: fixture.threadWrapper) } override func tearDown() { super.tearDown() - sut.stop() + sut.clear() + } + + func start() { + sut.addListener(self) } func testContinousANR_OneReported() { @@ -48,11 +52,25 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: self.fixture.timeoutInterval) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } + func testMultipleListeners() { + fixture.dispatchQueue.blockBeforeMainBlock = { + self.advanceTime(bySeconds: self.fixture.timeoutInterval) + return false + } + + let secondListener = SentryANRTrackerTestDelegate() + sut.addListener(secondListener) + + start() + + wait(for: [anrDetectedExpectation, anrStoppedExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout) + } + func testANRButAppInBackground_NoANR() { anrDetectedExpectation.isInverted = true fixture.crashWrapper.internalIsApplicationInForeground = false @@ -61,7 +79,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: self.fixture.timeoutInterval) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } @@ -80,7 +98,7 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } @@ -92,26 +110,46 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { self.advanceTime(bySeconds: delta) return false } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation], timeout: waitTimeout) } - func testStop_StopsReportingANRs() { + func testRemoveListener_StopsReportingANRs() { anrDetectedExpectation.isInverted = true let mainBlockExpectation = expectation(description: "Main Block") + fixture.dispatchQueue.blockBeforeMainBlock = { - self.sut.stop() + self.sut.removeListener(self) mainBlockExpectation.fulfill() return true } - sut.start() + start() wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation], timeout: waitTimeout) } + func testClear_StopsReportingANRs() { + let secondListener = SentryANRTrackerTestDelegate() + secondListener.anrDetectedExpectation.isInverted = true + anrDetectedExpectation.isInverted = true + + let mainBlockExpectation = expectation(description: "Main Block") + + fixture.dispatchQueue.blockBeforeMainBlock = { + self.sut.clear() + mainBlockExpectation.fulfill() + return true + } + + sut.addListener(secondListener) + start() + + wait(for: [anrDetectedExpectation, anrStoppedExpectation, mainBlockExpectation, secondListener.anrStoppedExpectation, secondListener.anrDetectedExpectation], timeout: waitTimeout) + } + func anrDetected() { anrDetectedExpectation.fulfill() } @@ -124,4 +162,23 @@ class SentryANRTrackerTests: XCTestCase, SentryANRTrackerDelegate { fixture.currentDate.setDate(date: fixture.currentDate.date().addingTimeInterval(bySeconds)) } } + +class SentryANRTrackerTestDelegate: NSObject, SentryANRTrackerDelegate { + + let anrDetectedExpectation = XCTestExpectation(description: "Test Delegate ANR Detection") + let anrStoppedExpectation = XCTestExpectation(description: "Test Delegate ANR Stopped") + + override init() { + anrStoppedExpectation.isInverted = true + } + + func anrStopped() { + anrStoppedExpectation.fulfill() + } + + func anrDetected() { + anrDetectedExpectation.fulfill() + } +} + #endif diff --git a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift index 643d5d5ae5a..413b4c5e554 100644 --- a/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/ANR/SentryANRTrackingIntegrationTests.swift @@ -1,65 +1,50 @@ import XCTest -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -class SentryANRTrackingIntegrationTests: XCTestCase { - - private static let dsn = TestConstants.dsnAsString(username: "SentryANRTrackingIntegrationTests") +class SentryANRTrackingIntegrationTests: SentrySDKIntegrationTestsBase { private class Fixture { let options: Options - let client: TestClient! - let crashWrapper: TestSentryCrashWrapper - let currentDate = TestCurrentDateProvider() - let fileManager: SentryFileManager + let currentDate = TestCurrentDateProvider() + init() { options = Options() - options.dsn = SentryANRTrackingIntegrationTests.dsn - - client = TestClient(options: options) - - crashWrapper = TestSentryCrashWrapper.sharedInstance() - SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper - - let hub = SentryHub(client: client, andScope: nil, andCrashWrapper: crashWrapper, andCurrentDateProvider: currentDate) - SentrySDK.setCurrentHub(hub) - - fileManager = try! SentryFileManager(options: options, andCurrentDateProvider: currentDate) + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 4.5 } } private var fixture: Fixture! private var sut: SentryANRTrackingIntegration! + override var options: Options { + self.fixture.options + } + override func setUp() { super.setUp() - fixture = Fixture() - fixture.fileManager.store(TestData.appState) } override func tearDown() { - super.tearDown() sut.uninstall() - fixture.fileManager.deleteAllFolders() clearTestState() + super.tearDown() } func testWhenBeingTraced_TrackerNotInitialized() { givenInitializedTracker(isBeingTraced: true) - XCTAssertNil(Dynamic(sut).tracker.asAnyObject) } func testWhenNoDebuggerAttached_TrackerInitialized() { givenInitializedTracker() - XCTAssertNotNil(Dynamic(sut).tracker.asAnyObject) } - func test_OOMDisabled_RemovesEnabledIntegration() { + func test_enableAppHangsTracking_Disabled_RemovesEnabledIntegration() { let options = Options() - options.enableOutOfMemoryTracking = false + options.enableAppHangTracking = false sut = SentryANRTrackingIntegration() sut.install(with: options) @@ -68,38 +53,82 @@ class SentryANRTrackingIntegrationTests: XCTestCase { assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } - func testANRDetected_UpdatesAppStateToTrue() { - givenInitializedTracker() + func test_appHangsTimeoutInterval_Zero_RemovesEnabledIntegration() { + let options = Options() + options.enableAppHangTracking = true + options.appHangTimeoutInterval = 0 - Dynamic(sut).anrDetected() + sut = SentryANRTrackingIntegration() + sut.install(with: options) - guard let appState = fixture.fileManager.readAppState() else { - XCTFail("appState must not be nil") - return - } - - XCTAssertTrue(appState.isANROngoing) + let expexted = Options.defaultIntegrations().filter { !$0.contains("ANRTracking") } + assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } - func testANRStopped_UpdatesAppStateToFalse() { + func testANRDetected_EventCaptured() { givenInitializedTracker() + setUpThreadInspector() - Dynamic(sut).anrStopped() + Dynamic(sut).anrDetected() - guard let appState = fixture.fileManager.readAppState() else { - XCTFail("appState must not be nil") - return + 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") + XCTAssertEqual(ex.value, "App hanging for at least 4500 ms.") + XCTAssertNotNil(ex.stacktrace) + XCTAssertEqual(ex.stacktrace?.frames.first?.function, "main") + XCTAssertTrue(event?.threads?[0].current?.boolValue ?? false) + + 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") } - XCTAssertFalse(appState.isANROngoing) } private func givenInitializedTracker(isBeingTraced: Bool = false) { - fixture.crashWrapper.internalIsBeingTraced = isBeingTraced + givenSdkWithHub() + self.crashWrapper.internalIsBeingTraced = isBeingTraced sut = SentryANRTrackingIntegration() - let options = Options() - Dynamic(sut).setTestConfigurationFilePath(nil) - sut.install(with: options) + sut.install(with: self.options) + } + + private func setUpThreadInspector() { + let threadInspector = TestThreadInspector.instance + + let frame1 = Sentry.Frame() + frame1.function = "Second_frame_function" + + let thread1 = Sentry.Thread(threadId: 0) + thread1.stacktrace = Stacktrace(frames: [frame1], registers: [:]) + thread1.current = true + + let frame2 = Sentry.Frame() + frame2.function = "main" + + let thread2 = Sentry.Thread(threadId: 1) + thread2.stacktrace = Stacktrace(frames: [frame2], registers: [:]) + thread2.current = false + + threadInspector.allThreds = [ + thread2, + thread1 + ] + + SentrySDK.currentHub().getClient()?.threadInspector = threadInspector } } - -#endif diff --git a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift index adbce9511ed..6216e053f37 100644 --- a/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/OutOfMemory/SentryOutOfMemoryIntegrationTests.swift @@ -2,6 +2,45 @@ import XCTest class SentryOutOfMemoryIntegrationTests: XCTestCase { + private class Fixture { + let options: Options + let client: TestClient! + let crashWrapper: TestSentryCrashWrapper + let currentDate = TestCurrentDateProvider() + let fileManager: SentryFileManager + + init() { + options = Options() + + client = TestClient(options: options) + + crashWrapper = TestSentryCrashWrapper.sharedInstance() + SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper + + let hub = SentryHub(client: client, andScope: nil, andCrashWrapper: crashWrapper, andCurrentDateProvider: currentDate) + SentrySDK.setCurrentHub(hub) + + fileManager = try! SentryFileManager(options: options, andCurrentDateProvider: currentDate) + } + } + + private var fixture: Fixture! + private var sut: SentryOutOfMemoryTrackingIntegration! + + override func setUp() { + super.setUp() + + fixture = Fixture() + fixture.fileManager.store(TestData.appState) + } + + override func tearDown() { + sut?.uninstall() + fixture.fileManager.deleteAllFolders() + clearTestState() + super.tearDown() + } + func testWhenUnitTests_TrackerNotInitialized() { let sut = SentryOutOfMemoryTrackingIntegration() sut.install(with: Options()) @@ -23,7 +62,35 @@ class SentryOutOfMemoryIntegrationTests: XCTestCase { XCTAssertEqual(path, ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"]) } +#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) + func testANRDetected_UpdatesAppStateToTrue() { + givenInitializedTracker() + + Dynamic(sut).anrDetected() + + guard let appState = fixture.fileManager.readAppState() else { + XCTFail("appState must not be nil") + return + } + + XCTAssertTrue(appState.isANROngoing) + } +#endif + + func testANRStopped_UpdatesAppStateToFalse() { + givenInitializedTracker() + + Dynamic(sut).anrStopped() + + guard let appState = fixture.fileManager.readAppState() else { + XCTFail("appState must not be nil") + return + } + XCTAssertFalse(appState.isANROngoing) + } + func test_OOMDisabled_RemovesEnabledIntegration() { + givenInitializedTracker() let options = Options() options.enableOutOfMemoryTracking = false @@ -33,4 +100,13 @@ class SentryOutOfMemoryIntegrationTests: XCTestCase { let expexted = Options.defaultIntegrations().filter { !$0.contains("OutOfMemory") } assertArrayEquals(expected: expexted, actual: Array(options.enabledIntegrations)) } + + private func givenInitializedTracker(isBeingTraced: Bool = false) { + fixture.crashWrapper.internalIsBeingTraced = isBeingTraced + sut = SentryOutOfMemoryTrackingIntegration() + let options = Options() + Dynamic(sut).setTestConfigurationFilePath(nil) + sut.install(with: options) + } + } diff --git a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift index 8a92c09ccbb..40cc04796f8 100644 --- a/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryThreadInspectorTests.swift @@ -1,17 +1,20 @@ @testable import Sentry +import SwiftUI import XCTest class SentryThreadInspectorTests: XCTestCase { private class Fixture { var testMachineContextWrapper = TestMachineContextWrapper() + var stacktraceBuilder = TestSentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))) func getSut(testWithRealMachineConextWrapper: Bool = false) -> SentryThreadInspector { let machineContextWrapper = testWithRealMachineConextWrapper ? SentryCrashDefaultMachineContextWrapper() : testMachineContextWrapper as SentryCrashMachineContextWrapper + let stacktraceBuilder = testWithRealMachineConextWrapper ? SentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))) : self.stacktraceBuilder return SentryThreadInspector( - stacktraceBuilder: SentryStacktraceBuilder(crashStackEntryMapper: SentryCrashStackEntryMapper(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []))), + stacktraceBuilder: stacktraceBuilder, andMachineContextWrapper: machineContextWrapper ) } @@ -37,6 +40,22 @@ class SentryThreadInspectorTests: XCTestCase { XCTAssertTrue(30 < stacktrace?.frames.count ?? 0, "Not enough stacktrace frames.") } + func testStacktraceHasFrames_forEveryThread() { + let actual = fixture.getSut(testWithRealMachineConextWrapper: true).getCurrentThreads(withStackTrace: true) + + //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 + var threadsWithFrames = 0 + + for thr in actual { + if (thr.stacktrace?.frames.count ?? 0) >= 1 { + threadsWithFrames += 1 + } + } + + XCTAssertTrue(threadsWithFrames > 1, "Not enough threads with frames") + } + func testOnlyCurrentThreadHasStacktrace() { let actual = fixture.getSut(testWithRealMachineConextWrapper: true).getCurrentThreads() XCTAssertEqual(true, actual[0].current) @@ -111,9 +130,36 @@ class SentryThreadInspectorTests: XCTestCase { let thread = actual[0] XCTAssertEqual(threadName, thread.name) } + + func testMainThreadAsFirstThread() { + fixture.testMachineContextWrapper.mockThreads = [ ThreadInfo(threadId: 2, name: "Second Thread"), ThreadInfo(threadId: 1, name: "main") ] + fixture.testMachineContextWrapper.mainThread = 1 + fixture.testMachineContextWrapper.threadCount = 2 + + let sut = fixture.getSut() + let threads = sut.getCurrentThreads() + + XCTAssertEqual(threads[0].name, "main") + XCTAssertEqual(threads[1].name, "Second Thread") + } +} + +private class TestSentryStacktraceBuilder: SentryStacktraceBuilder { + + var stackTraces = [SentryCrashThread: Stacktrace]() + override func buildStacktrace(forThread thread: SentryCrashThread) -> Stacktrace { + return stackTraces[thread] ?? Stacktrace(frames: [], registers: [:]) + } + +} + +private struct ThreadInfo { + var threadId: SentryCrashThread + var name: String } private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrapper { + func fillContext(forCurrentThread context: OpaquePointer) { // Do nothing } @@ -123,13 +169,16 @@ private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrap threadCount } + var mockThreads: [ThreadInfo]? func getThread(_ context: OpaquePointer, with index: Int32) -> SentryCrashThread { - 0 + mockThreads?[Int(index)].threadId ?? 0 } var threadName: String? = "" func getThreadName(_ thread: SentryCrashThread, andBuffer buffer: UnsafeMutablePointer, andBufLength bufLength: Int32) { - if threadName != nil { + if let mocks = mockThreads, let index = mocks.firstIndex(where: { $0.threadId == thread }) { + strcpy(buffer, mocks[index].name) + } else if threadName != nil { strcpy(buffer, threadName) } else { _ = Array(repeating: 0, count: Int(bufLength)).withUnsafeBufferPointer { bufferPointer in @@ -137,4 +186,9 @@ private class TestMachineContextWrapper: NSObject, SentryCrashMachineContextWrap } } } + + var mainThread: SentryCrashThread? + func isMainThread(_ thread: SentryCrashThread) -> Bool { + return thread == mainThread + } } diff --git a/Tests/SentryTests/SentryCrash/TestThreadInspector.swift b/Tests/SentryTests/SentryCrash/TestThreadInspector.swift index a796e5e3290..4c85b38d867 100644 --- a/Tests/SentryTests/SentryCrash/TestThreadInspector.swift +++ b/Tests/SentryTests/SentryCrash/TestThreadInspector.swift @@ -2,6 +2,8 @@ import Foundation class TestThreadInspector: SentryThreadInspector { + var allThreds: [Sentry.Thread]? + static var instance: TestThreadInspector { // We need something to pass to the super initializer, because the empty initializer has been marked unavailable. let inAppLogic = SentryInAppLogic(inAppIncludes: [], inAppExcludes: []) @@ -11,7 +13,11 @@ class TestThreadInspector: SentryThreadInspector { } override func getCurrentThreads() -> [Sentry.Thread] { - return [TestData.thread] + return allThreds ?? [TestData.thread] + } + + override func getCurrentThreads(withStackTrace getAllStacktraces: Bool) -> [Sentry.Thread] { + return allThreds ?? [TestData.thread] } } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 5ddfe69e1a5..02b771b832b 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -495,6 +495,8 @@ - (void)testNSNull_SetsDefaultValue @"enableUIViewControllerTracking" : [NSNull null], @"attachScreenshot" : [NSNull null], #endif + @"enableAppHangTracking" : [NSNull null], + @"appHangTimeoutInterval" : [NSNull null], @"enableNetworkTracking" : [NSNull null], @"tracesSampleRate" : [NSNull null], @"tracesSampler" : [NSNull null], @@ -541,6 +543,8 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertFalse(options.attachScreenshot); XCTAssertEqual(3.0, options.idleTimeout); #endif + XCTAssertFalse(options.enableAppHangTracking); + XCTAssertEqual(options.appHangTimeoutInterval, 2); XCTAssertEqual(YES, options.enableNetworkTracking); XCTAssertNil(options.tracesSampleRate); XCTAssertNil(options.tracesSampler); @@ -692,6 +696,17 @@ - (void)testIdleTimeout #endif +- (void)testEnableAppHangTracking +{ + [self testBooleanField:@"enableAppHangTracking" defaultValue:NO]; +} + +- (void)testDefaultAppHangsTimeout +{ + SentryOptions *options = [self getValidOptions:@{}]; + XCTAssertEqual(2, options.appHangTimeoutInterval); +} + - (void)testEnableNetworkTracking { [self testBooleanField:@"enableNetworkTracking"]; diff --git a/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift b/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift new file mode 100644 index 00000000000..6b3ecb8497e --- /dev/null +++ b/Tests/SentryTests/SentrySDKIntegrationTestsBase.swift @@ -0,0 +1,92 @@ +import Foundation +import XCTest + +class SentrySDKIntegrationTestsBase: XCTestCase { + + var currentDate = TestCurrentDateProvider() + var crashWrapper: TestSentryCrashWrapper! + + var options: Options { + Options() + } + + override func setUp() { + super.setUp() + crashWrapper = TestSentryCrashWrapper.sharedInstance() + SentryDependencyContainer.sharedInstance().crashWrapper = crashWrapper + currentDate = TestCurrentDateProvider() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func givenSdkWithHub(_ options: Options? = nil) { + let client = TestClient(options: options ?? self.options)! + let hub = SentryHub(client: client, andScope: Scope(), andCrashWrapper: TestSentryCrashWrapper.sharedInstance(), andCurrentDateProvider: currentDate) + + SentrySDK.setCurrentHub(hub) + } + + func givenSdkWithHubButNoClient() { + SentrySDK.setCurrentHub(SentryHub(client: nil, andScope: nil)) + } + + func assertEventCaptured(_ callback: (Event?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + XCTAssertEqual(1, client.captureEventInvocations.count, "More than one `Event` captured.") + callback(client.captureEventInvocations.first) + } + + func assertEventWithScopeCaptured(_ callback: (Event?, Scope?, [SentryEnvelopeItem]?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureEventWithScopeInvocations.count, "More than one `Event` captured.") + let capture = client.captureEventWithScopeInvocations.first + callback(capture?.event, capture?.scope, capture?.additionalEnvelopeItems) + } + + func lastErrorWithScopeCaptured(_ callback: (Error?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureErrorWithScopeInvocations.count, "More than one `Error` captured.") + let capture = client.captureErrorWithScopeInvocations.first + callback(capture?.error, capture?.scope) + } + + func assertExceptionWithScopeCaptured(_ callback: (NSException?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureExceptionWithScopeInvocations.count, "More than one `Exception` captured.") + let capture = client.captureExceptionWithScopeInvocations.first + callback(capture?.exception, capture?.scope) + } + + func assertMessageWithScopeCaptured(_ callback: (String?, Scope?) -> Void) { + guard let client = SentrySDK.currentHub().getClient() as? TestClient else { + XCTFail("Hub Client is not a `TestClient`") + return + } + + XCTAssertEqual(1, client.captureMessageWithScopeInvocations.count, "More than one `Exception` captured.") + let capture = client.captureMessageWithScopeInvocations.first + callback(capture?.message, capture?.scope) + } + + func advanceTime(bySeconds: TimeInterval) { + currentDate.setDate(date: currentDate.date().addingTimeInterval(bySeconds)) + } +} diff --git a/Tests/SentryTests/SentrySDKTests.swift b/Tests/SentryTests/SentrySDKTests.swift index e2d3cdb61d6..e52ce9cae99 100644 --- a/Tests/SentryTests/SentrySDKTests.swift +++ b/Tests/SentryTests/SentrySDKTests.swift @@ -340,38 +340,6 @@ class SentrySDKTests: XCTestCase { XCTAssert(span === newSpan) } - func testPerformanceOfConfigureScope() { - func buildCrumb(_ i: Int) -> Breadcrumb { - let crumb = Breadcrumb() - crumb.message = String(repeating: String(i), count: 100) - crumb.data = ["some": String(repeating: String(i), count: 1_000)] - crumb.category = String(i) - return crumb - } - - SentrySDK.start(options: ["dsn": SentrySDKTests.dsnAsString]) - - SentrySDK.configureScope { scope in - let user = User() - user.email = "someone@gmail.com" - scope.setUser(user) - } - - for i in 0...100 { - SentrySDK.configureScope { scope in - scope.add(buildCrumb(i)) - } - } - - self.measure { - for i in 0...10 { - SentrySDK.configureScope { scope in - scope.add(buildCrumb(i)) - } - } - } - } - func testInstallIntegrations() { let options = Options() options.dsn = "mine" @@ -396,47 +364,6 @@ class SentrySDKTests: XCTestCase { assertIntegrationsInstalled(integrations: []) } - @available(tvOS 13.0, *) - @available(OSX 10.15, *) - @available(iOS 13.0, *) - func testMemoryFootprintOfAddingBreadcrumbs() { - SentrySDK.start { options in - options.dsn = SentrySDKTests.dsnAsString - options.debug = true - options.diagnosticLevel = SentryLevel.debug - options.attachStacktrace = true - } - - self.measure(metrics: [XCTMemoryMetric()]) { - for i in 0...1_000 { - let crumb = TestData.crumb - crumb.message = "\(i)" - SentrySDK.addBreadcrumb(crumb: crumb) - } - } - } - - @available(tvOS 13.0, *) - @available(OSX 10.15, *) - @available(iOS 13.0, *) - func testMemoryFootprintOfTransactions() { - SentrySDK.start { options in - options.dsn = SentrySDKTests.dsnAsString - } - - self.measure(metrics: [XCTMemoryMetric()]) { - for _ in 0...1_000 { - let trans = SentrySDK.startTransaction(name: "no leak", operation: "") - - for _ in 0...10 { - let span = trans.startChild(operation: "ui.load") - span.finish() - } - trans.finish() - } - } - } - func testStartSession() { givenSdkWithHub() diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 4fad6a06489..79458534897 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -8,6 +8,7 @@ #import "NSURLProtocolSwizzle.h" #import "PrivateSentrySDKOnly.h" #import "SentryANRTracker.h" +#import "SentryANRTrackingIntegration.h" #import "SentryAppStartMeasurement.h" #import "SentryAppStartTracker.h" #import "SentryAppStartTrackingIntegration.h" @@ -161,7 +162,6 @@ #import "URLSessionTaskMock.h" #if SENTRY_HAS_UIKIT -# import "SentryANRTrackingIntegration.h" # import "SentryUIEventTracker.h" # import "SentryUIEventTrackingIntegration.h" #endif