diff --git a/CHANGELOG.md b/CHANGELOG.md index 04bb2c5e2e1..0e3fa3329b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Breaking Changes + +- [HTTP Client errors](https://docs.sentry.io/platforms/apple/guides/ios/configuration/http-client-errors/) now mark sessions as errored (#6633) + ## 9.0.0-alpha.1 ### Breaking Changes diff --git a/SentryTestUtils/Sources/TestClient.swift b/SentryTestUtils/Sources/TestClient.swift index bbc0b33f099..95c09e2e23a 100644 --- a/SentryTestUtils/Sources/TestClient.swift +++ b/SentryTestUtils/Sources/TestClient.swift @@ -66,47 +66,46 @@ public class TestClient: SentryClientInternal { return SentryId() } - var captureErrorInvocations = Invocations() + public var captureErrorInvocations = Invocations() public override func capture(error: Error) -> SentryId { + super.capture(error: error) + captureErrorInvocations.record(error) return SentryId() } public var captureErrorWithScopeInvocations = Invocations<(error: Error, scope: Scope)>() public override func capture(error: Error, scope: Scope) -> SentryId { + super.capture(error: error, scope: scope) + captureErrorWithScopeInvocations.record((error, scope)) return SentryId() } var captureExceptionInvocations = Invocations() public override func capture(exception: NSException) -> SentryId { + super.capture(exception: exception) + captureExceptionInvocations.record(exception) return SentryId() } public var captureExceptionWithScopeInvocations = Invocations<(exception: NSException, scope: Scope)>() public override func capture(exception: NSException, scope: Scope) -> SentryId { + super.capture(exception: exception, scope: scope) + captureExceptionWithScopeInvocations.record((exception, scope)) return SentryId() } - public var callSessionBlockWithIncrementSessionErrors = true - @_spi(Private) - public var captureErrorWithSessionInvocations = Invocations<(error: Error, session: SentrySession?, scope: Scope)>() - @_spi(Private) - public override func captureError(_ error: Error, with scope: Scope, incrementSessionErrors sessionBlock: @escaping () -> SentrySession) -> SentryId { - captureErrorWithSessionInvocations.record((error, callSessionBlockWithIncrementSessionErrors ? sessionBlock() : nil, scope)) - return SentryId() - } - - @_spi(Private) - public var captureExceptionWithSessionInvocations = Invocations<(exception: NSException, session: SentrySession?, scope: Scope)>() - @_spi(Private) - public override func capture(_ exception: NSException, with scope: Scope, incrementSessionErrors sessionBlock: @escaping () -> SentrySession) -> SentryId { - captureExceptionWithSessionInvocations.record((exception, callSessionBlockWithIncrementSessionErrors ? sessionBlock() : nil, scope)) + @_spi(Private) public var captureEventIncrementingSessionErrorCountInvocations = Invocations<(event: Event, scope: Scope)>() + @_spi(Private) public override func captureEventIncrementingSessionErrorCount(_ event: Event, with scope: Scope) -> SentryId { + super.captureEventIncrementingSessionErrorCount(event, with: scope) + + captureEventIncrementingSessionErrorCountInvocations.record((event, scope)) return SentryId() } - + public var captureFatalEventInvocations = Invocations<(event: Event, scope: Scope)>() public override func captureFatalEvent(_ event: Event, with scope: Scope) -> SentryId { captureFatalEventInvocations.record((event, scope)) diff --git a/SentryTestUtils/Sources/TestHub.swift b/SentryTestUtils/Sources/TestHub.swift index 8b2e41bc3ed..60bbfac1f9c 100644 --- a/SentryTestUtils/Sources/TestHub.swift +++ b/SentryTestUtils/Sources/TestHub.swift @@ -44,6 +44,13 @@ public class TestHub: SentryHubInternal { return event.eventId } + @_spi(Private) public var capturedErrorEvents = Invocations() + public override func captureErrorEvent(event: Event) -> SentryId { + self.capturedErrorEvents.record((event)) + + return event.eventId + } + public var capturedTransactionsWithScope = Invocations<(transaction: [String: Any], scope: Scope)>() public override func capture(_ transaction: Transaction, with scope: Scope) { capturedTransactionsWithScope.record((transaction.serialize(), scope)) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index c9a214566d9..d47ff59ab65 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -158,22 +158,7 @@ - (SentryId *)captureException:(NSException *)exception - (SentryId *)captureException:(NSException *)exception withScope:(SentryScope *)scope { SentryEvent *event = [self buildExceptionEvent:exception]; - return [self sendEvent:event withScope:scope alwaysAttachStacktrace:YES]; -} - -- (SentryId *)captureException:(NSException *)exception - withScope:(SentryScope *)scope - incrementSessionErrors:(SentrySession * (^)(void))sessionBlock -{ - SentryEvent *event = [self buildExceptionEvent:exception]; - event = [self prepareEvent:event withScope:scope alwaysAttachStacktrace:YES]; - - if (event != nil) { - SentrySession *session = sessionBlock(); - return [self sendEvent:event withSession:session withScope:scope]; - } - - return SentryId.empty; + return [self captureEventIncrementingSessionErrorCount:event withScope:scope]; } - (SentryEvent *)buildExceptionEvent:(NSException *)exception @@ -204,22 +189,7 @@ - (SentryId *)captureError:(NSError *)error - (SentryId *)captureError:(NSError *)error withScope:(SentryScope *)scope { SentryEvent *event = [self buildErrorEvent:error]; - return [self sendEvent:event withScope:scope alwaysAttachStacktrace:YES]; -} - -- (SentryId *)captureError:(NSError *)error - withScope:(SentryScope *)scope - incrementSessionErrors:(SentrySession * (^)(void))sessionBlock -{ - SentryEvent *event = [self buildErrorEvent:error]; - event = [self prepareEvent:event withScope:scope alwaysAttachStacktrace:YES]; - - if (event != nil) { - SentrySession *session = sessionBlock(); - return [self sendEvent:event withSession:session withScope:scope]; - } - - return SentryId.empty; + return [self captureEventIncrementingSessionErrorCount:event withScope:scope]; } - (SentryEvent *)buildErrorEvent:(NSError *)error @@ -368,6 +338,26 @@ - (SentryId *)captureEvent:(SentryEvent *)event additionalEnvelopeItems:additionalEnvelopeItems]; } +- (SentryId *)captureEventIncrementingSessionErrorCount:(SentryEvent *)event + withScope:(SentryScope *)scope +{ + SentryEvent *preparedEvent = [self prepareEvent:event + withScope:scope + alwaysAttachStacktrace:YES]; + + if (preparedEvent != nil) { + SentrySession *session = nil; + id delegate = self.sessionDelegate; + if (delegate != nil) { + session = [delegate incrementSessionErrors]; + } + + return [self sendEvent:preparedEvent withSession:session withScope:scope]; + } + + return SentryId.empty; +} + - (SentryId *)sendEvent:(SentryEvent *)event withScope:(SentryScope *)scope alwaysAttachStacktrace:(BOOL)alwaysAttachStacktrace @@ -445,7 +435,7 @@ - (SentryId *)sendEvent:(SentryEvent *)event } - (SentryId *)sendEvent:(SentryEvent *)event - withSession:(SentrySession *)session + withSession:(nullable SentrySession *)session withScope:(SentryScope *)scope { if (event == nil) { @@ -463,7 +453,14 @@ - (SentryId *)sendEvent:(SentryEvent *)event SentryTraceContext *traceContext = [self getTraceStateWithEvent:event withScope:scope]; - if (nil == session.releaseName || [session.releaseName length] == 0) { + if (session == nil) { + [self.transportAdapter sendEvent:event traceContext:traceContext attachments:attachments]; + return event.eventId; + } + + SentrySession *nonnullSession = SENTRY_UNWRAP_NULLABLE(SentrySession, session); + + if (nonnullSession.releaseName == nil || [nonnullSession.releaseName length] == 0) { SENTRY_LOG_DEBUG(DropSessionLogMessage); [self.transportAdapter sendEvent:event traceContext:traceContext attachments:attachments]; @@ -471,7 +468,7 @@ - (SentryId *)sendEvent:(SentryEvent *)event } [self.transportAdapter sendEvent:event - withSession:session + withSession:nonnullSession traceContext:traceContext attachments:attachments]; diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index 7969ffc383a..e30e53cbccd 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface SentryHubInternal () +@interface SentryHubInternal () @property (nullable, atomic, strong) SentryClientInternal *client; @property (nullable, nonatomic, strong) SentryScope *scope; @@ -69,6 +69,10 @@ - (instancetype)initWithClient:(nullable SentryClientInternal *)client _installedIntegrationNames = [[NSMutableSet alloc] init]; _errorsBeforeSession = 0; + if (_client != nil) { + _client.sessionDelegate = self; + } + if (_scope) { [_crashWrapper enrichScope:SENTRY_UNWRAP_NULLABLE(SentryScope, _scope)]; } @@ -219,6 +223,8 @@ - (nullable SentrySession *)incrementSessionErrors [_session incrementErrors]; [self storeCurrentSession:SENTRY_UNWRAP_NULLABLE(SentrySession, _session)]; sessionCopy = [_session copy]; + } else { + _errorsBeforeSession++; } } @@ -492,17 +498,10 @@ - (SentryId *)captureError:(NSError *)error - (SentryId *)captureError:(NSError *)error withScope:(SentryScope *)scope { - SentrySession *currentSession = _session; SentryClientInternal *client = self.client; + if (client != nil) { - if (currentSession != nil) { - return [client captureError:error - withScope:scope - incrementSessionErrors:^(void) { return [self incrementSessionErrors]; }]; - } else { - _errorsBeforeSession++; - return [client captureError:error withScope:scope]; - } + return [client captureError:error withScope:scope]; } return SentryId.empty; } @@ -514,17 +513,21 @@ - (SentryId *)captureException:(NSException *)exception - (SentryId *)captureException:(NSException *)exception withScope:(SentryScope *)scope { - SentrySession *currentSession = _session; SentryClientInternal *client = self.client; + if (client != nil) { - if (currentSession != nil) { - return [client captureException:exception - withScope:scope - incrementSessionErrors:^(void) { return [self incrementSessionErrors]; }]; - } else { - _errorsBeforeSession++; - return [client captureException:exception withScope:scope]; - } + return [client captureException:exception withScope:scope]; + } + return SentryId.empty; +} + +- (SentryId *)captureErrorEvent:(SentryEvent *)event +{ + SentryScope *scope = self.scope; + SentryClientInternal *client = self.client; + + if (client != nil) { + return [client captureEventIncrementingSessionErrorCount:event withScope:scope]; } return SentryId.empty; } @@ -575,7 +578,13 @@ - (nullable SentryClientInternal *)getClient - (void)bindClient:(nullable SentryClientInternal *)client { + self.client.sessionDelegate = nil; + self.client = client; + + if (client != nil) { + client.sessionDelegate = self; + } } - (SentryScope *)scope diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index 4c23f964428..d71768c64ba 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -393,7 +393,7 @@ - (void)captureFailedRequests:(NSURLSessionTask *)sessionTask event.context = context; - [SentrySDK captureEvent:event]; + [SentrySDKInternal.currentHub captureErrorEvent:event]; } - (BOOL)containsStatusCode:(NSInteger)statusCode diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index 576cb73bfbc..a2676f57b47 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -12,6 +12,12 @@ @class SentrySession; @class SentryDefaultThreadInspector; +@protocol SentrySessionDelegate + +- (nullable SentrySession *)incrementSessionErrors; + +@end + NS_ASSUME_NONNULL_BEGIN @protocol SentryClientAttachmentProcessor @@ -27,14 +33,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray> *attachmentProcessors; @property (nonatomic, strong) SentryDefaultThreadInspector *threadInspector; @property (nonatomic, strong) SentryFileManager *fileManager; - -- (SentryId *)captureError:(NSError *)error - withScope:(SentryScope *)scope - incrementSessionErrors:(SentrySession * (^)(void))sessionBlock; - -- (SentryId *)captureException:(NSException *)exception - withScope:(SentryScope *)scope - incrementSessionErrors:(SentrySession * (^)(void))sessionBlock; +@property (nonatomic, weak, nullable) id sessionDelegate; - (SentryId *)captureFatalEvent:(SentryEvent *)event withScope:(SentryScope *)scope; @@ -56,6 +55,9 @@ NS_ASSUME_NONNULL_BEGIN additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:)); +- (SentryId *)captureEventIncrementingSessionErrorCount:(SentryEvent *)event + withScope:(SentryScope *)scope; + - (void)captureReplayEvent:(SentryReplayEvent *)replayEvent replayRecording:(SentryReplayRecording *)replayRecording video:(NSURL *)videoURL diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index beb8ce15d38..0d5db9990e7 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -66,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:)); +- (SentryId *)captureErrorEvent:(SentryEvent *)event NS_SWIFT_NAME(captureErrorEvent(event:)); + - (void)captureSerializedFeedback:(NSDictionary *)serializedFeedback withEventId:(NSString *)feedbackEventId attachments:(NSArray *)feedbackAttachments; diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index d970f5cf301..b17821c9dbb 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -1013,11 +1013,9 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTask(task, setState: .completed) - guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { - XCTFail("Expected to capture 1 event") - return - } - let sentryRequest = try XCTUnwrap(envelope.event.request) + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + let capturedErrorEvent = try XCTUnwrap(self.fixture.hub.capturedErrorEvents.first) + let sentryRequest = try XCTUnwrap(capturedErrorEvent.request) XCTAssertEqual(sentryRequest.url, "https://www.domain.com/api") XCTAssertEqual(sentryRequest.method, "GET") @@ -1050,13 +1048,11 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTask(task, setState: .completed) - let envelope = try XCTUnwrap( - fixture.hub.capturedEventsWithScopes.first, - "Expected to capture 1 event" - ) - + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + let capturedErrorEvent = try XCTUnwrap(fixture.hub.capturedErrorEvents.first) + let graphQLContext = try XCTUnwrap( - envelope.event.context?["graphql"], + capturedErrorEvent.context?["graphql"], "Expected 'graphql' object in context" ) @@ -1080,11 +1076,10 @@ class SentryNetworkTrackerTests: XCTestCase { task.setResponse(try createResponse(code: 500)) sut.urlSessionTask(task, setState: .completed) - guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { - XCTFail("Expected to capture 1 event") - return - } - let sentryRequest = try XCTUnwrap(envelope.event.request) + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + let capturedErrorEvent = try XCTUnwrap(self.fixture.hub.capturedErrorEvents.first) + + let sentryRequest = try XCTUnwrap(capturedErrorEvent.request) XCTAssertEqual(sentryRequest.url, "https://[Filtered]:[Filtered]@www.domain.com/api") XCTAssertEqual(sentryRequest.headers, ["VALID_HEADER": "value"]) @@ -1104,11 +1099,11 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTask(task, setState: .completed) - guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { - XCTFail("Expected to capture 1 event") - return - } - let sentryResponse = try XCTUnwrap(envelope.event.context?["response"]) + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + + let capturedErrorEvent = try XCTUnwrap(self.fixture.hub.capturedErrorEvents.first) + + let sentryResponse = try XCTUnwrap(capturedErrorEvent.context?["response"]) XCTAssertEqual(sentryResponse["status_code"] as? NSNumber, 500) XCTAssertEqual(sentryResponse["headers"] as? [String: String], ["test": "test"]) @@ -1129,11 +1124,10 @@ class SentryNetworkTrackerTests: XCTestCase { task.setResponse(response) sut.urlSessionTask(task, setState: .completed) - guard let envelope = self.fixture.hub.capturedEventsWithScopes.first else { - XCTFail("Expected to capture 1 event") - return - } - let sentryResponse = try XCTUnwrap(envelope.event.context?["response"]) + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + let capturedErrorEvent = try XCTUnwrap(self.fixture.hub.capturedErrorEvents.first) + + let sentryResponse = try XCTUnwrap(capturedErrorEvent.context?["response"]) XCTAssertEqual(sentryResponse["headers"] as? [String: String], ["VALID_HEADER": "value"]) } @@ -1145,9 +1139,10 @@ class SentryNetworkTrackerTests: XCTestCase { sut.urlSessionTask(task, setState: .completed) - let envelope = try XCTUnwrap(self.fixture.hub.capturedEventsWithScopes.first) - - let exceptions = try XCTUnwrap(envelope.event.exceptions) + XCTAssertEqual(self.fixture.hub.capturedErrorEvents.count, 1, "Expected only one error event to be captured") + let capturedErrorEvent = try XCTUnwrap(self.fixture.hub.capturedErrorEvents.first) + + let exceptions = try XCTUnwrap(capturedErrorEvent.exceptions) XCTAssertEqual(exceptions.count, 1) let exception = try XCTUnwrap(exceptions.first) diff --git a/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift b/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift index ee0ed31e4e4..263d6f42714 100644 --- a/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Session/SentrySessionTrackerTests.swift @@ -815,25 +815,21 @@ class SentrySessionTrackerTests: XCTestCase { private func assertNoInitSessionSent(file: StaticString = #file, line: UInt = #line) { let eventWithSessions = fixture.client.captureFatalEventWithSessionInvocations.invocations.map({ triple in triple.session }) - let errorWithSessions = fixture.client.captureErrorWithSessionInvocations.invocations.map({ triple in triple.session }) - let exceptionWithSessions = fixture.client.captureExceptionWithSessionInvocations.invocations.map({ triple in triple.session }) - var sessions = fixture.client.captureSessionInvocations.invocations + eventWithSessions + errorWithSessions + exceptionWithSessions + var sessions = fixture.client.captureSessionInvocations.invocations + eventWithSessions - sessions.sort { first, second in return first!.started < second!.started } + sessions.sort { first, second in return first.started < second.started } if let session = sessions.last { - XCTAssertFalse(session?.flagInit?.boolValue ?? false, file: file, line: line) + XCTAssertFalse(session.flagInit?.boolValue ?? false, file: file, line: line) } } private func assertSessionsSent(count: Int, file: StaticString = #file, line: UInt = #line) { let eventWithSessions = fixture.client.captureFatalEventWithSessionInvocations.count - let errorWithSessions = fixture.client.captureErrorWithSessionInvocations.count - let exceptionWithSessions = fixture.client.captureExceptionWithSessionInvocations.count let sessions = fixture.client.captureSessionInvocations.count - let sessionsSent = eventWithSessions + errorWithSessions + exceptionWithSessions + sessions + let sessionsSent = eventWithSessions + sessions XCTAssertEqual(count, sessionsSent, file: file, line: line) } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index d7bd84ae2e1..5b380995742 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -476,9 +476,9 @@ class SentryClientTests: XCTestCase { } sut.addAttachmentProcessor(processor) - sut.captureError(error, with: Scope()) { - self.fixture.session - } + let sessionDelegate = SentryTestSessionDelegate { self.fixture.session } + sut.sessionDelegate = sessionDelegate + sut.capture(error: error, scope: Scope()) let sentAttachments = fixture.transportAdapter.sentEventsWithSessionTraceState.first?.attachments ?? [] @@ -498,10 +498,12 @@ class SentryClientTests: XCTestCase { } sut.addAttachmentProcessor(processor) - sut.captureError(error, with: Scope()) { - return SentrySession(releaseName: "", distinctId: "some-id") + let sessionDelegate = SentryTestSessionDelegate { + SentrySession(releaseName: "", distinctId: "some-id") } - + sut.sessionDelegate = sessionDelegate + sut.capture(error: error, scope: Scope()) + let sentAttachments = fixture.transportAdapter.sendEventWithTraceStateInvocations.first?.attachments ?? [] XCTAssertEqual(sentAttachments.count, 1) @@ -802,10 +804,13 @@ class SentryClientTests: XCTestCase { func testCaptureErrorWithSession() throws { let sessionBlockExpectation = expectation(description: "session block gets called") let scope = Scope() - let eventId = fixture.getSut().captureError(error, with: scope) { + let sut = fixture.getSut() + let sessionDelegate = SentryTestSessionDelegate { sessionBlockExpectation.fulfill() return self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(error: error, scope: scope) wait(for: [sessionBlockExpectation], timeout: 0.2) eventId.assertIsNotEmpty() @@ -819,18 +824,43 @@ class SentryClientTests: XCTestCase { expectedTraceContext.traceId) } } - + + func testCaptureErrorWithOutSession() throws { + let sessionBlockExpectation = expectation(description: "session block gets called") + let scope = Scope() + let sut = fixture.getSut() + let sessionDelegate = SentryTestSessionDelegate { + sessionBlockExpectation.fulfill() + return nil + } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(error: error, scope: scope) + wait(for: [sessionBlockExpectation], timeout: 0.2) + + eventId.assertIsNotEmpty() + let eventWithSessionArguments = try XCTUnwrap(fixture.transportAdapter.sendEventWithTraceStateInvocations.last) + + try assertValidErrorEvent(eventWithSessionArguments.event, error) + + let expectedTraceContext = TraceContext(trace: scope.propagationContext.traceId, options: Options(), replayId: nil) + XCTAssertEqual(eventWithSessionArguments.traceContext?.traceId, + expectedTraceContext.traceId) + } + func testCaptureErrorWithSession_WithBeforeSendReturnsNil() throws { let sessionBlockExpectation = expectation(description: "session block does not get called") sessionBlockExpectation.isInverted = true - let eventId = fixture.getSut(configureOptions: { options in + let sut = fixture.getSut(configureOptions: { options in options.beforeSend = { _ in return nil } - }).captureError(error, with: Scope()) { + }) + let sessionDelegate = SentryTestSessionDelegate { // This should NOT be called sessionBlockExpectation.fulfill() return self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(error: error, scope: Scope()) wait(for: [sessionBlockExpectation], timeout: 0.2) eventId.assertIsEmpty() @@ -1148,10 +1178,11 @@ class SentryClientTests: XCTestCase { assertValidExceptionEvent(actual) } - func testCaptureExceptionWithSession() { - let eventId = fixture.getSut().capture(exception, with: fixture.scope) { - self.fixture.session - } + func testCaptureException_IncreasesSessionErrors() { + let sut = fixture.getSut() + let sessionDelegate = SentryTestSessionDelegate { self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(exception: exception, scope: fixture.scope) eventId.assertIsNotEmpty() XCTAssertNotNil(fixture.transportAdapter.sentEventsWithSessionTraceState.last) @@ -1162,17 +1193,20 @@ class SentryClientTests: XCTestCase { } } - func testCaptureExceptionWithSession_WithBeforeSendReturnsNil() throws { + func testCaptureException_WithBeforeSendReturnsNil() throws { let sessionBlockExpectation = expectation(description: "session block does not get called") sessionBlockExpectation.isInverted = true - let eventId = fixture.getSut(configureOptions: { options in + let sut = fixture.getSut(configureOptions: { options in options.beforeSend = { _ in return nil } - }).capture(exception, with: fixture.scope) { + }) + let sessionDelegate = SentryTestSessionDelegate { // This should NOT be called sessionBlockExpectation.fulfill() return self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(exception: exception, scope: fixture.scope) wait(for: [sessionBlockExpectation], timeout: 0.2) eventId.assertIsEmpty() @@ -1210,11 +1244,13 @@ class SentryClientTests: XCTestCase { let session = SentrySession(releaseName: "", distinctId: "some-id") fixture.getSut().capture(session: session) - fixture.getSut().capture(exception, with: Scope()) { - session - } + let sut = fixture.getSut() + let sessionDelegate = SentryTestSessionDelegate { session } + sut.sessionDelegate = sessionDelegate + + sut.capture(exception: exception, scope: Scope()) .assertIsNotEmpty() - fixture.getSut().captureFatalEvent(fixture.event, with: session, with: Scope()) + sut.captureFatalEvent(fixture.event, with: session, with: Scope()) .assertIsNotEmpty() // No sessions sent @@ -1378,9 +1414,12 @@ class SentryClientTests: XCTestCase { func testNoDsn_EventWithSessionsNotSent() { _ = SentryEnvelope(event: Event()) - let eventId = fixture.getSut(configureOptions: { options in + let sut = fixture.getSut(configureOptions: { options in options.dsn = nil - }).captureFatalEvent(Event(), with: fixture.session, with: fixture.scope) + }) + let sessionDelegate = SentryTestSessionDelegate { self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.captureFatalEvent(fixture.event, with: fixture.session, with: fixture.scope) eventId.assertIsEmpty() assertNothingSent() @@ -1388,11 +1427,12 @@ class SentryClientTests: XCTestCase { func testNoDsn_ExceptionWithSessionsNotSent() { _ = SentryEnvelope(event: Event()) - let eventId = fixture.getSut(configureOptions: { options in + let sut = fixture.getSut(configureOptions: { options in options.dsn = nil - }).capture(self.exception, with: fixture.scope) { - self.fixture.session - } + }) + let sessionDelegate = SentryTestSessionDelegate { self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(exception: self.exception, scope: fixture.scope) eventId.assertIsEmpty() assertNothingSent() @@ -1400,11 +1440,12 @@ class SentryClientTests: XCTestCase { func testNoDsn_ErrorWithSessionsNotSent() { _ = SentryEnvelope(event: Event()) - let eventId = fixture.getSut(configureOptions: { options in + let sut = fixture.getSut(configureOptions: { options in options.dsn = nil - }).captureError(self.error, with: fixture.scope) { - self.fixture.session - } + }) + let sessionDelegate = SentryTestSessionDelegate { self.fixture.session } + sut.sessionDelegate = sessionDelegate + let eventId = sut.capture(error: self.error, scope: fixture.scope) eventId.assertIsEmpty() assertNothingSent() @@ -2260,6 +2301,19 @@ class SentryClientTests: XCTestCase { } private extension SentryClientTests { + + final class SentryTestSessionDelegate: NSObject, SentrySessionDelegate { + private let handler: () -> SentrySession? + + init(handler: @escaping () -> SentrySession?) { + self.handler = handler + } + + func incrementSessionErrors() -> SentrySession? { + handler() + } + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index db62f49ab56..f521ca61973 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -27,7 +27,7 @@ class SentryHubTests: XCTestCase { let random = TestRandom(value: 0.5) let queue = DispatchQueue(label: "SentryHubTests", qos: .utility, attributes: [.concurrent]) let dispatchQueueWrapper = TestSentryDispatchQueueWrapper() - + init() { options = Options() options.dsn = SentryHubTests.dsnAsString @@ -755,24 +755,28 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(fixture.scope, errorArguments.scope) } } - - func testCaptureErrorWithSessionWithScope() { + + func testCaptureErrorWithSessionWithScope() throws { + // Arrange let sut = fixture.getSut() sut.startSession() + + // Act sut.capture(error: fixture.error, scope: fixture.scope).assertIsNotEmpty() - - XCTAssertEqual(1, fixture.client.captureErrorWithSessionInvocations.count) - if let errorArguments = fixture.client.captureErrorWithSessionInvocations.first { + + // Assert + XCTAssertEqual(1, fixture.client.captureErrorWithScopeInvocations.count) + if let errorArguments = fixture.client.captureErrorWithScopeInvocations.first { XCTAssertEqual(fixture.error, errorArguments.error as NSError) - - XCTAssertEqual(1, errorArguments.session?.errors) - XCTAssertEqual(SentrySessionStatus.ok, errorArguments.session?.status) - XCTAssertEqual(fixture.scope, errorArguments.scope) } // only session init is sent - XCTAssertEqual(1, fixture.client.captureSessionInvocations.count) + XCTAssertEqual(fixture.client.captureSessionInvocations.count, 1) + + let actualSession = try XCTUnwrap(sut.session) + XCTAssertEqual(actualSession.errors, 1) + XCTAssertEqual(actualSession.status, SentrySessionStatus.ok) } func testCaptureErrorBeforeSessionStart() { @@ -827,23 +831,6 @@ class SentryHubTests: XCTestCase { } } - func testCaptureWithoutIncreasingErrorCount() { - let sut = fixture.getSut() - sut.startSession() - fixture.client.callSessionBlockWithIncrementSessionErrors = false - sut.capture(error: fixture.error, scope: fixture.scope).assertIsNotEmpty() - - XCTAssertEqual(1, fixture.client.captureErrorWithSessionInvocations.count) - if let errorArguments = fixture.client.captureErrorWithSessionInvocations.first { - XCTAssertEqual(fixture.error, errorArguments.error as NSError) - XCTAssertNil(errorArguments.session) - XCTAssertEqual(fixture.scope, errorArguments.scope) - } - - // only session init is sent - XCTAssertEqual(1, fixture.client.captureSessionInvocations.count) - } - func testCaptureErrorWithoutScope() { fixture.getSut(fixture.options, fixture.scope).capture(error: fixture.error).assertIsNotEmpty() @@ -873,77 +860,81 @@ class SentryHubTests: XCTestCase { XCTAssertEqual(fixture.scope, errorArguments.scope) } } - - func testCaptureExceptionWithSessionWithScope() { + + func testCaptureMultipleExceptionsInParallel_IncrementsSessionCount() throws { + // Arrange + let captureCount = 100 + let sut = fixture.getSut() + sut.startSession() - sut.capture(exception: fixture.exception, scope: fixture.scope).assertIsNotEmpty() - - XCTAssertEqual(1, fixture.client.captureExceptionWithSessionInvocations.count) - if let exceptionArguments = fixture.client.captureExceptionWithSessionInvocations.first { - XCTAssertEqual(fixture.exception, exceptionArguments.exception) - - XCTAssertEqual(1, exceptionArguments.session?.errors) - XCTAssertEqual(SentrySessionStatus.ok, exceptionArguments.session?.status) - - XCTAssertEqual(fixture.scope, exceptionArguments.scope) + + let expectation = XCTestExpectation(description: "Capture error") + expectation.expectedFulfillmentCount = captureCount + expectation.assertForOverFulfill = true + + // Act + for _ in 0.. Void) { - let sut = fixture.getSut() - sut.startSession() - - let queue = fixture.queue - - let expectation = XCTestExpectation(description: "Capture should be called \(count) times") - expectation.expectedFulfillmentCount = count - - for _ in 0..