diff --git a/CHANGELOG.md b/CHANGELOG.md index 9369362ff00..4427992ecec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Report start up crashes (#2220) - Add segment property to user (#2234) ## 7.26.0 diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index fe7630f1726..2f5b0137c77 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -410,6 +410,7 @@ 7B9657252683104C00C66E25 /* NSData+Sentry.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B9657232683104C00C66E25 /* NSData+Sentry.h */; }; 7B9657262683104C00C66E25 /* NSData+Sentry.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9657242683104C00C66E25 /* NSData+Sentry.m */; }; 7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B965727268321CD00C66E25 /* SentryCrashScopeObserverTests.swift */; }; + 7B984A9F28E572AF001F4BEE /* CrashReportWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B984A9E28E572AF001F4BEE /* CrashReportWriter.swift */; }; 7B98D7BC25FB607300C5A389 /* SentryOutOfMemoryTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B98D7BB25FB607300C5A389 /* SentryOutOfMemoryTracker.h */; }; 7B98D7CB25FB64EC00C5A389 /* SentryOutOfMemoryTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B98D7CA25FB64EC00C5A389 /* SentryOutOfMemoryTrackingIntegration.h */; }; 7B98D7CF25FB650F00C5A389 /* SentryOutOfMemoryTrackingIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B98D7CE25FB650F00C5A389 /* SentryOutOfMemoryTrackingIntegration.m */; }; @@ -1138,6 +1139,7 @@ 7B9657242683104C00C66E25 /* NSData+Sentry.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSData+Sentry.m"; sourceTree = ""; }; 7B965727268321CD00C66E25 /* SentryCrashScopeObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashScopeObserverTests.swift; sourceTree = ""; }; 7B9660B12783500E0014A767 /* ThreadSanitizer.sup */ = {isa = PBXFileReference; lastKnownFileType = text; path = ThreadSanitizer.sup; sourceTree = ""; }; + 7B984A9E28E572AF001F4BEE /* CrashReportWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReportWriter.swift; sourceTree = ""; }; 7B98D7BB25FB607300C5A389 /* SentryOutOfMemoryTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryOutOfMemoryTracker.h; path = include/SentryOutOfMemoryTracker.h; sourceTree = ""; }; 7B98D7CA25FB64EC00C5A389 /* SentryOutOfMemoryTrackingIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryOutOfMemoryTrackingIntegration.h; path = include/SentryOutOfMemoryTrackingIntegration.h; sourceTree = ""; }; 7B98D7CE25FB650F00C5A389 /* SentryOutOfMemoryTrackingIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryOutOfMemoryTrackingIntegration.m; sourceTree = ""; }; @@ -2175,6 +2177,7 @@ 7BED3575266F7BFF00EAA70D /* TestSentryCrashWrapper.m */, 0AABE2EF2885C2120057ED69 /* TestSentryPermissionsObserver.swift */, 0ADC33EF28D9BE690078D980 /* TestSentryUIDeviceWrapper.swift */, + 7B984A9E28E572AF001F4BEE /* CrashReportWriter.swift */, ); path = SentryCrash; sourceTree = ""; @@ -3558,6 +3561,7 @@ D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */, 8E25C97525F8511A00DC215B /* TestRandom.swift in Sources */, 7B26BBFB24C0A66D00A79CCC /* SentrySdkInfoNilTests.m in Sources */, + 7B984A9F28E572AF001F4BEE /* CrashReportWriter.swift in Sources */, 7BAF3DD7243DD4A1008A5414 /* TestConstants.swift in Sources */, 035E73CC27D575B3005EEB11 /* SentrySamplingProfilerTests.mm in Sources */, 7BED3576266F7BFF00EAA70D /* TestSentryCrashWrapper.m in Sources */, diff --git a/Sources/Sentry/SentryCrashInstallationReporter.m b/Sources/Sentry/SentryCrashInstallationReporter.m index 8db49f69b85..c3e5c848a73 100644 --- a/Sources/Sentry/SentryCrashInstallationReporter.m +++ b/Sources/Sentry/SentryCrashInstallationReporter.m @@ -11,22 +11,30 @@ SentryCrashInstallationReporter () @property (nonatomic, strong) SentryInAppLogic *inAppLogic; +@property (nonatomic, strong) SentryCrashWrapper *crashWrapper; +@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @end @implementation SentryCrashInstallationReporter - (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue { if (self = [super initWithRequiredProperties:[NSArray new]]) { self.inAppLogic = inAppLogic; + self.crashWrapper = crashWrapper; + self.dispatchQueue = dispatchQueue; } return self; } - (id)sink { - return [[SentryCrashReportSink alloc] initWithInAppLogic:self.inAppLogic]; + return [[SentryCrashReportSink alloc] initWithInAppLogic:self.inAppLogic + crashWrapper:self.crashWrapper + dispatchQueue:self.dispatchQueue]; } - (void)sendAllReports diff --git a/Sources/Sentry/SentryCrashIntegration.m b/Sources/Sentry/SentryCrashIntegration.m index 7bcd76b394b..bc5e45638ff 100644 --- a/Sources/Sentry/SentryCrashIntegration.m +++ b/Sources/Sentry/SentryCrashIntegration.m @@ -106,7 +106,10 @@ - (void)startCrashHandler [[SentryInAppLogic alloc] initWithInAppIncludes:self.options.inAppIncludes inAppExcludes:self.options.inAppExcludes]; - installation = [[SentryCrashInstallationReporter alloc] initWithInAppLogic:inAppLogic]; + installation = [[SentryCrashInstallationReporter alloc] + initWithInAppLogic:inAppLogic + crashWrapper:self.crashAdapter + dispatchQueue:self.dispatchQueueWrapper]; canSendReports = YES; } @@ -137,12 +140,20 @@ - (void)startCrashHandler // just not call sendAllReports as it doesn't make sense to call it twice as described // above. if (canSendReports) { - [installation sendAllReports]; + [SentryCrashIntegration sendAllSentryCrashReports]; } }; [self.dispatchQueueWrapper dispatchOnce:&installationToken block:block]; } +/** + * Internal, only needed for testing. + */ ++ (void)sendAllSentryCrashReports +{ + [installation sendAllReports]; +} + - (void)uninstall { if (nil != installation) { diff --git a/Sources/Sentry/SentryCrashReportSink.m b/Sources/Sentry/SentryCrashReportSink.m index af3300f1482..eb7e137647e 100644 --- a/Sources/Sentry/SentryCrashReportSink.m +++ b/Sources/Sentry/SentryCrashReportSink.m @@ -2,8 +2,11 @@ #import "SentryAttachment.h" #import "SentryClient.h" #import "SentryCrash.h" +#include "SentryCrashMonitor_AppState.h" #import "SentryCrashReportConverter.h" +#import "SentryCrashWrapper.h" #import "SentryDefines.h" +#import "SentryDispatchQueueWrapper.h" #import "SentryEvent.h" #import "SentryException.h" #import "SentryHub.h" @@ -13,23 +16,75 @@ #import "SentryScope.h" #import "SentryThread.h" +static const NSTimeInterval SENTRY_APP_START_CRASH_DURATION_THRESHOLD = 2.0; +static const NSTimeInterval SENTRY_APP_START_CRASH_FLUSH_DURATION = 5.0; + @interface SentryCrashReportSink () @property (nonatomic, strong) SentryInAppLogic *inAppLogic; +@property (nonatomic, strong) SentryCrashWrapper *crashWrapper; +@property (nonatomic, strong) SentryDispatchQueueWrapper *dispatchQueue; @end @implementation SentryCrashReportSink - (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue { if (self = [super init]) { self.inAppLogic = inAppLogic; + self.crashWrapper = crashWrapper; + self.dispatchQueue = dispatchQueue; } return self; } +- (void)filterReports:(NSArray *)reports + onCompletion:(SentryCrashReportFilterCompletion)onCompletion +{ + NSTimeInterval durationFromCrashStateInitToLastCrash + = self.crashWrapper.durationFromCrashStateInitToLastCrash; + if (durationFromCrashStateInitToLastCrash > 0 + && durationFromCrashStateInitToLastCrash <= SENTRY_APP_START_CRASH_DURATION_THRESHOLD) { + SENTRY_LOG_WARN(@"Startup crash: detected."); + [self sendReports:reports onCompletion:onCompletion]; + + [SentrySDK flush:SENTRY_APP_START_CRASH_FLUSH_DURATION]; + SENTRY_LOG_DEBUG(@"Startup crash: Finished flushing."); + + } else { + [self.dispatchQueue + dispatchAsyncWithBlock:^{ [self sendReports:reports onCompletion:onCompletion]; }]; + } +} + +- (void)sendReports:(NSArray *)reports onCompletion:(SentryCrashReportFilterCompletion)onCompletion +{ + NSMutableArray *sentReports = [NSMutableArray new]; + for (NSDictionary *report in reports) { + SentryCrashReportConverter *reportConverter = + [[SentryCrashReportConverter alloc] initWithReport:report inAppLogic:self.inAppLogic]; + if (nil != [SentrySDK.currentHub getClient]) { + SentryEvent *event = [reportConverter convertReportToEvent]; + if (nil != event) { + [self handleConvertedEvent:event report:report sentReports:sentReports]; + } + } else { + SENTRY_LOG_ERROR( + @"Crash reports were found but no [SentrySDK.currentHub getClient] is set. " + @"Cannot send crash reports to Sentry. This is probably a misconfiguration, " + @"make sure you set the client with [SentrySDK.currentHub bindClient] before " + @"calling startCrashHandlerWithError:."); + } + } + if (onCompletion) { + onCompletion(sentReports, TRUE, nil); + } +} + - (void)handleConvertedEvent:(SentryEvent *)event report:(NSDictionary *)report sentReports:(NSMutableArray *)sentReports @@ -46,33 +101,4 @@ - (void)handleConvertedEvent:(SentryEvent *)event [SentrySDK captureCrashEvent:event withScope:scope]; } -- (void)filterReports:(NSArray *)reports - onCompletion:(SentryCrashReportFilterCompletion)onCompletion -{ - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul); - dispatch_async(queue, ^{ - NSMutableArray *sentReports = [NSMutableArray new]; - for (NSDictionary *report in reports) { - SentryCrashReportConverter *reportConverter = - [[SentryCrashReportConverter alloc] initWithReport:report - inAppLogic:self.inAppLogic]; - if (nil != [SentrySDK.currentHub getClient]) { - SentryEvent *event = [reportConverter convertReportToEvent]; - if (nil != event) { - [self handleConvertedEvent:event report:report sentReports:sentReports]; - } - } else { - SENTRY_LOG_ERROR( - @"Crash reports were found but no [SentrySDK.currentHub getClient] is set. " - @"Cannot send crash reports to Sentry. This is probably a misconfiguration, " - @"make sure you set the client with [SentrySDK.currentHub bindClient] before " - @"calling startCrashHandlerWithError:."); - } - } - if (onCompletion) { - onCompletion(sentReports, TRUE, nil); - } - }); -} - @end diff --git a/Sources/Sentry/SentryCrashWrapper.m b/Sources/Sentry/SentryCrashWrapper.m index db599881906..a92a7c7d31e 100644 --- a/Sources/Sentry/SentryCrashWrapper.m +++ b/Sources/Sentry/SentryCrashWrapper.m @@ -26,6 +26,11 @@ - (BOOL)crashedLastLaunch return SentryCrash.sharedInstance.crashedLastLaunch; } +- (NSTimeInterval)durationFromCrashStateInitToLastCrash +{ + return sentrycrashstate_currentState()->durationFromCrashStateInitToLastCrash; +} + - (NSTimeInterval)activeDurationSinceLastCrash { return SentryCrash.sharedInstance.activeDurationSinceLastCrash; diff --git a/Sources/Sentry/SentryFileManager.m b/Sources/Sentry/SentryFileManager.m index 02d3f725dd3..98a8f055ed8 100644 --- a/Sources/Sentry/SentryFileManager.m +++ b/Sources/Sentry/SentryFileManager.m @@ -46,6 +46,8 @@ - (nullable instancetype)initWithOptions:(SentryOptions *)options = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) .firstObject; + SENTRY_LOG_DEBUG(@"SentryFileManager.cachePath: %@", cachePath); + self.sentryPath = [cachePath stringByAppendingPathComponent:@"io.sentry"]; self.sentryPath = [self.sentryPath stringByAppendingPathComponent:[options.parsedDsn getHash]]; diff --git a/Sources/Sentry/include/SentryCrashInstallationReporter.h b/Sources/Sentry/include/SentryCrashInstallationReporter.h index d0d64b8563e..aae2ca73afd 100644 --- a/Sources/Sentry/include/SentryCrashInstallationReporter.h +++ b/Sources/Sentry/include/SentryCrashInstallationReporter.h @@ -3,14 +3,16 @@ #import "SentryDefines.h" #import -@class SentryInAppLogic; +@class SentryInAppLogic, SentryCrashWrapper, SentryDispatchQueueWrapper; NS_ASSUME_NONNULL_BEGIN @interface SentryCrashInstallationReporter : SentryCrashInstallation SENTRY_NO_INIT -- (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic; +- (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue; - (void)sendAllReports; diff --git a/Sources/Sentry/include/SentryCrashIntegration.h b/Sources/Sentry/include/SentryCrashIntegration.h index dea058f84c1..07b37d34c1b 100644 --- a/Sources/Sentry/include/SentryCrashIntegration.h +++ b/Sources/Sentry/include/SentryCrashIntegration.h @@ -13,6 +13,11 @@ static NSString *const SentryDeviceContextAppMemoryKey = @"app_memory"; + (void)enrichScope:(SentryScope *)scope crashWrapper:(SentryCrashWrapper *)crashWrapper; +/** + * Needed for testing. + */ ++ (void)sendAllSentryCrashReports; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryCrashReportSink.h b/Sources/Sentry/include/SentryCrashReportSink.h index 2c88e1b9a30..aba12fb9ac4 100644 --- a/Sources/Sentry/include/SentryCrashReportSink.h +++ b/Sources/Sentry/include/SentryCrashReportSink.h @@ -2,14 +2,16 @@ #import "SentryDefines.h" #import -@class SentryInAppLogic; +@class SentryInAppLogic, SentryCrashWrapper, SentryDispatchQueueWrapper; NS_ASSUME_NONNULL_BEGIN @interface SentryCrashReportSink : NSObject SENTRY_NO_INIT -- (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic; +- (instancetype)initWithInAppLogic:(SentryInAppLogic *)inAppLogic + crashWrapper:(SentryCrashWrapper *)crashWrapper + dispatchQueue:(SentryDispatchQueueWrapper *)dispatchQueue; @end diff --git a/Sources/Sentry/include/SentryCrashWrapper.h b/Sources/Sentry/include/SentryCrashWrapper.h index e069ad73c65..7e88021293b 100644 --- a/Sources/Sentry/include/SentryCrashWrapper.h +++ b/Sources/Sentry/include/SentryCrashWrapper.h @@ -13,6 +13,8 @@ SENTRY_NO_INIT - (BOOL)crashedLastLaunch; +- (NSTimeInterval)durationFromCrashStateInitToLastCrash; + - (NSTimeInterval)activeDurationSinceLastCrash; - (BOOL)isBeingTraced; diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c index eb58fe8d804..a59dd9c908f 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c @@ -49,6 +49,7 @@ #define kKeyFormatVersion "version" #define kKeyCrashedLastLaunch "crashedLastLaunch" +#define kKeyDurationFromCrashStateInitToLastCrash "durationFromCrashStateInitToLastCrash" #define kKeyActiveDurationSinceLastCrash "activeDurationSinceLastCrash" #define kKeyBackgroundDurationSinceLastCrash "backgroundDurationSinceLastCrash" #define kKeyLaunchesSinceLastCrash "launchesSinceLastCrash" @@ -65,6 +66,8 @@ static const char *g_stateFilePath; /** Current state. */ static SentryCrash_AppState g_state; +static double g_crashstate_initialize_time; + static volatile bool g_isEnabled = false; // ============================================================================ @@ -96,6 +99,10 @@ onFloatingPointElement(const char *const name, const double value, void *const u return SentryCrashJSON_ERROR_INVALID_DATA; } + if (strcmp(name, kKeyDurationFromCrashStateInitToLastCrash) == 0) { + state->durationFromCrashStateInitToLastCrash = value; + } + if (strcmp(name, kKeyActiveDurationSinceLastCrash) == 0) { state->activeDurationSinceLastCrash = value; } @@ -277,6 +284,22 @@ saveState(const char *const path) != SentryCrashJSON_OK) { goto done; } + + // SentryCrash resets the app state when enabling it in setEnabled. To keep the value alive for + // the application's lifetime, we don't modify the g_state. Instead, we only save the value to + // the crash state file without setting it to g_state. When initializing the app state, the code + // reads the value from the file and keeps it in memory. The code uses the same pattern for + // CrashedLastLaunch. Ideally, we would refactor this, but we must be aware of possible side + // effects. + double durationFromCrashStateInitToLastCrash = 0; + if (g_state.crashedThisLaunch) { + durationFromCrashStateInitToLastCrash = timeSince(g_crashstate_initialize_time); + } + if ((result = sentrycrashjson_addFloatingPointElement(&JSONContext, + kKeyDurationFromCrashStateInitToLastCrash, durationFromCrashStateInitToLastCrash)) + != SentryCrashJSON_OK) { + goto done; + } if ((result = sentrycrashjson_addFloatingPointElement( &JSONContext, kKeyActiveDurationSinceLastCrash, g_state.activeDurationSinceLastCrash)) != SentryCrashJSON_OK) { @@ -315,6 +338,7 @@ saveState(const char *const path) void sentrycrashstate_initialize(const char *const stateFilePath) { + g_crashstate_initialize_time = getCurentTime(); g_stateFilePath = strdup(stateFilePath); memset(&g_state, 0, sizeof(g_state)); loadState(g_stateFilePath); @@ -345,6 +369,12 @@ sentrycrashstate_reset() return false; } +const char * +sentrycrashstate_filePath(void) +{ + return g_stateFilePath; +} + void sentrycrashstate_notifyAppActive(const bool isActive) { @@ -411,7 +441,7 @@ sentrycrashstate_notifyAppCrash(void) } } -const SentryCrash_AppState *const +const SentryCrash_AppState * sentrycrashstate_currentState(void) { return &g_state; diff --git a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h index 3802c0ae3a1..fe1235586dd 100644 --- a/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h +++ b/Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h @@ -66,6 +66,10 @@ typedef struct { /** If true, the application crashed on the previous launch. */ bool crashedLastLaunch; + /** Total time in seconds from the crash state init to the last crash. Only contains a value + * bigger than zero if crashedLastLaunch is true. */ + double durationFromCrashStateInitToLastCrash; + // Live data /** If true, the application crashed on this launch. */ @@ -93,6 +97,8 @@ void sentrycrashstate_initialize(const char *stateFilePath); */ bool sentrycrashstate_reset(void); +const char *sentrycrashstate_filePath(void); + /** Notify the crash reporter of the application active state. * * @param isActive true if the application is active, otherwise false. @@ -116,7 +122,7 @@ void sentrycrashstate_notifyAppCrash(void); /** Read-only access into the current state. */ -const SentryCrash_AppState *const sentrycrashstate_currentState(void); +const SentryCrash_AppState *sentrycrashstate_currentState(void); /** Access the Monitor API. */ diff --git a/Tests/Resources/CrashState_legacy_1.json b/Tests/Resources/CrashState_legacy_1.json new file mode 100644 index 00000000000..549ac5b9c2b --- /dev/null +++ b/Tests/Resources/CrashState_legacy_1.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "crashedLastLaunch": false, + "activeDurationSinceLastCrash": 2.5, + "backgroundDurationSinceLastCrash": 5.0, + "launchesSinceLastCrash": 10, + "sessionsSinceLastCrash": 10 +} diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift index 0b51050cb06..f639cfbd52e 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift @@ -265,6 +265,41 @@ class SentryCrashIntegrationTests: NotificationCenterTestCase { assertLocaleOnHub(locale: Locale.autoupdatingCurrent.identifier, hub: hub) } + func testStartUpCrash_CallsFlush() throws { + let (sut, hub) = givenSutWithGlobalHubAndCrashWrapper() + sut.install(with: Options()) + + // Manually reset and enable the crash state because tearing down the global state in SentryCrash to achieve the same is complicated and doesn't really work. + let crashStatePath = String(cString: sentrycrashstate_filePath()) + let api = sentrycrashcm_appstate_getAPI() + sentrycrashstate_initialize(crashStatePath) + api?.pointee.setEnabled(true) + + let transport = TestTransport() + let client = Client(options: fixture.options) + Dynamic(client).transportAdapter = TestTransportAdapter(transport: transport, options: fixture.options) + hub.bindClient(client) + + delayNonBlocking(timeout: 0.01) + + // Manually simulate a crash + sentrycrashstate_notifyAppCrash() + + try givenStoredSentryCrashReport(resource: "Resources/crash-report-1") + + // Force reloading of crash state + sentrycrashstate_initialize(sentrycrashstate_filePath()) + // Force sending all reports, because the crash reports are only sent once after first init. + SentryCrashIntegration.sendAllSentryCrashReports() + + XCTAssertEqual(1, transport.flushInvocations.count) + XCTAssertEqual(5.0, transport.flushInvocations.first) + + // Reset and disable crash state + sentrycrashstate_reset() + api?.pointee.setEnabled(false) + } + private func givenCurrentSession() -> SentrySession { // serialize sets the timestamp let session = SentrySession(jsonObject: fixture.session.serialize())! diff --git a/Tests/SentryTests/SentryCrash/CrashReportWriter.swift b/Tests/SentryTests/SentryCrash/CrashReportWriter.swift new file mode 100644 index 00000000000..1abf941c9a7 --- /dev/null +++ b/Tests/SentryTests/SentryCrash/CrashReportWriter.swift @@ -0,0 +1,12 @@ +import XCTest + +extension XCTestCase { + func givenStoredSentryCrashReport(resource: String) throws { + let jsonPath = Bundle(for: type(of: self)).path(forResource: resource, ofType: "json") + let jsonData = try Data(contentsOf: URL(fileURLWithPath: jsonPath ?? "")) + jsonData.withUnsafeBytes { ( bytes: UnsafeRawBufferPointer) -> Void in + let pointer = bytes.bindMemory(to: Int8.self) + sentrycrashcrs_addUserReport(pointer.baseAddress, Int32(jsonData.count)) + } + } +} diff --git a/Tests/SentryTests/SentryCrash/SentryCrashInstallationReporterTests.swift b/Tests/SentryTests/SentryCrash/SentryCrashInstallationReporterTests.swift index 03b75702514..0ead723a73e 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashInstallationReporterTests.swift +++ b/Tests/SentryTests/SentryCrash/SentryCrashInstallationReporterTests.swift @@ -11,7 +11,7 @@ class SentryCrashInstallationReporterTests: XCTestCase { override func setUp() { super.setUp() - sut = SentryCrashInstallationReporter(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: [])) + sut = SentryCrashInstallationReporter(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []), crashWrapper: TestSentryCrashWrapper.sharedInstance(), dispatchQueue: TestSentryDispatchQueueWrapper()) sut.install() // Works only if SentryCrash is installed sentrycrash_deleteAllReports() @@ -25,7 +25,8 @@ class SentryCrashInstallationReporterTests: XCTestCase { func testFaultyReportIsNotSentAndDeleted() throws { sdkStarted() - sentryCrashHasFaultyCrashReport() + + try givenStoredSentryCrashReport(resource: "Resources/Crash-faulty-report") sut.sendAllReports() @@ -49,19 +50,6 @@ class SentryCrashInstallationReporterTests: XCTestCase { let hub = SentryHub(client: testClient, andScope: nil) SentrySDK.setCurrentHub(hub) } - - private func sentryCrashHasFaultyCrashReport() { - do { - let jsonPath = Bundle(for: type(of: self)).path(forResource: "Resources/Crash-faulty-report", ofType: "json") - let jsonData = try Data(contentsOf: URL(fileURLWithPath: jsonPath ?? "")) - jsonData.withUnsafeBytes { ( bytes: UnsafeRawBufferPointer) -> Void in - let pointer = bytes.bindMemory(to: Int8.self) - sentrycrashcrs_addUserReport(pointer.baseAddress, Int32(jsonData.count)) - } - } catch { - XCTFail("Failed to store faulty crash report in SentryCrash.") - } - } private func assertNoEventsSent() { XCTAssertEqual(0, testClient.captureEventWithScopeInvocations.count) diff --git a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_AppState_Tests.m b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_AppState_Tests.m index 19b5fcfdf0a..0948e1fb050 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashMonitor_AppState_Tests.m +++ b/Tests/SentryTests/SentryCrash/SentryCrashMonitor_AppState_Tests.m @@ -69,6 +69,7 @@ - (void)testInitRelaunch XCTAssertFalse(context.crashedThisLaunch); XCTAssertFalse(context.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -87,6 +88,7 @@ - (void)testInitRelaunch XCTAssertFalse(context.crashedThisLaunch); XCTAssertFalse(context.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testInitCrash @@ -119,6 +121,8 @@ - (void)testInitCrash XCTAssertTrue(checkpointC.crashedThisLaunch); XCTAssertFalse(checkpointC.crashedLastLaunch); + XCTAssertEqual(checkpoint0.durationFromCrashStateInitToLastCrash, + checkpointC.durationFromCrashStateInitToLastCrash); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -137,6 +141,8 @@ - (void)testInitCrash XCTAssertFalse(context.crashedThisLaunch); XCTAssertTrue(context.crashedLastLaunch); + XCTAssertGreaterThan(context.durationFromCrashStateInitToLastCrash, 0.0); + XCTAssertLessThan(context.durationFromCrashStateInitToLastCrash, 1.0); } - (void)testInitWithWrongCrashState @@ -165,6 +171,7 @@ - (void)testInitWithWrongCrashState XCTAssertFalse(context.crashedThisLaunch); XCTAssertFalse(context.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -179,6 +186,48 @@ - (void)testInitWithWrongCrashState XCTAssertEqual(context.sessionsSinceLastCrash, 1); } +- (void)testInitWithCrashStateLegacy +{ + NSString *stateFile = [self.tempPath stringByAppendingPathComponent:@"state.json"]; + NSString *jsonPath = + [[NSBundle bundleForClass:self.class] pathForResource:@"Resources/CrashState_legacy_1" + ofType:@"json"]; + NSData *jsonData = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:jsonPath]]; + [jsonData writeToFile:stateFile atomically:true]; + + [self initializeCrashState]; + SentryCrash_AppState context = *sentrycrashstate_currentState(); + + XCTAssertTrue(context.applicationIsInForeground); + XCTAssertFalse(context.applicationIsActive); + + XCTAssertEqual(context.activeDurationSinceLastCrash, 2.5); + XCTAssertEqual(context.backgroundDurationSinceLastCrash, 5.0); + XCTAssertEqual(context.launchesSinceLastCrash, 11); + XCTAssertEqual(context.sessionsSinceLastCrash, 11); + + XCTAssertEqual(context.activeDurationSinceLaunch, 0.0); + XCTAssertEqual(context.backgroundDurationSinceLaunch, 0.0); + XCTAssertEqual(context.sessionsSinceLaunch, 1); + + XCTAssertFalse(context.crashedThisLaunch); + XCTAssertFalse(context.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); + + [self initializeCrashState]; + context = *sentrycrashstate_currentState(); + XCTAssertEqual(context.launchesSinceLastCrash, 12); + XCTAssertEqual(context.sessionsSinceLastCrash, 12); + + [jsonData writeToFile:stateFile atomically:true]; + + [self initializeCrashState]; + context = *sentrycrashstate_currentState(); + XCTAssertEqual(context.launchesSinceLastCrash, 11); + XCTAssertEqual(context.sessionsSinceLastCrash, 11); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); +} + - (void)testActRelaunch { [self initializeCrashState]; @@ -211,6 +260,7 @@ - (void)testActRelaunch XCTAssertFalse(checkpoint1.crashedThisLaunch); XCTAssertFalse(checkpoint1.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); usleep(1); [self initializeCrashState]; @@ -230,6 +280,7 @@ - (void)testActRelaunch XCTAssertFalse(context.crashedThisLaunch); XCTAssertFalse(context.crashedLastLaunch); + XCTAssertEqual(context.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testActCrash @@ -262,6 +313,7 @@ - (void)testActCrash XCTAssertTrue(checkpointC.crashedThisLaunch); XCTAssertFalse(checkpointC.crashedLastLaunch); + XCTAssertEqual(checkpointC.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; SentryCrash_AppState context = *sentrycrashstate_currentState(); @@ -280,6 +332,8 @@ - (void)testActCrash XCTAssertFalse(context.crashedThisLaunch); XCTAssertTrue(context.crashedLastLaunch); + XCTAssertGreaterThan(context.durationFromCrashStateInitToLastCrash, 0.0); + XCTAssertLessThan(context.durationFromCrashStateInitToLastCrash, 1.0); } - (void)testActDeactRelaunch @@ -313,6 +367,7 @@ - (void)testActDeactRelaunch XCTAssertFalse(checkpoint1.crashedThisLaunch); XCTAssertFalse(checkpoint1.crashedLastLaunch); + XCTAssertEqual(checkpoint1.durationFromCrashStateInitToLastCrash, 0.0); usleep(1); [self initializeCrashState]; @@ -333,6 +388,7 @@ - (void)testActDeactRelaunch XCTAssertFalse(checkpointR.crashedThisLaunch); XCTAssertFalse(checkpointR.crashedLastLaunch); + XCTAssertEqual(checkpointR.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testActDeactCrash @@ -368,6 +424,7 @@ - (void)testActDeactCrash XCTAssertTrue(checkpointC.crashedThisLaunch); XCTAssertFalse(checkpointC.crashedLastLaunch); + XCTAssertEqual(checkpointC.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -386,6 +443,8 @@ - (void)testActDeactCrash XCTAssertFalse(context.crashedThisLaunch); XCTAssertTrue(context.crashedLastLaunch); + XCTAssertGreaterThan(context.durationFromCrashStateInitToLastCrash, 0.0); + XCTAssertLessThan(context.durationFromCrashStateInitToLastCrash, 1.0); } - (void)testActDeactBGRelaunch @@ -421,6 +480,7 @@ - (void)testActDeactBGRelaunch XCTAssertFalse(checkpoint1.crashedThisLaunch); XCTAssertFalse(checkpoint1.crashedLastLaunch); + XCTAssertEqual(checkpoint1.durationFromCrashStateInitToLastCrash, 0.0); usleep(1); [self initializeCrashState]; @@ -440,6 +500,7 @@ - (void)testActDeactBGRelaunch XCTAssertFalse(checkpointR.crashedThisLaunch); XCTAssertFalse(checkpointR.crashedLastLaunch); + XCTAssertEqual(checkpointR.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testActDeactBGTerminate @@ -474,6 +535,7 @@ - (void)testActDeactBGTerminate XCTAssertFalse(checkpointR.crashedThisLaunch); XCTAssertFalse(checkpointR.crashedLastLaunch); + XCTAssertEqual(checkpointR.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testActDeactBGCrash @@ -510,6 +572,7 @@ - (void)testActDeactBGCrash XCTAssertTrue(checkpointC.crashedThisLaunch); XCTAssertFalse(checkpointC.crashedLastLaunch); + XCTAssertEqual(checkpointC.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -528,6 +591,8 @@ - (void)testActDeactBGCrash XCTAssertFalse(context.crashedThisLaunch); XCTAssertTrue(context.crashedLastLaunch); + XCTAssertGreaterThan(context.durationFromCrashStateInitToLastCrash, 0.0); + XCTAssertLessThan(context.durationFromCrashStateInitToLastCrash, 1.0); } - (void)testActDeactBGFGRelaunch @@ -565,6 +630,7 @@ - (void)testActDeactBGFGRelaunch XCTAssertFalse(checkpoint1.crashedThisLaunch); XCTAssertFalse(checkpoint1.crashedLastLaunch); + XCTAssertEqual(checkpoint1.durationFromCrashStateInitToLastCrash, 0.0); usleep(1); [self initializeCrashState]; @@ -585,6 +651,7 @@ - (void)testActDeactBGFGRelaunch XCTAssertFalse(checkpointR.crashedThisLaunch); XCTAssertFalse(checkpointR.crashedLastLaunch); + XCTAssertEqual(checkpointR.durationFromCrashStateInitToLastCrash, 0.0); } - (void)testActDeactBGFGCrash @@ -624,6 +691,7 @@ - (void)testActDeactBGFGCrash XCTAssertTrue(checkpointC.crashedThisLaunch); XCTAssertFalse(checkpointC.crashedLastLaunch); + XCTAssertEqual(checkpointC.durationFromCrashStateInitToLastCrash, 0.0); [self initializeCrashState]; context = *sentrycrashstate_currentState(); @@ -642,6 +710,8 @@ - (void)testActDeactBGFGCrash XCTAssertFalse(context.crashedThisLaunch); XCTAssertTrue(context.crashedLastLaunch); + XCTAssertGreaterThan(context.durationFromCrashStateInitToLastCrash, 0.0); + XCTAssertLessThan(context.durationFromCrashStateInitToLastCrash, 1.0); } @end diff --git a/Tests/SentryTests/SentryCrash/SentryCrashReportSinkTest.swift b/Tests/SentryTests/SentryCrash/SentryCrashReportSinkTest.swift index 5a076ba31d9..0b213b277d1 100644 --- a/Tests/SentryTests/SentryCrash/SentryCrashReportSinkTest.swift +++ b/Tests/SentryTests/SentryCrash/SentryCrashReportSinkTest.swift @@ -1,34 +1,38 @@ import XCTest class SentryCrashReportSinkTests: SentrySDKIntegrationTestsBase { + + private class Fixture { + let crashWrapper = TestSentryCrashWrapper.sharedInstance() + let dispatchQueue = TestSentryDispatchQueueWrapper() - func testFilterReports_withScreenShots() { - givenSdkWithHub() - - let reportSink = SentryCrashReportSink(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: [])) - let expect = expectation(description: "Callback Called") + var sut: SentryCrashReportSink { + return SentryCrashReportSink(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: []), crashWrapper: crashWrapper, dispatchQueue: dispatchQueue) + } + } + + private var fixture: Fixture! + + override func setUp() { + super.setUp() + fixture = Fixture() - let report = ["attachments": ["file.png"]] + givenSdkWithHub() + } - reportSink.filterReports([report]) { _, _, _ in - self.assertCrashEventWithScope { _, scope in - XCTAssertEqual(scope?.attachments.count, 1) - expect.fulfill() - } - } - - wait(for: [expect], timeout: 1) + func testFilterReports_withScreenShots() { + filterReportWithAttachment() + XCTAssertEqual(1, fixture.dispatchQueue.dispatchAsyncCalled) } func testFilterReports_CopyHubScope() { - givenSdkWithHub() SentrySDK.currentHub().scope.setEnvironment("testFilterReports_CopyHubScope") - let reportSink = SentryCrashReportSink(inAppLogic: SentryInAppLogic(inAppIncludes: [], inAppExcludes: [])) let expect = expectation(description: "Callback Called") let report = [String: Any]() + let reportSink = fixture.sut reportSink.filterReports([report]) { _, _, _ in self.assertCrashEventWithScope { _, scope in let data = scope?.serialize() @@ -38,5 +42,78 @@ class SentryCrashReportSinkTests: SentrySDKIntegrationTestsBase { } wait(for: [expect], timeout: 1) + + XCTAssertEqual(1, fixture.dispatchQueue.dispatchAsyncCalled) + } + + func testAppStartCrash_LowerBound_CallsFlush() { + fixture.crashWrapper.internalDurationFromCrashStateInitToLastCrash = 0.001 + + filterReportWithAttachment() + + let client = getTestClient() + XCTAssertEqual(1, client.flushInvoctions.count) + XCTAssertEqual(5, client.flushInvoctions.first) + XCTAssertEqual(0, fixture.dispatchQueue.dispatchAsyncCalled) + } + + func testAppStartCrash_UpperBound_CallsFlush() { + fixture.crashWrapper.internalDurationFromCrashStateInitToLastCrash = 2.0 + + filterReportWithAttachment() + + let client = getTestClient() + XCTAssertEqual(1, client.flushInvoctions.count) + XCTAssertEqual(5, client.flushInvoctions.first) + XCTAssertEqual(0, fixture.dispatchQueue.dispatchAsyncCalled) + } + + func testAppStartCrash_DurationTooSmall_DoesNotCallFlush() { + fixture.crashWrapper.internalDurationFromCrashStateInitToLastCrash = 0 + + filterReportWithAttachment() + + let client = getTestClient() + XCTAssertEqual(0, client.flushInvoctions.count) + XCTAssertEqual(1, fixture.dispatchQueue.dispatchAsyncCalled) + } + + func testAppStartCrash_DurationNegative_DoesNotCallFlush() { + fixture.crashWrapper.internalDurationFromCrashStateInitToLastCrash = -0.001 + + filterReportWithAttachment() + + let client = getTestClient() + XCTAssertEqual(0, client.flushInvoctions.count) + XCTAssertEqual(1, fixture.dispatchQueue.dispatchAsyncCalled) + } + + func testAppStartCrash_DurationTooBig_DoesNotCallFlush() { + fixture.crashWrapper.internalDurationFromCrashStateInitToLastCrash = 2.000_01 + + filterReportWithAttachment() + + let client = getTestClient() + XCTAssertEqual(0, client.flushInvoctions.count) + XCTAssertEqual(1, fixture.dispatchQueue.dispatchAsyncCalled) + } + + private func filterReportWithAttachment() { + let report = ["attachments": ["file.png"]] + fixture.sut.filterReports([report]) { _, _, _ in + self.assertCrashEventWithScope { _, scope in + XCTAssertEqual(scope?.attachments.count, 1) + } + } + } + + private func getTestClient() -> TestClient { + let client = SentrySDK.currentHub().getClient() as? TestClient + + if client == nil { + XCTFail("Hub Client is not a `TestClient`") + } + + return client! } } diff --git a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h index e2ba87fe97b..4ca76a8bc63 100644 --- a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h +++ b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.h @@ -13,6 +13,8 @@ SENTRY_NO_INIT @property (nonatomic, assign) BOOL internalCrashedLastLaunch; +@property (nonatomic, assign) NSTimeInterval internalDurationFromCrashStateInitToLastCrash; + @property (nonatomic, assign) NSTimeInterval internalActiveDurationSinceLastCrash; @property (nonatomic, assign) BOOL internalIsBeingTraced; diff --git a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m index 90c17aff8fb..4c6d77345ee 100644 --- a/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m +++ b/Tests/SentryTests/SentryCrash/TestSentryCrashWrapper.m @@ -8,6 +8,7 @@ + (instancetype)sharedInstance { TestSentryCrashWrapper *instance = [[self alloc] init]; instance.internalActiveDurationSinceLastCrash = NO; + instance.internalDurationFromCrashStateInitToLastCrash = 0; instance.internalActiveDurationSinceLastCrash = 0; instance.internalIsBeingTraced = NO; instance.internalIsSimulatorBuild = NO; @@ -25,6 +26,11 @@ - (BOOL)crashedLastLaunch return self.internalCrashedLastLaunch; } +- (NSTimeInterval)durationFromCrashStateInitToLastCrash +{ + return self.internalDurationFromCrashStateInitToLastCrash; +} + - (NSTimeInterval)activeDurationSinceLastCrash { return self.internalActiveDurationSinceLastCrash; diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 1dba179c9e6..9a335992fc6 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -41,6 +41,7 @@ #import "SentryCrashMachineContext.h" #import "SentryCrashMonitor.h" #import "SentryCrashMonitorContext.h" +#import "SentryCrashMonitor_AppState.h" #import "SentryCrashMonitor_System.h" #import "SentryCrashReport.h" #import "SentryCrashReportSink.h" diff --git a/Tests/SentryTests/TestClient.swift b/Tests/SentryTests/TestClient.swift index be38ad75e72..e76c3f73ad5 100644 --- a/Tests/SentryTests/TestClient.swift +++ b/Tests/SentryTests/TestClient.swift @@ -128,6 +128,11 @@ class TestClient: Client { override func recordLostEvent(_ category: SentryDataCategory, reason: SentryDiscardReason) { recordLostEvents.record((category, reason)) } + + var flushInvoctions = Invocations() + override func flush(timeout: TimeInterval) { + flushInvoctions.record(timeout) + } } class TestFileManager: SentryFileManager {