Skip to content

Commit

Permalink
feat: Add onCrashedLastRun (#808)
Browse files Browse the repository at this point in the history
Add a callback onCrashedLastRun to SentryOptions that is called by the SDK passing the eventId
when Sentry is initialized and the last program execution terminated with a crash.

Fixes GH-699
  • Loading branch information
philipphofmann authored Nov 25, 2020
1 parent 4647554 commit 5cf484e
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## unreleased

- feat: Add onCrashedLastRun #808
- feat: Add SentrySdkInfo to SentryOptions #859

## 6.0.9
Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/Public/SentryDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ typedef SentryBreadcrumb *_Nullable (^SentryBeforeBreadcrumbCallback)(
*/
typedef SentryEvent *_Nullable (^SentryBeforeSendEventCallback)(SentryEvent *_Nonnull event);

/**
* A callback to be notified when the last program execution terminated with a crash.
*/
typedef void (^SentryOnCrashedLastRunCallback)(SentryEvent *_Nonnull event);

/**
* 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
Expand Down
11 changes: 11 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ 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. It is not guaranteed that this is called on the main thread.
*
* @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;

/**
* Array of integrations to install.
*/
Expand Down
51 changes: 44 additions & 7 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import "SentryMessage.h"
#import "SentryMeta.h"
#import "SentryOptions.h"
#import "SentrySDK+Private.h"
#import "SentryScope.h"
#import "SentryStacktraceBuilder.h"
#import "SentryThreadInspector.h"
Expand Down Expand Up @@ -175,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];
}

Expand All @@ -198,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];
Expand Down Expand Up @@ -289,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]) {
Expand Down Expand Up @@ -340,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];
}

Expand All @@ -366,6 +396,13 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event
event = self.options.beforeSend(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;
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Sentry/SentryHub.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ - (void)validateOptions:(NSDictionary<NSString *, id> *)options
self.beforeBreadcrumb = options[@"beforeBreadcrumb"];
}

if (nil != options[@"onCrashedLastRun"]) {
self.onCrashedLastRun = options[@"onCrashedLastRun"];
}

if (nil != options[@"integrations"]) {
self.integrations = options[@"integrations"];
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@implementation SentrySDK

static SentryHub *currentHub;
static BOOL crashedLastRunCalled;

@dynamic logLevel;

Expand All @@ -43,6 +44,16 @@ + (void)setCurrentHub:(SentryHub *)hub
}
}

+ (BOOL)crashedLastRunCalled
{
return crashedLastRunCalled;
}

+ (void)setCrashedLastRunCalled:(BOOL)value
{
crashedLastRunCalled = value;
}

+ (void)startWithOptions:(NSDictionary<NSString *, id> *)optionsDict
{
NSError *error = nil;
Expand Down
8 changes: 5 additions & 3 deletions Sources/Sentry/include/SentryClient+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions Sources/Sentry/include/SentrySDK+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ 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

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Expand All @@ -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
Expand Down Expand Up @@ -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.")
Expand Down
2 changes: 2 additions & 0 deletions Tests/SentryTests/SentryClient+TestInit.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#import "SentryTransport.h"
#import <Sentry/Sentry.h>

@class SentryCrashAdapter;

NS_ASSUME_NONNULL_BEGIN

/** Expose the internal test init for testing. */
Expand Down
Loading

0 comments on commit 5cf484e

Please sign in to comment.