diff --git a/CHANGELOG.md b/CHANGELOG.md index 489ce48df48..0e30bfda5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add pause and resume AppHangTracking API (#4077). You can now pause and resume app hang tracking with `SentrySDK.pauseAppHangTracking()` and `SentrySDK.resumeAppHangTracking()`. +### Fixes + +- `storeEnvelope` ends session for unhandled errors (#4073) + ## 8.29.1 ### Fixes diff --git a/SentryTestUtils/TestHub.swift b/SentryTestUtils/TestHub.swift index ad96dd190fe..ef55578857f 100644 --- a/SentryTestUtils/TestHub.swift +++ b/SentryTestUtils/TestHub.swift @@ -3,15 +3,19 @@ import Foundation public class TestHub: SentryHub { - var startSessionInvocations: Int = 0 - var closeCachedSessionInvocations: Int = 0 - var endSessionTimestamp: Date? - var closeCachedSessionTimestamp: Date? + public var startSessionInvocations: Int = 0 + public var closeCachedSessionInvocations: Int = 0 + public var endSessionTimestamp: Date? + public var closeCachedSessionTimestamp: Date? public override func startSession() { startSessionInvocations += 1 } - + + public func setTestSession() { + self.session = SentrySession(releaseName: "Test Release", distinctId: "123") + } + public override func closeCachedSession(withTimestamp timestamp: Date?) { closeCachedSessionTimestamp = timestamp closeCachedSessionInvocations += 1 diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 19aa7b96b3a..dd41d8ebecd 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -209,14 +209,9 @@ - (void)captureSession:(nullable SentrySession *)session SentryClient *client = _client; if (client.options.diagnosticLevel == kSentryLevelDebug) { - NSData *sessionData = [NSJSONSerialization dataWithJSONObject:[session serialize] - options:0 - error:nil]; - NSString *sessionString = [[NSString alloc] initWithData:sessionData - encoding:NSUTF8StringEncoding]; [SentryLog logWithMessage:[NSString stringWithFormat:@"Capturing session with status: %@", - sessionString] + [self createSessionDebugString:session]] andLevel:kSentryLevelDebug]; } [client captureSession:session]; @@ -630,6 +625,21 @@ - (void)setUser:(nullable SentryUser *)user } } +/** + * Needed by hybrid SDKs as react-native to synchronously store an envelope to disk. + */ +- (void)storeEnvelope:(SentryEnvelope *)envelope +{ + SentryClient *client = _client; + if (client == nil) { + return; + } + + // Envelopes are stored only when crash occurs. We should not start a new session when + // the app is about to crash. + [client storeEnvelope:[self updateSessionState:envelope startNewSession:NO]]; +} + - (void)captureEnvelope:(SentryEnvelope *)envelope { SentryClient *client = _client; @@ -637,10 +647,13 @@ - (void)captureEnvelope:(SentryEnvelope *)envelope return; } - [client captureEnvelope:[self updateSessionState:envelope]]; + // If captured envelope cointains not handled errors, these are not going to crash the app and + // we should create new session. + [client captureEnvelope:[self updateSessionState:envelope startNewSession:YES]]; } - (SentryEnvelope *)updateSessionState:(SentryEnvelope *)envelope + startNewSession:(BOOL)startNewSession { BOOL handled = YES; if ([self envelopeContainsEventWithErrorOrHigher:envelope.items wasHandled:&handled]) { @@ -654,9 +667,17 @@ - (SentryEnvelope *)updateSessionState:(SentryEnvelope *)envelope [currentSession endSessionCrashedWithTimestamp:[SentryDependencyContainer.sharedInstance .dateProvider date]]; - // Setting _session to nil so startSession doesn't capture it again - _session = nil; - [self startSession]; + if (_client.options.diagnosticLevel == kSentryLevelDebug) { + [SentryLog + logWithMessage:[NSString stringWithFormat:@"Ending session with status: %@", + [self createSessionDebugString:currentSession]] + andLevel:kSentryLevelDebug]; + } + if (startNewSession) { + // Setting _session to nil so startSession doesn't capture it again + _session = nil; + [self startSession]; + } } } @@ -715,6 +736,18 @@ - (void)reportFullyDisplayed #endif // SENTRY_HAS_UIKIT } +- (NSString *)createSessionDebugString:(SentrySession *)session +{ + if (session == nil) { + return @"Session is nil."; + } + + NSData *sessionData = [NSJSONSerialization dataWithJSONObject:[session serialize] + options:0 + error:nil]; + return [[NSString alloc] initWithData:sessionData encoding:NSUTF8StringEncoding]; +} + - (void)flush:(NSTimeInterval)timeout { [_metrics flush]; diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index daabac85f45..91400401954 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -384,9 +384,7 @@ + (void)captureEnvelope:(SentryEnvelope *)envelope */ + (void)storeEnvelope:(SentryEnvelope *)envelope { - if (nil != [SentrySDK.currentHub getClient]) { - [[SentrySDK.currentHub getClient] storeEnvelope:envelope]; - } + [SentrySDK.currentHub storeEnvelope:envelope]; } + (void)captureUserFeedback:(SentryUserFeedback *)userFeedback diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index cf643e217c6..95299c307f1 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -60,6 +60,7 @@ SentryHub () withScope:(SentryScope *)scope additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems; +- (void)storeEnvelope:(SentryEnvelope *)envelope; - (void)captureEnvelope:(SentryEnvelope *)envelope; - (nullable id)getInstalledIntegration:(Class)integrationClass; diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index 946ba5733c6..ca87b4532cd 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -18,7 +18,27 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertEqual(1, client?.storedEnvelopeInvocations.count) XCTAssertEqual(envelope, client?.storedEnvelopeInvocations.first) } + + func testStoreEnvelopeWithUndhandled_MarksSessionAsCrashedAndDoesNotStartNewSession() { + let client = TestClient(options: Options()) + let hub = TestHub(client: client, andScope: nil) + SentrySDK.setCurrentHub(hub) + hub.setTestSession() + let sessionToBeCrashed = hub.session + let envelope = getUnhandledExceptionEnvelope() + PrivateSentrySDKOnly.store(envelope) + + let storedEnvelope = client?.storedEnvelopeInvocations.first + let attachedSessionData = storedEnvelope!.items.last!.data + let attachedSession = try! JSONSerialization.jsonObject(with: attachedSessionData) as! [String: Any] + + XCTAssertEqual(0, hub.startSessionInvocations) + // Assert crashed session was attached to the envelope + XCTAssertEqual(sessionToBeCrashed!.sessionId.uuidString, attachedSession["sid"] as! String) + XCTAssertEqual("crashed", attachedSession["status"] as! String) + } + func testCaptureEnvelope() { let client = TestClient(options: Options()) SentrySDK.setCurrentHub(TestHub(client: client, andScope: nil)) @@ -29,6 +49,27 @@ class PrivateSentrySDKOnlyTests: XCTestCase { XCTAssertEqual(1, client?.captureEnvelopeInvocations.count) XCTAssertEqual(envelope, client?.captureEnvelopeInvocations.first) } + + func testCaptureEnvelopeWithUndhandled_MarksSessionAsCrashedAndStartsNewSession() { + let client = TestClient(options: Options()) + let hub = TestHub(client: client, andScope: nil) + SentrySDK.setCurrentHub(hub) + hub.setTestSession() + let sessionToBeCrashed = hub.session + + let envelope = getUnhandledExceptionEnvelope() + PrivateSentrySDKOnly.capture(envelope) + + let capturedEnvelope = client?.captureEnvelopeInvocations.first + let attachedSessionData = capturedEnvelope!.items.last!.data + let attachedSession = try! JSONSerialization.jsonObject(with: attachedSessionData) as! [String: Any] + + // Assert new session was started + XCTAssertEqual(1, hub.startSessionInvocations) + // Assert crashed session was attached to the envelope + XCTAssertEqual(sessionToBeCrashed!.sessionId.uuidString, attachedSession["sid"] as! String) + XCTAssertEqual("crashed", attachedSession["status"] as! String) + } func testSetSdkName() { let originalName = PrivateSentrySDKOnly.getSdkName() @@ -343,4 +384,13 @@ class PrivateSentrySDKOnlyTests: XCTestCase { } } #endif + + func getUnhandledExceptionEnvelope() -> SentryEnvelope { + let event = Event() + event.message = SentryMessage(formatted: "Test Event with unhandled exception") + event.level = .error + event.exceptions = [TestData.exception] + event.exceptions?.first?.mechanism?.handled = false + return SentryEnvelope(event: event) + } }