diff --git a/CHANGELOG.md b/CHANGELOG.md index 731a2196832..493d4872d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Enrich error events with any underlying NSErrors reported by Cocoa APIs (#3230) + ### Fixes - Missing `mechanism.handled` is not considered crash (#3353) diff --git a/Sources/Sentry/Public/SentryEvent.h b/Sources/Sentry/Public/SentryEvent.h index 56d2e00a8dd..15f49eb380b 100644 --- a/Sources/Sentry/Public/SentryEvent.h +++ b/Sources/Sentry/Public/SentryEvent.h @@ -24,7 +24,7 @@ NS_SWIFT_NAME(Event) /** * The error of the event. This property adds convenience to access the error directly in * @c beforeSend. This property is not serialized. Instead when preparing the event the - * @c SentryClient puts the error into exceptions. + * @c SentryClient puts the error and any underlying errors into exceptions. */ @property (nonatomic, copy) NSError *_Nullable error; @@ -138,8 +138,8 @@ NS_SWIFT_NAME(Event) @property (nonatomic, strong) NSArray *_Nullable threads; /** - * General information about the @c SentryException, usually there is only one - * exception in the array. + * General information about the @c SentryException. Multiple exceptions indicate a chain of + * exceptions encountered, starting with the oldest at the beginning of the array. */ @property (nonatomic, strong) NSArray *_Nullable exceptions; diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index ef9bd6028cb..b9a1f30636b 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -236,6 +236,34 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error { SentryEvent *event = [[SentryEvent alloc] initWithError:error]; + // flatten any recursive description of underlying errors into a list, to ultimately report them + // as a list of exceptions with error mechanisms, sorted oldest to newest (so, the leaf node + // underlying error as oldest, with the root as the newest) + NSMutableArray *errors = [NSMutableArray arrayWithObject:error]; + NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey]; + while (underlyingError != nil) { + [errors addObject:underlyingError]; + underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; + } + + NSMutableArray *exceptions = [NSMutableArray array]; + [errors enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(NSError *_Nonnull nextError, NSUInteger __unused idx, + BOOL *_Nonnull __unused stop) { + [exceptions addObject:[self exceptionForError:nextError]]; + }]; + + event.exceptions = exceptions; + + // Once the UI displays the mechanism data we can the userInfo from the event.context using only + // the root error's userInfo. + [self setUserInfo:[error.userInfo sentry_sanitize] withEvent:event]; + + return event; +} + +- (SentryException *)exceptionForError:(NSError *)error +{ NSString *exceptionValue; // If the error has a debug description, use that. @@ -274,12 +302,8 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error NSDictionary *userInfo = [error.userInfo sentry_sanitize]; mechanism.data = userInfo; exception.mechanism = mechanism; - event.exceptions = @[ exception ]; - // Once the UI displays the mechanism data we can the userInfo from the event.context. - [self setUserInfo:userInfo withEvent:event]; - - return event; + return exception; } - (SentryId *)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope diff --git a/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift b/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift index 947ec89f4e0..54a2d9fea0c 100644 --- a/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift +++ b/Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift @@ -1,3 +1,4 @@ +import SentryTestUtils import XCTest class SentryMechanismMetaTests: XCTestCase { @@ -19,8 +20,8 @@ class SentryMechanismMetaTests: XCTestCase { return } let nsError = expected.error! as SentryNSError - XCTAssertEqual(nsError.domain, error["domain"] as? String) - XCTAssertEqual(nsError.code, error["code"] as? Int) + XCTAssertEqual(Dynamic(nsError).domain, error["domain"] as? String) + XCTAssertEqual(Dynamic(nsError).code, error["code"] as? Int) guard let signal = actual["signal"] as? [String: Any] else { XCTFail("The serialization doesn't contain signal") diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index b12838912cd..ea6718adcc4 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -159,11 +159,11 @@ class SentryClientTest: XCTestCase { XCTAssertTrue(fixture.getSut().isEnabled) } - func testCaptureMessage() { + func testCaptureMessage() throws { let eventId = fixture.getSut().capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(SentryLevel.info, actual.level) XCTAssertEqual(fixture.message, actual.message) @@ -172,13 +172,13 @@ class SentryClientTest: XCTestCase { } } - func testCaptureMessageWithoutStacktrace() { + func testCaptureMessageWithoutStacktrace() throws { let eventId = fixture.getSut(configureOptions: { options in options.attachStacktrace = false }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(SentryLevel.info, actual.level) XCTAssertEqual(fixture.message, actual.message) XCTAssertNil(actual.debugMeta) @@ -187,7 +187,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEvent() { + func testCaptureEvent() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message let scope = Scope() @@ -197,7 +197,7 @@ class SentryClientTest: XCTestCase { let eventId = fixture.getSut().capture(event: event, scope: scope) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(event.level, actual.level) XCTAssertEqual(event.message, actual.message) XCTAssertNotNil(actual.debugMeta) @@ -210,7 +210,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEventWithScope_SerializedTagsAndExtraShouldMatch() { + func testCaptureEventWithScope_SerializedTagsAndExtraShouldMatch() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message let scope = Scope() @@ -222,7 +222,7 @@ class SentryClientTest: XCTestCase { let eventId = fixture.getSut().capture(event: event, scope: scope) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let serializedEvent = actual.serialize() let tags = try! XCTUnwrap(serializedEvent["tags"] as? [String: String]) let extra = try! XCTUnwrap(serializedEvent["extra"] as? [String: String]) @@ -231,7 +231,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEventTypeTransactionDoesNotIncludeThreadAndDebugMeta() { + func testCaptureEventTypeTransactionDoesNotIncludeThreadAndDebugMeta() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message event.type = SentryEnvelopeItemTypeTransaction @@ -242,7 +242,7 @@ class SentryClientTest: XCTestCase { let eventId = fixture.getSut().capture(event: event, scope: scope) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(event.level, actual.level) XCTAssertEqual(event.message, actual.message) XCTAssertNil(actual.debugMeta) @@ -255,13 +255,13 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEventWithException() { + func testCaptureEventWithException() throws { let event = Event() event.exceptions = [ Exception(value: "", type: "")] fixture.getSut().capture(event: event, scope: fixture.scope) - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in assertValidDebugMeta(actual: actual.debugMeta, forThreads: event.threads) assertValidThreads(actual: actual.threads) } @@ -358,7 +358,7 @@ class SentryClientTest: XCTestCase { eventId.assertIsNotEmpty() } - func testCaptureEventWithDebugMeta_KeepsDebugMeta() { + func testCaptureEventWithDebugMeta_KeepsDebugMeta() throws { let sut = fixture.getSut(configureOptions: { options in options.attachStacktrace = true }) @@ -366,13 +366,13 @@ class SentryClientTest: XCTestCase { let event = givenEventWithDebugMeta() sut.capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(event.debugMeta, actual.debugMeta) assertValidThreads(actual: actual.threads) } } - func testCaptureEventWithAttachedThreads_KeepsThreads() { + func testCaptureEventWithAttachedThreads_KeepsThreads() throws { let sut = fixture.getSut(configureOptions: { options in options.attachStacktrace = true }) @@ -380,13 +380,13 @@ class SentryClientTest: XCTestCase { let event = givenEventWithThreads() sut.capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in assertValidDebugMeta(actual: actual.debugMeta, forThreads: event.threads) XCTAssertEqual(event.threads, actual.threads) } } - func testCaptureEventWithAttachStacktrace() { + func testCaptureEventWithAttachStacktrace() throws { let event = Event(level: SentryLevel.fatal) event.message = fixture.message let eventId = fixture.getSut(configureOptions: { options in @@ -394,7 +394,7 @@ class SentryClientTest: XCTestCase { }).capture(event: event) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(event.level, actual.level) XCTAssertEqual(event.message, actual.message) assertValidDebugMeta(actual: actual.debugMeta, forThreads: event.threads) @@ -402,28 +402,28 @@ class SentryClientTest: XCTestCase { } } - func testCaptureErrorWithoutAttachStacktrace() { + func testCaptureErrorWithoutAttachStacktrace() throws { let eventId = fixture.getSut(configureOptions: { options in options.attachStacktrace = false }).capture(error: error, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in - assertValidErrorEvent(actual, error) + try assertLastSentEventWithAttachment { actual in + try assertValidErrorEvent(actual, error) } } - func testCaptureErrorWithEnum() { + func testCaptureErrorWithEnum() throws { let eventId = fixture.getSut().capture(error: TestError.invalidTest) eventId.assertIsNotEmpty() let error = TestError.invalidTest as NSError - assertLastSentEvent { actual in - assertValidErrorEvent(actual, error, exceptionValue: "invalidTest (Code: 0)") + try assertLastSentEvent { actual in + try assertValidErrorEvent(actual, error, exceptionValue: "invalidTest (Code: 0)") } } - func testCaptureErrorUsesErrorDebugDescriptionWhenSet() { + func testCaptureErrorUsesErrorDebugDescriptionWhenSet() throws { let error = NSError( domain: "com.sentry", code: 999, @@ -432,17 +432,13 @@ class SentryClientTest: XCTestCase { let eventId = fixture.getSut().capture(error: error) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in - do { - let exceptions = try XCTUnwrap(actual.exceptions) - XCTAssertEqual("Custom error description (Code: 999)", try XCTUnwrap(exceptions.first).value) - } catch { - XCTFail("Exception expected but was nil") - } + try assertLastSentEvent { actual in + let exceptions = try XCTUnwrap(actual.exceptions) + XCTAssertEqual("Custom error description (Code: 999)", try XCTUnwrap(exceptions.first).value) } } - func testCaptureErrorUsesErrorCodeAsDescriptionIfNoCustomDescriptionProvided() { + func testCaptureErrorUsesErrorCodeAsDescriptionIfNoCustomDescriptionProvided() throws { let error = NSError( domain: "com.sentry", code: 999, @@ -451,7 +447,7 @@ class SentryClientTest: XCTestCase { let eventId = fixture.getSut().capture(error: error) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in do { let exceptions = try XCTUnwrap(actual.exceptions) XCTAssertEqual("Code: 999", try XCTUnwrap(exceptions.first).value) @@ -461,11 +457,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureSwiftError_UsesSwiftStringDescription() { + func testCaptureSwiftError_UsesSwiftStringDescription() throws { let eventId = fixture.getSut().capture(error: SentryClientError.someError) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in do { let exceptions = try XCTUnwrap(actual.exceptions) XCTAssertEqual("someError (Code: 1)", try XCTUnwrap(exceptions.first).value) @@ -475,11 +471,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureSwiftErrorStruct_UsesSwiftStringDescription() { + func testCaptureSwiftErrorStruct_UsesSwiftStringDescription() throws { let eventId = fixture.getSut().capture(error: XMLParsingError(line: 10, column: 12, kind: .internalError)) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in do { let exceptions = try XCTUnwrap(actual.exceptions) XCTAssertEqual("XMLParsingError(line: 10, column: 12, kind: SentryTests.XMLParsingError.ErrorKind.internalError) (Code: 1)", try XCTUnwrap(exceptions.first).value) @@ -489,11 +485,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureSwiftErrorWithData_UsesSwiftStringDescription() { + func testCaptureSwiftErrorWithData_UsesSwiftStringDescription() throws { let eventId = fixture.getSut().capture(error: SentryClientError.invalidInput("hello")) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in do { let exceptions = try XCTUnwrap(actual.exceptions) XCTAssertEqual("invalidInput(\"hello\") (Code: 0)", try XCTUnwrap(exceptions.first).value) @@ -503,11 +499,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureSwiftErrorWithDebugDescription_UsesDebugDescription() { + func testCaptureSwiftErrorWithDebugDescription_UsesDebugDescription() throws { let eventId = fixture.getSut().capture(error: SentryClientErrorWithDebugDescription.someError) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in do { let exceptions = try XCTUnwrap(actual.exceptions) XCTAssertEqual("anotherError (Code: 0)", try XCTUnwrap(exceptions.first).value) @@ -517,19 +513,34 @@ class SentryClientTest: XCTestCase { } } - func testCaptureErrorWithComplexUserInfo() { + func testCaptureErrorWithComplexUserInfo() throws { let url = URL(string: "https://github.com/getsentry")! let error = NSError(domain: "domain", code: 0, userInfo: ["url": url]) let eventId = fixture.getSut().capture(error: error, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(url.absoluteString, actual.context!["user info"]!["url"] as? String) } } + + func testCaptureErrorWithNestedUnderlyingErrors() throws { + let error = NSError(domain: "domain1", code: 100, userInfo: [ + NSUnderlyingErrorKey: NSError(domain: "domain2", code: 101, userInfo: [ + NSUnderlyingErrorKey: NSError(domain: "domain3", code: 102) + ]) + ]) + + fixture.getSut().capture(error: error) + + let lastSentEventArguments = try XCTUnwrap(fixture.transportAdapter.sendEventWithTraceStateInvocations.last) + XCTAssertEqual(try XCTUnwrap(lastSentEventArguments.event.exceptions).count, 3) + XCTAssertEqual(try XCTUnwrap(lastSentEventArguments.event.exceptions?.first?.mechanism?.meta?.error).code, 102) + XCTAssertEqual(try XCTUnwrap(lastSentEventArguments.event.exceptions?.last?.mechanism?.meta?.error).code, 100) + } - func testCaptureErrorWithSession() { + func testCaptureErrorWithSession() throws { let sessionBlockExpectation = expectation(description: "session block gets called") let scope = Scope() let eventId = fixture.getSut().captureError(error, with: scope) { @@ -541,7 +552,7 @@ class SentryClientTest: XCTestCase { eventId.assertIsNotEmpty() XCTAssertNotNil(fixture.transportAdapter.sentEventsWithSessionTraceState.last) if let eventWithSessionArguments = fixture.transportAdapter.sentEventsWithSessionTraceState.last { - assertValidErrorEvent(eventWithSessionArguments.event, error) + try assertValidErrorEvent(eventWithSessionArguments.event, error) XCTAssertEqual(fixture.session, eventWithSessionArguments.session) XCTAssertEqual(eventWithSessionArguments.traceContext?.traceId, @@ -593,24 +604,24 @@ class SentryClientTest: XCTestCase { } } - func testCaptureCrashEvent() { + func testCaptureCrashEvent() throws { let eventId = fixture.getSut().captureCrash(fixture.event, with: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { event in + try assertLastSentEventWithAttachment { event in XCTAssertEqual(fixture.event.eventId, event.eventId) XCTAssertEqual(fixture.event.message, event.message) XCTAssertEqual("value", event.tags?["key"] ?? "") } } - func testCaptureOOMEvent_RemovesMutableInfoFromDeviceContext() { + func testCaptureOOMEvent_RemovesMutableInfoFromDeviceContext() throws { let oomEvent = TestData.oomEvent _ = fixture.getSut().captureCrash(oomEvent, with: fixture.scope) - assertLastSentEventWithAttachment { event in + try assertLastSentEventWithAttachment { event in XCTAssertEqual(oomEvent.eventId, event.eventId) XCTAssertNil(event.context?["device"]?["free_memory"]) XCTAssertNil(event.context?["device"]?["free_storage"]) @@ -621,49 +632,49 @@ class SentryClientTest: XCTestCase { } } - func testCaptureOOMEvent_WithNoContext_ContextNotModified() { + func testCaptureOOMEvent_WithNoContext_ContextNotModified() throws { let oomEvent = TestData.oomEvent _ = fixture.getSut().captureCrash(oomEvent, with: Scope()) - assertLastSentEvent { event in + try assertLastSentEvent { event in XCTAssertEqual(oomEvent.eventId, event.eventId) XCTAssertEqual(oomEvent.context?.count, event.context?.count) } } - func testCaptureOOMEvent_WithNoDeviceContext_ContextNotModified() { + func testCaptureOOMEvent_WithNoDeviceContext_ContextNotModified() throws { let oomEvent = TestData.oomEvent let scope = Scope() scope.setContext(value: ["some": "thing"], key: "any") _ = fixture.getSut().captureCrash(oomEvent, with: scope) - assertLastSentEvent { event in + try assertLastSentEvent { event in XCTAssertEqual(oomEvent.eventId, event.eventId) XCTAssertEqual(oomEvent.context?.count, event.context?.count) } } - func testCaptureCrash_DoesntOverideStacktraceFor() { + func testCaptureCrash_DoesntOverideStacktraceFor() throws { let event = TestData.event event.threads = nil event.debugMeta = nil fixture.getSut().captureCrash(event, with: fixture.scope) - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertNil(actual.threads) XCTAssertNil(actual.debugMeta) } } - func testCaptureCrash_NoExtraContext() { + func testCaptureCrash_NoExtraContext() throws { let event = TestData.event fixture.getSut().captureCrash(event, with: fixture.scope) - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(1, actual.context?["device"]?.count, "The device context should only contain free_memory") let eventFreeMemory = actual.context?["device"]?[SentryDeviceContextFreeMemoryKey] as? Int @@ -674,14 +685,14 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEvent_AddCurrentMemoryStorageAndCPUCoreCount() { + func testCaptureEvent_AddCurrentMemoryStorageAndCPUCoreCount() throws { let sut = fixture.getSut() fixture.processWrapper.overrides.processorCount = 12 sut.capture(event: TestData.event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let eventFreeMemory = actual.context?["device"]?[SentryDeviceContextFreeMemoryKey] as? Int XCTAssertEqual(eventFreeMemory, 123_456) @@ -696,11 +707,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEvent_DeviceProperties() { -#if os(iOS) +#if os(iOS) || targetEnvironment(macCatalyst) + func testCaptureEvent_DeviceProperties() throws { fixture.getSut().capture(event: TestData.event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let orientation = actual.context?["device"]?["orientation"] as? String XCTAssertEqual(orientation, "portrait") @@ -710,30 +721,28 @@ class SentryClientTest: XCTestCase { let batteryLevel = actual.context?["device"]?["battery_level"] as? Int XCTAssertEqual(batteryLevel, 60) } -#endif } - func testCaptureEvent_DeviceProperties_OtherValues() { -#if os(iOS) || targetEnvironment(macCatalyst) + func testCaptureEvent_DeviceProperties_OtherValues() throws { fixture.deviceWrapper.internalOrientation = .landscapeLeft fixture.deviceWrapper.internalBatteryState = .full fixture.getSut().capture(event: TestData.event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let orientation = actual.context?["device"]?["orientation"] as? String XCTAssertEqual(orientation, "landscape") let charging = actual.context?["device"]?["charging"] as? Bool XCTAssertEqual(charging, false) } -#endif // os(iOS) || targetEnvironment(macCatalyst) } +#endif // os(iOS) || targetEnvironment(macCatalyst) - func testCaptureEvent_AddCurrentCulture() { + func testCaptureEvent_AddCurrentCulture() throws { fixture.getSut().capture(event: TestData.event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let culture = actual.context?["culture"] if #available(iOS 10, macOS 10.12, watchOS 3, tvOS 10, *) { @@ -749,13 +758,13 @@ class SentryClientTest: XCTestCase { } } - func testCaptureErrorWithUserInfo() { + func testCaptureErrorWithUserInfo() throws { let expectedValue = "val" let error = NSError(domain: "domain", code: 0, userInfo: ["key": expectedValue]) let eventId = fixture.getSut().capture(error: error, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(expectedValue, actual.context!["user info"]!["key"] as? String) } } @@ -764,7 +773,7 @@ class SentryClientTest: XCTestCase { func testCaptureExceptionWithAppStateInForegroudDoNotAddIfAppStateNil() { let event = TestData.event fixture.getSut().capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let inForeground = actual.context?["app"]?["in_foreground"] as? Bool XCTAssertEqual(inForeground, nil) } @@ -779,7 +788,7 @@ class SentryClientTest: XCTestCase { let event = TestData.event event.context?.removeValue(forKey: "app") fixture.getSut().capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let inForeground = actual.context?["app"]?["in_foreground"] as? Bool XCTAssertEqual(inForeground, true) } @@ -794,7 +803,7 @@ class SentryClientTest: XCTestCase { let event = TestData.event event.context!["app"] = [ "test": "keep-value" ] fixture.getSut().capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let inForeground = actual.context?["app"]?["in_foreground"] as? Bool XCTAssertEqual(inForeground, true) XCTAssertEqual(actual.context?["app"]?["test"] as? String, "keep-value") @@ -810,20 +819,20 @@ class SentryClientTest: XCTestCase { let event = TestData.event event.context!["app"] = [ "in_foreground": "keep-value" ] fixture.getSut().capture(event: event) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in let inForeground = actual.context?["app"]?["in_foreground"] as? String XCTAssertEqual(inForeground, "keep-value") } } #endif - func testCaptureExceptionWithoutAttachStacktrace() { + func testCaptureExceptionWithoutAttachStacktrace() throws { let eventId = fixture.getSut(configureOptions: { options in options.attachStacktrace = false }).capture(exception: exception, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in assertValidExceptionEvent(actual) } } @@ -853,22 +862,22 @@ class SentryClientTest: XCTestCase { assertLastSentEnvelopeIsASession() } - func testCaptureExceptionWithUserInfo() { + func testCaptureExceptionWithUserInfo() throws { let expectedValue = "val" let exception = NSException(name: NSExceptionName("exception"), reason: "reason", userInfo: ["key": expectedValue]) let eventId = fixture.getSut().capture(exception: exception, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(expectedValue, actual.context!["user info"]!["key"] as? String) } } - func testScopeIsNotNil() { + func testScopeIsNotNil() throws { let eventId = fixture.getSut().capture(message: fixture.messageAsString, scope: fixture.scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(fixture.environment, actual.environment) } } @@ -921,7 +930,7 @@ class SentryClientTest: XCTestCase { assertLostEventRecorded(category: .transaction, reason: .beforeSend) } - func testBeforeSendReturnsNewEvent_NewEventSent() { + func testBeforeSendReturnsNewEvent_NewEventSent() throws { let newEvent = Event() let releaseName = "1.0.0" let eventId = fixture.getSut(configureOptions: { options in @@ -932,13 +941,13 @@ class SentryClientTest: XCTestCase { }).capture(message: fixture.messageAsString) XCTAssertEqual(newEvent.eventId, eventId) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(newEvent.eventId, actual.eventId) XCTAssertNil(actual.releaseName) } } - func testBeforeSendModifiesEvent_ModifiedEventSent() { + func testBeforeSendModifiesEvent_ModifiedEventSent() throws { fixture.getSut(configureOptions: { options in options.beforeSend = { event in event.threads = [] @@ -948,7 +957,7 @@ class SentryClientTest: XCTestCase { options.attachStacktrace = true }).capture(message: fixture.messageAsString) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual([], actual.debugMeta) XCTAssertEqual([], actual.threads) } @@ -1025,23 +1034,23 @@ class SentryClientTest: XCTestCase { assertNothingSent() } - func testSampleRateNil_EventNotSampled() { - assertSampleRate(sampleRate: nil, randomValue: 0, isSampled: false) + func testSampleRateNil_EventNotSampled() throws { + try assertSampleRate(sampleRate: nil, randomValue: 0, isSampled: false) } - func testSampleRateBiggerRandom_EventNotSampled() { - assertSampleRate(sampleRate: 0.5, randomValue: 0.49, isSampled: false) + func testSampleRateBiggerRandom_EventNotSampled() throws { + try assertSampleRate(sampleRate: 0.5, randomValue: 0.49, isSampled: false) } - func testSampleRateEqualsRandom_EventNotSampled() { - assertSampleRate(sampleRate: 0.5, randomValue: 0.5, isSampled: false) + func testSampleRateEqualsRandom_EventNotSampled() throws { + try assertSampleRate(sampleRate: 0.5, randomValue: 0.5, isSampled: false) } - func testSampleRateSmallerRandom_EventSampled() { - assertSampleRate(sampleRate: 0.50, randomValue: 0.51, isSampled: true) + func testSampleRateSmallerRandom_EventSampled() throws { + try assertSampleRate(sampleRate: 0.50, randomValue: 0.51, isSampled: true) } - private func assertSampleRate( sampleRate: NSNumber?, randomValue: Double, isSampled: Bool) { + private func assertSampleRate( sampleRate: NSNumber?, randomValue: Double, isSampled: Bool) throws { fixture.random.value = randomValue let eventId = fixture.getSut(configureOptions: { options in @@ -1053,13 +1062,13 @@ class SentryClientTest: XCTestCase { assertNothingSent() } else { eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(eventId, actual.eventId) } } } - func testSampleRateDoesNotImpactTransactions() { + func testSampleRateDoesNotImpactTransactions() throws { fixture.random.value = 0.51 let eventId = fixture.getSut(configureOptions: { options in @@ -1067,7 +1076,7 @@ class SentryClientTest: XCTestCase { }).capture(event: fixture.transaction) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(eventId, actual.eventId) } } @@ -1114,40 +1123,40 @@ class SentryClientTest: XCTestCase { assertNothingSent() } - func testDistIsSet() { + func testDistIsSet() throws { let dist = "dist" let eventId = fixture.getSut(configureOptions: { options in options.dist = dist }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(dist, actual.dist) } } - func testEnvironmentDefaultToProduction() { + func testEnvironmentDefaultToProduction() throws { let eventId = fixture.getSut().capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual("production", actual.environment) } } - func testEnvironmentIsSetViaOptions() { + func testEnvironmentIsSetViaOptions() throws { let environment = "environment" let eventId = fixture.getSut(configureOptions: { options in options.environment = environment }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(environment, actual.environment) } } - func testEnvironmentIsSetInEventTakesPrecedenceOverOptions() { + func testEnvironmentIsSetInEventTakesPrecedenceOverOptions() throws { let optionsEnvironment = "environment" let event = Event() event.environment = "event" @@ -1158,12 +1167,12 @@ class SentryClientTest: XCTestCase { }).capture(event: event, scope: scope) eventId.assertIsNotEmpty() - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual("event", actual.environment) } } - func testEnvironmentIsSetInEventTakesPrecedenceOverScope() { + func testEnvironmentIsSetInEventTakesPrecedenceOverScope() throws { let optionsEnvironment = "environment" let event = Event() event.environment = "event" @@ -1172,12 +1181,12 @@ class SentryClientTest: XCTestCase { }).capture(event: event) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual("event", actual.environment) } } - func testSetSDKIntegrations() { + func testSetSDKIntegrations() throws { SentrySDK.start(options: Options()) let eventId = fixture.getSut().capture(message: fixture.messageAsString) @@ -1189,7 +1198,7 @@ class SentryClientTest: XCTestCase { expectedIntegrations = ["ANRTracking"] + expectedIntegrations } - assertLastSentEvent { actual in + try assertLastSentEvent { actual in assertArrayEquals( expected: expectedIntegrations, actual: actual.sdk?["integrations"] as? [String] @@ -1198,14 +1207,14 @@ class SentryClientTest: XCTestCase { } #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - func testTrackPreWarmedAppStartTracking() { - testFeatureTrackingAsIntegration(integrationName: "PreWarmedAppStartTracing") { + func testTrackPreWarmedAppStartTracking() throws { + try testFeatureTrackingAsIntegration(integrationName: "PreWarmedAppStartTracing") { $0.enablePreWarmedAppStartTracing = true } } #endif - private func testFeatureTrackingAsIntegration(integrationName: String, configureOptions: (Options) -> Void) { + private func testFeatureTrackingAsIntegration(integrationName: String, configureOptions: (Options) -> Void) throws { SentrySDK.start(options: Options()) let eventId = fixture.getSut(configureOptions: { options in @@ -1213,7 +1222,7 @@ class SentryClientTest: XCTestCase { }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in var expectedIntegrations = ["AutoBreadcrumbTracking", "AutoSessionTracking", "Crash", "NetworkTracking", integrationName] if !SentryDependencyContainer.sharedInstance().crashWrapper.isBeingTraced() { expectedIntegrations = ["ANRTracking"] + expectedIntegrations @@ -1226,7 +1235,7 @@ class SentryClientTest: XCTestCase { } } - func testSetSDKIntegrations_NoIntegrations() { + func testSetSDKIntegrations_NoIntegrations() throws { let expected: [String] = [] let eventId = fixture.getSut(configureOptions: { options in @@ -1234,7 +1243,7 @@ class SentryClientTest: XCTestCase { }).capture(message: fixture.messageAsString) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in assertArrayEquals(expected: expected, actual: actual.sdk?["integrations"] as? [String]) } } @@ -1251,49 +1260,49 @@ class SentryClientTest: XCTestCase { SentryFileManager.tearDownInitError() } - func testInstallationIdSetWhenNoUserId() { + func testInstallationIdSetWhenNoUserId() throws { fixture.getSut().capture(message: "any message") - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(SentryInstallation.id(), actual.user?.userId) } } - func testInstallationIdNotSetWhenUserIsSetWithoutId() { + func testInstallationIdNotSetWhenUserIsSetWithoutId() throws { let scope = fixture.scope scope.setUser(fixture.user) fixture.getSut().capture(message: "any message", scope: scope) - assertLastSentEventWithAttachment { actual in + try assertLastSentEventWithAttachment { actual in XCTAssertEqual(fixture.user.userId, actual.user?.userId) XCTAssertEqual(fixture.user.email, actual.user?.email) } } - func testInstallationIdNotSetWhenUserIsSetWithId() { + func testInstallationIdNotSetWhenUserIsSetWithId() throws { let scope = Scope() let user = fixture.user user.userId = "id" scope.setUser(user) fixture.getSut().capture(message: "any message", scope: scope) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(user.userId, actual.user?.userId) XCTAssertEqual(fixture.user.email, actual.user?.email) } } - func testSendDefaultPiiEnabled_GivenNoIP_AutoIsSet() { + func testSendDefaultPiiEnabled_GivenNoIP_AutoIsSet() throws { fixture.getSut(configureOptions: { options in options.sendDefaultPii = true }).capture(message: "any") - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual("{{auto}}", actual.user?.ipAddress) } } - func testSendDefaultPiiEnabled_GivenIP_IPAddressNotChanged() { + func testSendDefaultPiiEnabled_GivenIP_IPAddressNotChanged() throws { let scope = Scope() scope.setUser(fixture.user) @@ -1301,18 +1310,18 @@ class SentryClientTest: XCTestCase { options.sendDefaultPii = true }).capture(message: "any", scope: scope) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(fixture.user.ipAddress, actual.user?.ipAddress) } } - func testSendDefaultPiiDisabled_GivenIP_IPAddressNotChanged() { + func testSendDefaultPiiDisabled_GivenIP_IPAddressNotChanged() throws { let scope = Scope() scope.setUser(fixture.user) fixture.getSut().capture(message: "any", scope: scope) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(fixture.user.ipAddress, actual.user?.ipAddress) } } @@ -1499,17 +1508,17 @@ class SentryClientTest: XCTestCase { XCTAssertFalse(eventWasSent) } - private func assertLastSentEvent(assert: (Event) -> Void) { + private func assertLastSentEvent(assert: (Event) throws -> Void) throws { XCTAssertNotNil(fixture.transportAdapter.sendEventWithTraceStateInvocations.last) if let lastSentEventArguments = fixture.transportAdapter.sendEventWithTraceStateInvocations.last { - assert(lastSentEventArguments.event) + try assert(lastSentEventArguments.event) } } - private func assertLastSentEventWithAttachment(assert: (Event) -> Void) { + private func assertLastSentEventWithAttachment(assert: (Event) throws -> Void) throws { XCTAssertNotNil(fixture.transportAdapter.sendEventWithTraceStateInvocations.last) if let lastSentEventArguments = fixture.transportAdapter.sendEventWithTraceStateInvocations.last { - assert(lastSentEventArguments.event) + try assert(lastSentEventArguments.event) XCTAssertEqual([TestData.dataAttachment], lastSentEventArguments.attachments) } @@ -1522,29 +1531,28 @@ class SentryClientTest: XCTestCase { } } - private func assertValidErrorEvent(_ event: Event, _ error: NSError, exceptionValue: String? = nil) { + private func assertValidErrorEvent(_ event: Event, _ expectedError: NSError, exceptionValue: String? = nil) throws { XCTAssertEqual(SentryLevel.error, event.level) - XCTAssertEqual(error, event.error as NSError?) + XCTAssertEqual(expectedError, event.error as NSError?) guard let exceptions = event.exceptions else { XCTFail("Event should contain one exception"); return } XCTAssertEqual(1, exceptions.count) let exception = exceptions[0] - XCTAssertEqual(error.domain, exception.type) + XCTAssertEqual(expectedError.domain, exception.type) - XCTAssertEqual(exceptionValue ?? "Code: \(error.code)", exception.value) + XCTAssertEqual(exceptionValue ?? "Code: \(expectedError.code)", exception.value) XCTAssertNil(exception.threadId) XCTAssertNil(exception.stacktrace) - - guard let mechanism = exception.mechanism else { - XCTFail("Exception doesn't contain a mechanism"); return - } + + let mechanism = try XCTUnwrap(exception.mechanism) + let meta = try XCTUnwrap(mechanism.meta) + let actualError = try XCTUnwrap(meta.error) XCTAssertEqual("NSError", mechanism.type) - XCTAssertNotNil(mechanism.meta?.error) - XCTAssertEqual(error.domain, mechanism.meta?.error?.domain) - XCTAssertEqual(error.code, mechanism.meta?.error?.code) + XCTAssertEqual(expectedError.domain, Dynamic(actualError).domain.asString) + XCTAssertEqual(expectedError.code, Dynamic(actualError).code.asInt) assertValidDebugMeta(actual: event.debugMeta, forThreads: event.threads) assertValidThreads(actual: event.threads)