From 0961c8987bfd0ed1ae93827c16cf49da90e4fbbd Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 21 Oct 2020 12:13:04 +0200 Subject: [PATCH 01/13] feat: Add onCrashedLastRun Add a callback onCrashedLastRun to SentryOptions that is called by the SDK passing the eventId when Sentry is initialised and the last program execution terminated with a crash. --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 14 ++++++++++++++ Sources/Sentry/Public/SentryDefines.h | 8 +++++++- Sources/Sentry/Public/SentryOptions.h | 2 ++ Sources/Sentry/SentryClient.m | 5 +++++ Sources/Sentry/SentryOptions.m | 4 ++++ 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index bde3d3d51da..616cd56a31f 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -8,9 +8,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SentrySDK.start { options in options.dsn = "https://387714a4f3654858a6f0ff63fd551485@o447951.ingest.sentry.io/5428557" + options.beforeSend = { event in return event } + + options.onCrashedLastRun = { eventId in + self.displayUserFeedback(eventId: eventId) + } + options.debug = true options.logLevel = SentryLogLevel.verbose options.attachStacktrace = true @@ -19,6 +25,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + + private func displayUserFeedback(eventId: SentryId) { + let userFeedback = UserFeedack(eventId: eventId) + userFeedback.comments = "It broke on iOS-Swift from onCrashedLastRun." + userFeedback.email = "john@me.com" + userFeedback.name = "John Me" + SentrySDK.capture(userFeedback: userFeedback) + } // MARK: UISceneSession Lifecycle diff --git a/Sources/Sentry/Public/SentryDefines.h b/Sources/Sentry/Public/SentryDefines.h index ad33eeb08a7..c288b4c8c9f 100644 --- a/Sources/Sentry/Public/SentryDefines.h +++ b/Sources/Sentry/Public/SentryDefines.h @@ -22,7 +22,7 @@ -(instancetype)init NS_UNAVAILABLE; \ +(instancetype) new NS_UNAVAILABLE; -@class SentryEvent, SentryBreadcrumb; +@class SentryEvent, SentryBreadcrumb, SentryId; /** * Block used for returning after a request finished @@ -48,6 +48,12 @@ typedef SentryBreadcrumb *_Nullable (^SentryBeforeBreadcrumbCallback)( */ typedef SentryEvent *_Nullable (^SentryBeforeSendEventCallback)(SentryEvent *_Nonnull event); +/** + * Block can be used to mutate event before its send. + * To avoid sending the event altogether, return nil instead. + */ +typedef void (^SentryOnCrashedLastRunCallback)(SentryId *_Nonnull eventId); + /** * Block can be used to determine if an event should be queued and stored * locally. It will be tried to send again after next successful send. Note that diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index f1474691533..03fc2e62c18 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -73,6 +73,8 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, copy) SentryBeforeBreadcrumbCallback _Nullable beforeBreadcrumb; +@property (nonatomic, copy) SentryOnCrashedLastRunCallback _Nullable onCrashedLastRun; + /** * Array of integrations to install. */ diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 024481bd90c..4b63fb019dd 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -23,6 +23,7 @@ #import "SentryTransport.h" #import "SentryTransportFactory.h" #import "SentryUser.h" +#import "SentrySDK.h" #if SENTRY_HAS_UIKIT # import @@ -328,6 +329,10 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event if (nil != self.options.beforeSend) { event = self.options.beforeSend(event); } + + if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun && event.level == kSentryLevelFatal) { + self.options.onCrashedLastRun(event.eventId); + } return event; } diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 09076c93adb..8d336dadca3 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -119,6 +119,10 @@ - (void)validateOptions:(NSDictionary *)options if (nil != options[@"beforeBreadcrumb"]) { self.beforeBreadcrumb = options[@"beforeBreadcrumb"]; } + + if (nil != options[@"onCrashedLastRun"]) { + self.onCrashedLastRun = options[@"onCrashedLastRun"]; + } if (nil != options[@"integrations"]) { self.integrations = options[@"integrations"]; From 416321d566f40663bf7d9b0560e7e762542fa8eb Mon Sep 17 00:00:00 2001 From: Clang Robot Date: Wed, 21 Oct 2020 10:18:50 +0000 Subject: [PATCH 02/13] Committing formatted code --- Sources/Sentry/SentryClient.m | 7 ++++--- Sources/Sentry/SentryOptions.m | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 4b63fb019dd..7a0be2bffea 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -17,13 +17,13 @@ #import "SentryMessage.h" #import "SentryMeta.h" #import "SentryOptions.h" +#import "SentrySDK.h" #import "SentryScope.h" #import "SentryStacktraceBuilder.h" #import "SentryThreadInspector.h" #import "SentryTransport.h" #import "SentryTransportFactory.h" #import "SentryUser.h" -#import "SentrySDK.h" #if SENTRY_HAS_UIKIT # import @@ -329,8 +329,9 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event if (nil != self.options.beforeSend) { event = self.options.beforeSend(event); } - - if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun && event.level == kSentryLevelFatal) { + + if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun + && event.level == kSentryLevelFatal) { self.options.onCrashedLastRun(event.eventId); } diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 8d336dadca3..55ea4f31600 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -119,7 +119,7 @@ - (void)validateOptions:(NSDictionary *)options if (nil != options[@"beforeBreadcrumb"]) { self.beforeBreadcrumb = options[@"beforeBreadcrumb"]; } - + if (nil != options[@"onCrashedLastRun"]) { self.onCrashedLastRun = options[@"onCrashedLastRun"]; } From fd0d8360b12b9f15ec452ec40ce85d4c7b5f5506 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 12 Nov 2020 10:15:37 +0100 Subject: [PATCH 03/13] Call callback only for first crash event --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 8 ++++---- Sources/Sentry/Public/SentryDefines.h | 5 ++--- Sources/Sentry/Public/SentryOptions.h | 8 ++++++++ Sources/Sentry/SentryClient.m | 6 ++++-- Sources/Sentry/include/SentrySDK+Private.h | 2 ++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 616cd56a31f..6e01eea6887 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -13,8 +13,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return event } - options.onCrashedLastRun = { eventId in - self.displayUserFeedback(eventId: eventId) + options.onCrashedLastRun = { event in + self.displayUserFeedback(event: event) } options.debug = true @@ -26,8 +26,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - private func displayUserFeedback(eventId: SentryId) { - let userFeedback = UserFeedack(eventId: eventId) + private func displayUserFeedback(event: Event) { + let userFeedback = UserFeedback(eventId: event.eventId) userFeedback.comments = "It broke on iOS-Swift from onCrashedLastRun." userFeedback.email = "john@me.com" userFeedback.name = "John Me" diff --git a/Sources/Sentry/Public/SentryDefines.h b/Sources/Sentry/Public/SentryDefines.h index c288b4c8c9f..6bca6708a93 100644 --- a/Sources/Sentry/Public/SentryDefines.h +++ b/Sources/Sentry/Public/SentryDefines.h @@ -49,10 +49,9 @@ typedef SentryBreadcrumb *_Nullable (^SentryBeforeBreadcrumbCallback)( typedef SentryEvent *_Nullable (^SentryBeforeSendEventCallback)(SentryEvent *_Nonnull event); /** - * Block can be used to mutate event before its send. - * To avoid sending the event altogether, return nil instead. + * A callback to be notified when the last program execution terminated with a crash. */ -typedef void (^SentryOnCrashedLastRunCallback)(SentryId *_Nonnull eventId); +typedef void (^SentryOnCrashedLastRunCallback)(SentryEvent *_Nonnull event); /** * Block can be used to determine if an event should be queued and stored diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index d5c2c3c208c..b82c2b0df14 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -79,6 +79,14 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, copy) SentryBeforeBreadcrumbCallback _Nullable beforeBreadcrumb; +/** + * This gets called shortly after the initialization of the SDK when the last program execution + * terminated with a crash. + * + * @discussion When the SDK has to send multiple crash events, which can happen when the program + * terminates with a crash before the SDK can send the crash event, this callback is only executed + * for the first crash event. You can look into beforeSend if you prefer a callback for every event. + */ @property (nonatomic, copy) SentryOnCrashedLastRunCallback _Nullable onCrashedLastRun; /** diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index a5db0e361f2..a53edfd5e69 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -17,6 +17,7 @@ #import "SentryMessage.h" #import "SentryMeta.h" #import "SentryOptions.h" +#import "SentrySDK+Private.h" #import "SentrySDK.h" #import "SentryScope.h" #import "SentryStacktraceBuilder.h" @@ -368,8 +369,9 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event } if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun - && event.level == kSentryLevelFatal) { - self.options.onCrashedLastRun(event.eventId); + && !SentrySDK.crashedLastRunCalled && event.level == kSentryLevelFatal) { + SentrySDK.crashedLastRunCalled = YES; + self.options.onCrashedLastRun(event); } return event; diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index fd5b6db00fd..3144e8ff120 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)captureCrashEvent:(SentryEvent *)event; +@property (nonatomic, class) BOOL crashedLastRunCalled; + @end NS_ASSUME_NONNULL_END From eb52611ca1a2a2cde430580d8bfb23b5602d943f Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 12 Nov 2020 10:43:23 +0100 Subject: [PATCH 04/13] Add comment about downside of callback --- Sources/Sentry/SentryClient.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index a53edfd5e69..bbefc173ccb 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -368,6 +368,11 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event event = self.options.beforeSend(event); } + // The callback is also called when SentrySDK.crashedLastRun is true and the + // SentryCrashReportSink didn't capture the crash event yet and the user manually captures an + // event with level fatal. + // We could also call this callback in SentryHub.captureCrashEvent, but then the event would + // miss detail from SentryClient.prepareEvent. if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun && !SentrySDK.crashedLastRunCalled && event.level == kSentryLevelFatal) { SentrySDK.crashedLastRunCalled = YES; From 16ff083d9a964742d3ab37af44dc68f7d0e24445 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Mon, 16 Nov 2020 15:54:09 +0100 Subject: [PATCH 05/13] Check if event contains unhandled exception --- Sources/Sentry/SentryClient.m | 39 ++++++++++++++++++++++++++--------- Sources/Sentry/SentrySDK.m | 11 ++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index bbefc173ccb..49551b1dbec 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -14,6 +14,7 @@ #import "SentryId.h" #import "SentryInstallation.h" #import "SentryLog.h" +#import "SentryMechanism.h" #import "SentryMessage.h" #import "SentryMeta.h" #import "SentryOptions.h" @@ -368,16 +369,7 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event event = self.options.beforeSend(event); } - // The callback is also called when SentrySDK.crashedLastRun is true and the - // SentryCrashReportSink didn't capture the crash event yet and the user manually captures an - // event with level fatal. - // We could also call this callback in SentryHub.captureCrashEvent, but then the event would - // miss detail from SentryClient.prepareEvent. - if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun - && !SentrySDK.crashedLastRunCalled && event.level == kSentryLevelFatal) { - SentrySDK.crashedLastRunCalled = YES; - self.options.onCrashedLastRun(event); - } + [self callOnCrashedLastRun:event]; return event; } @@ -435,6 +427,33 @@ - (void)setUserIdIfNoUserSet:(SentryEvent *)event } } +/** + * Calls onCrashedLastRun if the execution terminated with a crash. + */ +- (void)callOnCrashedLastRun:(SentryEvent *)event +{ + if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun + && !SentrySDK.crashedLastRunCalled && nil != event.exceptions) { + + NSPredicate *unhandledExpeptions = [NSPredicate predicateWithBlock:^BOOL( + id _Nullable object, NSDictionary *_Nullable bindings) { + SentryException *exception = object; + return nil != exception.mechanism && nil != exception.mechanism.handled + && ![exception.mechanism.handled boolValue]; + }]; + + // We only want to filter the array if the above conditions are true to avoid unecessary work + BOOL eventContainsUnhandledExceptions = + [event.exceptions filteredArrayUsingPredicate:unhandledExpeptions].count > 0; + + if (eventContainsUnhandledExceptions) { + // We only want to call the callback once. It can occur that multiple crash events are about to be sent. + SentrySDK.crashedLastRunCalled = YES; + self.options.onCrashedLastRun(event); + } + } +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 7abc697f8ef..f8d1600d536 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -23,6 +23,7 @@ @implementation SentrySDK static SentryHub *currentHub; +static BOOL crashedLastRunCalled; @dynamic logLevel; @@ -43,6 +44,16 @@ + (void)setCurrentHub:(SentryHub *)hub } } ++ (BOOL)crashedLastRunCalled +{ + return crashedLastRunCalled; +} + ++ (void)setCrashedLastRunCalled:(BOOL)called +{ + crashedLastRunCalled = called; +} + + (void)startWithOptions:(NSDictionary *)optionsDict { NSError *error = nil; From c99eeab8e6a211edc4700074eb703e2218a4e687 Mon Sep 17 00:00:00 2001 From: Clang Robot Date: Mon, 16 Nov 2020 14:55:38 +0000 Subject: [PATCH 06/13] Committing formatted code --- Sources/Sentry/SentryClient.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 49551b1dbec..62dc747ab61 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -442,12 +442,14 @@ - (void)callOnCrashedLastRun:(SentryEvent *)event && ![exception.mechanism.handled boolValue]; }]; - // We only want to filter the array if the above conditions are true to avoid unecessary work + // We only want to filter the array if the above conditions are true to avoid unecessary + // work BOOL eventContainsUnhandledExceptions = [event.exceptions filteredArrayUsingPredicate:unhandledExpeptions].count > 0; if (eventContainsUnhandledExceptions) { - // We only want to call the callback once. It can occur that multiple crash events are about to be sent. + // We only want to call the callback once. It can occur that multiple crash events are + // about to be sent. SentrySDK.crashedLastRunCalled = YES; self.options.onCrashedLastRun(event); } From dacc5612cbe15357007b6d1406ce4dfff23779fd Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 19 Nov 2020 19:21:16 +0100 Subject: [PATCH 07/13] Add tests --- Sources/Sentry/Public/SentryOptions.h | 9 ++-- Sources/Sentry/SentryClient.m | 8 ++- Sources/Sentry/SentrySDK.m | 4 +- Sources/Sentry/include/SentrySDK+Private.h | 3 ++ Tests/SentryTests/SentryClient+TestInit.h | 5 +- Tests/SentryTests/SentryClientTests.swift | 57 +++++++++++++++++++++- Tests/SentryTests/SentryOptionsTest.m | 22 +++++++++ 7 files changed, 99 insertions(+), 9 deletions(-) diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index b82c2b0df14..0284025ac8c 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -81,11 +81,12 @@ NS_SWIFT_NAME(Options) /** * This gets called shortly after the initialization of the SDK when the last program execution - * terminated with a crash. + * terminated with a crash. It is not guaranteed that this is called on the main thread. * - * @discussion When the SDK has to send multiple crash events, which can happen when the program - * terminates with a crash before the SDK can send the crash event, this callback is only executed - * for the first crash event. You can look into beforeSend if you prefer a callback for every event. + * @discussion This callback is only executed once during the entire run of the program to avoid + * multiple callbacks if there are multiple crash events to send. This can happen when the program + * terminates with a crash before the SDK can send the crash event. You can look into beforeSend if + * you prefer a callback for every event. */ @property (nonatomic, copy) SentryOnCrashedLastRunCallback _Nullable onCrashedLastRun; diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 62dc747ab61..0fbdfc58530 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -1,5 +1,6 @@ #import "SentryClient.h" #import "NSDictionary+SentrySanitize.h" +#import "SentryCrashAdapter.h" #import "SentryCrashDefaultBinaryImageProvider.h" #import "SentryCrashDefaultMachineContextWrapper.h" #import "SentryDebugMetaBuilder.h" @@ -41,6 +42,7 @@ @property (nonatomic, strong) SentryFileManager *fileManager; @property (nonatomic, strong) SentryDebugMetaBuilder *debugMetaBuilder; @property (nonatomic, strong) SentryThreadInspector *threadInspector; +@property (nonatomic, strong) SentryCrashAdapter *crashAdapter; @end @@ -82,6 +84,8 @@ - (_Nullable instancetype)initWithOptions:(SentryOptions *)options self.transport = [SentryTransportFactory initTransport:self.options sentryFileManager:self.fileManager]; + + self.crashAdapter = [[SentryCrashAdapter alloc] init]; } return self; } @@ -90,11 +94,13 @@ - (_Nullable instancetype)initWithOptions:(SentryOptions *)options - (instancetype)initWithOptions:(SentryOptions *)options andTransport:(id)transport andFileManager:(SentryFileManager *)fileManager + andCrashAdapter:(SentryCrashAdapter *)crashAdapter { self = [self initWithOptions:options]; self.transport = transport; self.fileManager = fileManager; + self.crashAdapter = crashAdapter; return self; } @@ -432,7 +438,7 @@ - (void)setUserIdIfNoUserSet:(SentryEvent *)event */ - (void)callOnCrashedLastRun:(SentryEvent *)event { - if (nil != self.options.onCrashedLastRun && SentrySDK.crashedLastRun + if (nil != self.options.onCrashedLastRun && self.crashAdapter.crashedLastLaunch && !SentrySDK.crashedLastRunCalled && nil != event.exceptions) { NSPredicate *unhandledExpeptions = [NSPredicate predicateWithBlock:^BOOL( diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index f8d1600d536..a96750122ed 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -49,9 +49,9 @@ + (BOOL)crashedLastRunCalled return crashedLastRunCalled; } -+ (void)setCrashedLastRunCalled:(BOOL)called ++ (void)setCrashedLastRunCalled:(BOOL)value { - crashedLastRunCalled = called; + crashedLastRunCalled = value; } + (void)startWithOptions:(NSDictionary *)optionsDict diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index 3144e8ff120..634d1f9f3a0 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -8,6 +8,9 @@ NS_ASSUME_NONNULL_BEGIN + (void)captureCrashEvent:(SentryEvent *)event; +/** + * SDK private field to store the state if onCrashedLastRun was called. + */ @property (nonatomic, class) BOOL crashedLastRunCalled; @end diff --git a/Tests/SentryTests/SentryClient+TestInit.h b/Tests/SentryTests/SentryClient+TestInit.h index 9d03af701d9..4520e806ce6 100644 --- a/Tests/SentryTests/SentryClient+TestInit.h +++ b/Tests/SentryTests/SentryClient+TestInit.h @@ -1,6 +1,8 @@ #import "SentryTransport.h" #import +@class SentryCrashAdapter; + NS_ASSUME_NONNULL_BEGIN /** Expose the internal test init for testing. */ @@ -8,7 +10,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithOptions:(SentryOptions *)options andTransport:(id)transport - andFileManager:(SentryFileManager *)fileManager; + andFileManager:(SentryFileManager *)fileManager + andCrashAdapter:(SentryCrashAdapter *)crashAdapter; @end diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 16ffc82e979..7eb7251c015 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -25,6 +25,8 @@ class SentryClientTest: XCTestCase { let user: User let fileManager: SentryFileManager + let crashAdapter = TestSentryCrashWrapper() + init() { session = SentrySession(releaseName: "release") session.incrementErrors() @@ -48,7 +50,7 @@ class SentryClientTest: XCTestCase { ]) configureOptions(options) - client = Client(options: options, andTransport: transport, andFileManager: fileManager) + client = Client(options: options, andTransport: transport, andFileManager: fileManager, andCrashAdapter: crashAdapter) } catch { XCTFail("Options could not be created") } @@ -76,6 +78,16 @@ class SentryClientTest: XCTestCase { return scope } } + + var eventWithCrash: Event { + let event = TestData.event + let exception = Exception(value: "value", type: "type") + let mechanism = Mechanism(type: "mechanism") + mechanism.handled = false + exception.mechanism = mechanism + event.exceptions = [exception] + return event + } } private let error = NSError(domain: "domain", code: -20, userInfo: [NSLocalizedDescriptionKey: "Object does not exist"]) @@ -90,6 +102,11 @@ class SentryClientTest: XCTestCase { fixture.fileManager.deleteAllEnvelopes() } + override func tearDown() { + super.tearDown() + SentrySDK.crashedLastRunCalled = false + } + func testCaptureMessage() { let eventId = fixture.getSut().capture(message: fixture.messageAsString) @@ -615,6 +632,44 @@ class SentryClientTest: XCTestCase { XCTAssertEqual(1, fixture.fileManager.getAllEnvelopes().count) } + func testOnCrashedLastRun_WithTwoCrashes_OnlyInvoceOnce() { + let event = fixture.eventWithCrash + + var onCrashedLastRunCalled = false + let client = fixture.getSut(configureOptions: { options in + options.onCrashedLastRun = { crashEvent in + onCrashedLastRunCalled = true + XCTAssertEqual(event.eventId, crashEvent.eventId) + } + }) + fixture.crashAdapter.internalCrashedLastLaunch = true + + client.capture(event: event) + client.capture(event: fixture.eventWithCrash) + + XCTAssertTrue(onCrashedLastRunCalled) + } + + func testOnCrashedLastRun_WithHandledException() { + var onCrashedLastRunCalled = false + let client = fixture.getSut(configureOptions: { options in + options.onCrashedLastRun = { _ in + onCrashedLastRunCalled = true + } + }) + fixture.crashAdapter.internalCrashedLastLaunch = true + + client.capture(event: TestData.event) + + XCTAssertFalse(onCrashedLastRunCalled) + } + + func testOnCrashedLastRun_WithOutCallback_DoesNothing() { + let client = fixture.getSut() + fixture.crashAdapter.internalCrashedLastLaunch = true + client.capture(event: fixture.eventWithCrash) + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 9c58cbdaa89..8d45f70ab24 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -203,6 +203,28 @@ - (void)testDefaultBeforeBreadcrumb XCTAssertNil(options.beforeBreadcrumb); } +- (void)testOnCrashedLastRun +{ + __block BOOL onCrashedLastRunCalled = NO; + void (^callback)(SentryEvent *event) = ^(SentryEvent *event) { + onCrashedLastRunCalled = YES; + XCTAssertNotNil(event); + }; + SentryOptions *options = [self getValidOptions:@{ @"onCrashedLastRun" : callback }]; + + options.onCrashedLastRun([[SentryEvent alloc] init]); + + XCTAssertEqual(callback, options.onCrashedLastRun); + XCTAssertTrue(onCrashedLastRunCalled); +} + +- (void)testDefaultOnCrashedLastRun +{ + SentryOptions *options = [self getValidOptions:@{}]; + + XCTAssertNil(options.onCrashedLastRun); +} + - (void)testIntegrations { NSArray *integrations = @[ @"integration1", @"integration2" ]; From b3e49d8722bcedc9a8d6b86f4ba2495c3146dfa7 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 19 Nov 2020 19:23:04 +0100 Subject: [PATCH 08/13] Undo changes in iOS-Swift AppDelegate --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 6e01eea6887..bde3d3d51da 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -8,15 +8,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SentrySDK.start { options in options.dsn = "https://387714a4f3654858a6f0ff63fd551485@o447951.ingest.sentry.io/5428557" - options.beforeSend = { event in return event } - - options.onCrashedLastRun = { event in - self.displayUserFeedback(event: event) - } - options.debug = true options.logLevel = SentryLogLevel.verbose options.attachStacktrace = true @@ -25,14 +19,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - - private func displayUserFeedback(event: Event) { - let userFeedback = UserFeedback(eventId: event.eventId) - userFeedback.comments = "It broke on iOS-Swift from onCrashedLastRun." - userFeedback.email = "john@me.com" - userFeedback.name = "John Me" - SentrySDK.capture(userFeedback: userFeedback) - } // MARK: UISceneSession Lifecycle From 80439eaa633ff6cacbb39ce6f8b99b988d5a85b0 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 19 Nov 2020 19:24:03 +0100 Subject: [PATCH 09/13] Remove SentryId from SentryDefines --- Sources/Sentry/Public/SentryDefines.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/Public/SentryDefines.h b/Sources/Sentry/Public/SentryDefines.h index 6bca6708a93..b6e1affe373 100644 --- a/Sources/Sentry/Public/SentryDefines.h +++ b/Sources/Sentry/Public/SentryDefines.h @@ -22,7 +22,7 @@ -(instancetype)init NS_UNAVAILABLE; \ +(instancetype) new NS_UNAVAILABLE; -@class SentryEvent, SentryBreadcrumb, SentryId; +@class SentryEvent, SentryBreadcrumb; /** * Block used for returning after a request finished From fa66553d001712d2f7c0d75a6a9e60a772bf0078 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 19 Nov 2020 19:25:53 +0100 Subject: [PATCH 10/13] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc411a26303..52648e0b820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## unreleased +- feat: Add onCrashedLastRun #808 + ## 6.0.9 - fix: Serialization of SentryScope #841 From 8a9faea4e8863fecc1dfe2e8094c8d6ee16ab524 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 24 Nov 2020 09:49:56 +0100 Subject: [PATCH 11/13] Toggle BOOL after action for callOnCrashedLastRun Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> --- Sources/Sentry/SentryClient.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 0fbdfc58530..390f20b2f45 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -456,8 +456,8 @@ - (void)callOnCrashedLastRun:(SentryEvent *)event if (eventContainsUnhandledExceptions) { // We only want to call the callback once. It can occur that multiple crash events are // about to be sent. - SentrySDK.crashedLastRunCalled = YES; self.options.onCrashedLastRun(event); + SentrySDK.crashedLastRunCalled = YES; } } } From b04e3880f8013e0dc534978f6f04b8cf592e6002 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 24 Nov 2020 09:52:39 +0100 Subject: [PATCH 12/13] Fix typos --- Sources/Sentry/SentryClient.m | 2 +- Tests/SentryTests/SentryClientTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 390f20b2f45..8e9adf5fa60 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -434,7 +434,7 @@ - (void)setUserIdIfNoUserSet:(SentryEvent *)event } /** - * Calls onCrashedLastRun if the execution terminated with a crash. + * Calls onCrashedLastRun if the last execution terminated with a crash. */ - (void)callOnCrashedLastRun:(SentryEvent *)event { diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 7eb7251c015..851d039dc2c 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -632,7 +632,7 @@ class SentryClientTest: XCTestCase { XCTAssertEqual(1, fixture.fileManager.getAllEnvelopes().count) } - func testOnCrashedLastRun_WithTwoCrashes_OnlyInvoceOnce() { + func testOnCrashedLastRun_WithTwoCrashes_OnlyInvokeOnce() { let event = fixture.eventWithCrash var onCrashedLastRunCalled = false @@ -664,7 +664,7 @@ class SentryClientTest: XCTestCase { XCTAssertFalse(onCrashedLastRunCalled) } - func testOnCrashedLastRun_WithOutCallback_DoesNothing() { + func testOnCrashedLastRun_WithoutCallback_DoesNothing() { let client = fixture.getSut() fixture.crashAdapter.internalCrashedLastLaunch = true client.capture(event: fixture.eventWithCrash) From 4950e6bd7da8dab881199aa5b8c41591f0c3a2a5 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 24 Nov 2020 11:22:27 +0100 Subject: [PATCH 13/13] Add captureCrash to SentryClient --- Sources/Sentry/SentryClient.m | 87 +++++++++-------- Sources/Sentry/SentryHub.m | 4 +- Sources/Sentry/include/SentryClient+Private.h | 8 +- .../SentrySessionTrackerTests.swift | 6 +- Tests/SentryTests/SentryClient+TestInit.h | 3 +- Tests/SentryTests/SentryClientTests.swift | 94 +++++++++++++------ Tests/SentryTests/SentryHubTests.swift | 20 ++-- Tests/SentryTests/TestClient.swift | 12 ++- 8 files changed, 141 insertions(+), 93 deletions(-) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8e9adf5fa60..a274338a086 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -1,6 +1,5 @@ #import "SentryClient.h" #import "NSDictionary+SentrySanitize.h" -#import "SentryCrashAdapter.h" #import "SentryCrashDefaultBinaryImageProvider.h" #import "SentryCrashDefaultMachineContextWrapper.h" #import "SentryDebugMetaBuilder.h" @@ -15,12 +14,10 @@ #import "SentryId.h" #import "SentryInstallation.h" #import "SentryLog.h" -#import "SentryMechanism.h" #import "SentryMessage.h" #import "SentryMeta.h" #import "SentryOptions.h" #import "SentrySDK+Private.h" -#import "SentrySDK.h" #import "SentryScope.h" #import "SentryStacktraceBuilder.h" #import "SentryThreadInspector.h" @@ -42,7 +39,6 @@ @property (nonatomic, strong) SentryFileManager *fileManager; @property (nonatomic, strong) SentryDebugMetaBuilder *debugMetaBuilder; @property (nonatomic, strong) SentryThreadInspector *threadInspector; -@property (nonatomic, strong) SentryCrashAdapter *crashAdapter; @end @@ -84,8 +80,6 @@ - (_Nullable instancetype)initWithOptions:(SentryOptions *)options self.transport = [SentryTransportFactory initTransport:self.options sentryFileManager:self.fileManager]; - - self.crashAdapter = [[SentryCrashAdapter alloc] init]; } return self; } @@ -94,13 +88,11 @@ - (_Nullable instancetype)initWithOptions:(SentryOptions *)options - (instancetype)initWithOptions:(SentryOptions *)options andTransport:(id)transport andFileManager:(SentryFileManager *)fileManager - andCrashAdapter:(SentryCrashAdapter *)crashAdapter { self = [self initWithOptions:options]; self.transport = transport; self.fileManager = fileManager; - self.crashAdapter = crashAdapter; return self; } @@ -184,13 +176,19 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error return event; } -- (SentryId *)captureEvent:(SentryEvent *)event - withSession:(SentrySession *)session - withScope:(SentryScope *)scope +- (SentryId *)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope +{ + return [self sendEvent:event withScope:scope alwaysAttachStacktrace:NO isCrashEvent:YES]; +} + +- (SentryId *)captureCrashEvent:(SentryEvent *)event + withSession:(SentrySession *)session + withScope:(SentryScope *)scope { SentryEvent *preparedEvent = [self prepareEvent:event withScope:scope - alwaysAttachStacktrace:NO]; + alwaysAttachStacktrace:NO + isCrashEvent:YES]; return [self sendEvent:preparedEvent withSession:session]; } @@ -207,10 +205,22 @@ - (SentryId *)captureEvent:(SentryEvent *)event withScope:(SentryScope *)scope - (SentryId *)sendEvent:(SentryEvent *)event withScope:(SentryScope *)scope alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace +{ + return [self sendEvent:event + withScope:scope + alwaysAttachStacktrace:alwaysAttachStacktrace + isCrashEvent:NO]; +} + +- (SentryId *)sendEvent:(SentryEvent *)event + withScope:(SentryScope *)scope + alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace + isCrashEvent:(BOOL)isCrashEvent { SentryEvent *preparedEvent = [self prepareEvent:event withScope:scope - alwaysAttachStacktrace:alwaysAttachStacktrace]; + alwaysAttachStacktrace:alwaysAttachStacktrace + isCrashEvent:isCrashEvent]; if (nil != preparedEvent) { [self.transport sendEvent:preparedEvent]; @@ -298,6 +308,17 @@ - (BOOL)checkSampleRate:(NSNumber *)sampleRate - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event withScope:(SentryScope *)scope alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace +{ + return [self prepareEvent:event + withScope:scope + alwaysAttachStacktrace:alwaysAttachStacktrace + isCrashEvent:NO]; +} + +- (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event + withScope:(SentryScope *)scope + alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace + isCrashEvent:(BOOL)isCrashEvent { NSParameterAssert(event); if ([self isDisabled]) { @@ -349,12 +370,12 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event || (nil != event.exceptions && [event.exceptions count] > 0); BOOL debugMetaNotAttached = !(nil != event.debugMeta && event.debugMeta.count > 0); - if (shouldAttachStacktrace && debugMetaNotAttached) { + if (!isCrashEvent && shouldAttachStacktrace && debugMetaNotAttached) { event.debugMeta = [self.debugMetaBuilder buildDebugMeta]; } BOOL threadsNotAttached = !(nil != event.threads && event.threads.count > 0); - if (shouldAttachStacktrace && threadsNotAttached) { + if (!isCrashEvent && shouldAttachStacktrace && threadsNotAttached) { event.threads = [self.threadInspector getCurrentThreads]; } @@ -375,7 +396,12 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event event = self.options.beforeSend(event); } - [self callOnCrashedLastRun:event]; + if (isCrashEvent && nil != self.options.onCrashedLastRun && !SentrySDK.crashedLastRunCalled) { + // We only want to call the callback once. It can occur that multiple crash events are + // about to be sent. + self.options.onCrashedLastRun(event); + SentrySDK.crashedLastRunCalled = YES; + } return event; } @@ -433,35 +459,6 @@ - (void)setUserIdIfNoUserSet:(SentryEvent *)event } } -/** - * Calls onCrashedLastRun if the last execution terminated with a crash. - */ -- (void)callOnCrashedLastRun:(SentryEvent *)event -{ - if (nil != self.options.onCrashedLastRun && self.crashAdapter.crashedLastLaunch - && !SentrySDK.crashedLastRunCalled && nil != event.exceptions) { - - NSPredicate *unhandledExpeptions = [NSPredicate predicateWithBlock:^BOOL( - id _Nullable object, NSDictionary *_Nullable bindings) { - SentryException *exception = object; - return nil != exception.mechanism && nil != exception.mechanism.handled - && ![exception.mechanism.handled boolValue]; - }]; - - // We only want to filter the array if the above conditions are true to avoid unecessary - // work - BOOL eventContainsUnhandledExceptions = - [event.exceptions filteredArrayUsingPredicate:unhandledExpeptions].count > 0; - - if (eventContainsUnhandledExceptions) { - // We only want to call the callback once. It can occur that multiple crash events are - // about to be sent. - self.options.onCrashedLastRun(event); - SentrySDK.crashedLastRunCalled = YES; - } - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 34bb9990a17..c90752a046f 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -203,13 +203,13 @@ - (void)captureCrashEvent:(SentryEvent *)event // It can be that there is no session yet, because autoSessionTracking was just enabled and // there is a previous crash on disk. In this case we just send the crash event. if (nil != crashedSession) { - [client captureEvent:event withSession:crashedSession withScope:self.scope]; + [client captureCrashEvent:event withSession:crashedSession withScope:self.scope]; [fileManager deleteCrashedSession]; return; } } - [self captureEvent:event withScope:self.scope]; + [client captureCrashEvent:event withScope:self.scope]; } - (SentryId *)captureEvent:(SentryEvent *)event diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index a02b7729c1e..3e48b55d646 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -17,9 +17,11 @@ NS_ASSUME_NONNULL_BEGIN withSession:(SentrySession *)session withScope:(SentryScope *)scope; -- (SentryId *)captureEvent:(SentryEvent *)event - withSession:(SentrySession *)session - withScope:(SentryScope *)scope; +- (SentryId *)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope; + +- (SentryId *)captureCrashEvent:(SentryEvent *)event + withSession:(SentrySession *)session + withScope:(SentryScope *)scope; @end diff --git a/Tests/SentryTests/Integrations/SentrySessionTrackerTests.swift b/Tests/SentryTests/Integrations/SentrySessionTrackerTests.swift index 548de25e82a..537971a95f2 100644 --- a/Tests/SentryTests/Integrations/SentrySessionTrackerTests.swift +++ b/Tests/SentryTests/Integrations/SentrySessionTrackerTests.swift @@ -448,7 +448,7 @@ class SentrySessionTrackerTests: XCTestCase { } private func assertNoInitSessionSent() { - let eventWithSessions = fixture.client.captureEventWithSessionArguments.map({ triple in triple.second }) + let eventWithSessions = fixture.client.captureCrashEventWithSessionArguments.map({ triple in triple.second }) let errorWithSessions = fixture.client.captureErrorWithSessionArguments.map({ triple in triple.second }) let exceptionWithSessions = fixture.client.captureExceptionWithSessionArguments.map({ triple in triple.second }) @@ -462,7 +462,7 @@ class SentrySessionTrackerTests: XCTestCase { } private func assertSessionsSent(count: Int) { - let eventWithSessions = fixture.client.captureEventWithSessionArguments.count + let eventWithSessions = fixture.client.captureCrashEventWithSessionArguments.count let errorWithSessions = fixture.client.captureErrorWithSessionArguments.count let exceptionWithSessions = fixture.client.captureExceptionWithSessionArguments.count let sessions = fixture.client.sessions.count @@ -496,7 +496,7 @@ class SentrySessionTrackerTests: XCTestCase { sut.start() SentrySDK.captureCrash(Event()) - if let session = fixture.client.captureEventWithSessionArguments.last?.second { + if let session = fixture.client.captureCrashEventWithSessionArguments.last?.second { assertSession(session: session, started: sessionStartTime, status: SentrySessionStatus.crashed, duration: 5) } else { XCTFail("No session sent with event.") diff --git a/Tests/SentryTests/SentryClient+TestInit.h b/Tests/SentryTests/SentryClient+TestInit.h index 4520e806ce6..2e5a39c6617 100644 --- a/Tests/SentryTests/SentryClient+TestInit.h +++ b/Tests/SentryTests/SentryClient+TestInit.h @@ -10,8 +10,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithOptions:(SentryOptions *)options andTransport:(id)transport - andFileManager:(SentryFileManager *)fileManager - andCrashAdapter:(SentryCrashAdapter *)crashAdapter; + andFileManager:(SentryFileManager *)fileManager; @end diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 851d039dc2c..4df78257c81 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -24,8 +24,6 @@ class SentryClientTest: XCTestCase { let user: User let fileManager: SentryFileManager - - let crashAdapter = TestSentryCrashWrapper() init() { session = SentrySession(releaseName: "release") @@ -50,7 +48,7 @@ class SentryClientTest: XCTestCase { ]) configureOptions(options) - client = Client(options: options, andTransport: transport, andFileManager: fileManager, andCrashAdapter: crashAdapter) + client = Client(options: options, andTransport: transport, andFileManager: fileManager) } catch { XCTFail("Options could not be created") } @@ -292,20 +290,56 @@ class SentryClientTest: XCTestCase { assertLastSentEnvelopeIsASession() } - func testCaptureEventWithSession() { - let eventId = fixture.getSut().capture(fixture.event, with: fixture.session, with: fixture.scope) + func testCaptureCrashEventWithSession() { + let eventId = fixture.getSut().captureCrash(fixture.event, with: fixture.session, with: fixture.scope) eventId.assertIsNotEmpty() - XCTAssertNotNil(fixture.transport.sentEventsWithSession.last) - if let eventWithSessionArguments = fixture.transport.sentEventsWithSession.last { - let event = eventWithSessionArguments.first + + assertLastSentEventWithSession { event, session in XCTAssertEqual(fixture.event.eventId, event.eventId) XCTAssertEqual(fixture.event.message, event.message) XCTAssertEqual("value", event.tags?["key"] ?? "") - XCTAssertEqual(fixture.session, eventWithSessionArguments.second) + XCTAssertEqual(fixture.session, session) } + } + + func testCaptureCrashWithSession_DoesntOverideStacktrace() { + let event = TestData.event + event.threads = nil + event.debugMeta = nil + + fixture.getSut().captureCrash(event, with: fixture.session, with: fixture.scope) + + assertLastSentEventWithSession { event, _ in + XCTAssertNil(event.threads) + XCTAssertNil(event.debugMeta) + } + } + + func testCaptureCrashEvent() { + let eventId = fixture.getSut().captureCrash(fixture.event, with: fixture.scope) + eventId.assertIsNotEmpty() + + assertLastSentEvent { event in + XCTAssertEqual(fixture.event.eventId, event.eventId) + XCTAssertEqual(fixture.event.message, event.message) + XCTAssertEqual("value", event.tags?["key"] ?? "") + } + } + + func testCaptureCrash_DoesntOverideStacktraceFor() { + let event = TestData.event + event.threads = nil + event.debugMeta = nil + + fixture.getSut().captureCrash(event, with: fixture.scope) + + assertLastSentEvent { actual in + XCTAssertNil(actual.threads) + XCTAssertNil(actual.debugMeta) + } } func testCaptureErrorWithUserInfo() { @@ -383,7 +417,7 @@ class SentryClientTest: XCTestCase { fixture.getSut().capture(session: session) fixture.getSut().capture(exception, with: session, with: Scope()) .assertIsNotEmpty() - fixture.getSut().capture(fixture.event, with: session, with: Scope()) + fixture.getSut().captureCrash(fixture.event, with: session, with: Scope()) .assertIsNotEmpty() // No sessions sent @@ -476,7 +510,7 @@ class SentryClientTest: XCTestCase { _ = SentryEnvelope(event: Event()) let eventId = fixture.getSut(configureOptions: { options in options.dsn = nil - }).capture(Event(), with: fixture.session, with: Scope()) + }).captureCrash(Event(), with: fixture.session, with: Scope()) eventId.assertIsEmpty() assertNothingSent() @@ -632,42 +666,39 @@ class SentryClientTest: XCTestCase { XCTAssertEqual(1, fixture.fileManager.getAllEnvelopes().count) } - func testOnCrashedLastRun_WithTwoCrashes_OnlyInvokeOnce() { - let event = fixture.eventWithCrash + func testOnCrashedLastRun_OnCaptureCrashWithSession() { + let event = TestData.event var onCrashedLastRunCalled = false - let client = fixture.getSut(configureOptions: { options in - options.onCrashedLastRun = { crashEvent in + fixture.getSut(configureOptions: { options in + options.onCrashedLastRun = { _ in onCrashedLastRunCalled = true - XCTAssertEqual(event.eventId, crashEvent.eventId) } - }) - fixture.crashAdapter.internalCrashedLastLaunch = true - - client.capture(event: event) - client.capture(event: fixture.eventWithCrash) + }).captureCrash(event, with: fixture.session, with: fixture.scope) XCTAssertTrue(onCrashedLastRunCalled) } - func testOnCrashedLastRun_WithHandledException() { + func testOnCrashedLastRun_WithTwoCrashes_OnlyInvokeCallbackOnce() { + let event = TestData.event + var onCrashedLastRunCalled = false let client = fixture.getSut(configureOptions: { options in - options.onCrashedLastRun = { _ in + options.onCrashedLastRun = { crashEvent in onCrashedLastRunCalled = true + XCTAssertEqual(event.eventId, crashEvent.eventId) } }) - fixture.crashAdapter.internalCrashedLastLaunch = true - client.capture(event: TestData.event) + client.captureCrash(event, with: fixture.scope) + client.captureCrash(TestData.event, with: fixture.scope) - XCTAssertFalse(onCrashedLastRunCalled) + XCTAssertTrue(onCrashedLastRunCalled) } func testOnCrashedLastRun_WithoutCallback_DoesNothing() { let client = fixture.getSut() - fixture.crashAdapter.internalCrashedLastLaunch = true - client.capture(event: fixture.eventWithCrash) + client.captureCrash(TestData.event, with: fixture.scope) } private func givenEventWithDebugMeta() -> Event { @@ -706,6 +737,13 @@ class SentryClientTest: XCTestCase { } } + private func assertLastSentEventWithSession(assert: (Event, SentrySession) -> Void) { + XCTAssertNotNil(fixture.transport.sentEventsWithSession.last) + if let args = fixture.transport.sentEventsWithSession.last { + assert(args.first, args.second) + } + } + private func assertValidErrorEvent(_ event: Event) { XCTAssertEqual(SentryLevel.error, event.level) XCTAssertEqual("\(error.domain) \(error.code)", event.message.formatted) diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index 77e8c9e3d40..9a8f3fdf721 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -365,7 +365,7 @@ class SentryHubTests: XCTestCase { // Make sure further crash events are sent sut.captureCrash(fixture.event) - assertEventSent() + assertCrashEventSent() } func testCaptureCrashEvent_CrashedSessionDoesNotExist() { @@ -373,7 +373,7 @@ class SentryHubTests: XCTestCase { sut.captureCrash(fixture.event) assertNoCrashedSessionSent() - assertEventSent() + assertCrashEventSent() } /** @@ -382,7 +382,7 @@ class SentryHubTests: XCTestCase { func testCatpureCrashEvent_CrashExistsButNoSessionExists() { sut.captureCrash(fixture.event) - assertEventSent() + assertCrashEventSent() } func testCaptureCrashEvent_WithoutExistingSessionAndAutoSessionTrackingEnabled() { @@ -390,7 +390,7 @@ class SentryHubTests: XCTestCase { sut.captureCrash(fixture.event) - assertEventSent() + assertCrashEventSent() } func testCaptureCrashEvent_SessionExistsButAutoSessionTrackingDisabled() { @@ -399,7 +399,7 @@ class SentryHubTests: XCTestCase { sut.captureCrash(fixture.event) - assertEventSent() + assertCrashEventSent() } func testCaptureCrashEvent_ClientIsNil() { @@ -478,7 +478,7 @@ class SentryHubTests: XCTestCase { private func assertNoEventsSent() { XCTAssertEqual(0, fixture.client.captureEventArguments.count) - XCTAssertEqual(0, fixture.client.captureEventWithSessionArguments.count) + XCTAssertEqual(0, fixture.client.captureCrashEventWithSessionArguments.count) } private func assertEventSent() { @@ -486,9 +486,15 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(1, arguments.count) XCTAssertEqual(fixture.event, arguments.first?.first) } + + private func assertCrashEventSent() { + let arguments = fixture.client.captureCrashEventArguments + XCTAssertEqual(1, arguments.count) + XCTAssertEqual(fixture.event, arguments.first?.first) + } private func assertEventSentWithSession() { - let arguments = fixture.client.captureEventWithSessionArguments + let arguments = fixture.client.captureCrashEventWithSessionArguments XCTAssertEqual(1, arguments.count) let argument = arguments.first diff --git a/Tests/SentryTests/TestClient.swift b/Tests/SentryTests/TestClient.swift index 62672b65d43..39c2f34bdb1 100644 --- a/Tests/SentryTests/TestClient.swift +++ b/Tests/SentryTests/TestClient.swift @@ -71,9 +71,15 @@ class TestClient: Client { return SentryId() } - var captureEventWithSessionArguments: [Triple] = [] - override func capture(_ event: Event, with session: SentrySession, with scope: Scope) -> SentryId { - captureEventWithSessionArguments.append(Triple(event, session, scope)) + var captureCrashEventArguments: [Pair] = [] + override func captureCrash(_ event: Event, with scope: Scope) -> SentryId { + captureCrashEventArguments.append(Pair(event, scope)) + return SentryId() + } + + var captureCrashEventWithSessionArguments: [Triple] = [] + override func captureCrash(_ event: Event, with session: SentrySession, with scope: Scope) -> SentryId { + captureCrashEventWithSessionArguments.append(Triple(event, session, scope)) return SentryId() }