From d733858548dab6d0f957ccc817a2412c3e2cfa50 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 16 Aug 2023 18:01:24 -0800 Subject: [PATCH 1/7] feat: add underlying error info to reported NSErrors --- Sources/Sentry/Public/SentryNSError.h | 17 +--------- Sources/Sentry/SentryClient.m | 2 +- Sources/Sentry/SentryNSError.m | 47 ++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Sources/Sentry/Public/SentryNSError.h b/Sources/Sentry/Public/SentryNSError.h index a4e6fd8d53f..162d8a8d6b6 100644 --- a/Sources/Sentry/Public/SentryNSError.h +++ b/Sources/Sentry/Public/SentryNSError.h @@ -10,22 +10,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryNSError : NSObject SENTRY_NO_INIT -/** - * The domain of an @c NSError . - */ -@property (nonatomic, copy) NSString *domain; - -/** - * The error code of an @c NSError . - */ -@property (nonatomic, assign) NSInteger code; - -/** - * Initializes @c SentryNSError and sets the domain and code. - * @param domain The domain of an @c NSError. - * @param code The error code of an @c NSError. - */ -- (instancetype)initWithDomain:(NSString *)domain code:(NSInteger)code; +- (instancetype)initWithError:(NSError *)error; @end diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 8717d6a1eeb..76ce66f2a9c 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -273,7 +273,7 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error // Sentry uses the error domain and code on the mechanism for gouping SentryMechanism *mechanism = [[SentryMechanism alloc] initWithType:@"NSError"]; SentryMechanismMeta *mechanismMeta = [[SentryMechanismMeta alloc] init]; - mechanismMeta.error = [[SentryNSError alloc] initWithDomain:error.domain code:error.code]; + mechanismMeta.error = [[SentryNSError alloc] initWithError:error]; mechanism.meta = mechanismMeta; // The description of the error can be especially useful for error from swift that // use a simple enum. diff --git a/Sources/Sentry/SentryNSError.m b/Sources/Sentry/SentryNSError.m index 783dd88c0c7..eb3f7425118 100644 --- a/Sources/Sentry/SentryNSError.m +++ b/Sources/Sentry/SentryNSError.m @@ -3,20 +3,59 @@ NS_ASSUME_NONNULL_BEGIN +@interface +SentryNSError () + +@property (nonatomic, copy) NSString *domain; +@property (nonatomic, assign) NSInteger code; + +/** + * @note Can be empty, but never @c nil . + */ +@property (nonatomic, copy) NSArray *underlyingErrors; + +@end + @implementation SentryNSError -- (instancetype)initWithDomain:(NSString *)domain code:(NSInteger)code +- (instancetype)initWithError:(NSError *)error { if (self = [super init]) { - _domain = domain; - _code = code; + _domain = error.domain; + _code = error.code; + + if (@available(iOS 14.5, *)) { + NSMutableArray *underlyingErrors = + [NSMutableArray array]; + [error.underlyingErrors enumerateObjectsUsingBlock:^( + NSError *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + [underlyingErrors addObject:[[SentryNSError alloc] initWithError:obj]]; + }]; + _underlyingErrors = underlyingErrors; + } else { + NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey]; + if (underlyingError == nil) { + _underlyingErrors = @[]; + } else { + _underlyingErrors = @[ [[SentryNSError alloc] initWithError:underlyingError] ]; + } + } } return self; } - (NSDictionary *)serialize { - return @{ @"domain" : self.domain, @"code" : @(self.code) }; + NSMutableDictionary *dict = [NSMutableDictionary + dictionaryWithObjectsAndKeys:self.domain, @"domain", @(self.code), @"code", nil]; + if (self.underlyingErrors.count > 0) { + NSMutableArray *> *serializedUnderlyingErrors = + [NSMutableArray *> array]; + [_underlyingErrors enumerateObjectsUsingBlock:^(SentryNSError *_Nonnull obj, NSUInteger idx, + BOOL *_Nonnull stop) { [serializedUnderlyingErrors addObject:[obj serialize]]; }]; + dict[@"underlying_errors"] = serializedUnderlyingErrors; + } + return dict; } @end From e031af6e98d83332c7a8b1c22dbb460d4ceb71c8 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 31 Aug 2023 15:12:10 -0400 Subject: [PATCH 2/7] fix tests with some minor refactors (biggest one is throwing test cases with XCTUnwrap) --- Sources/Sentry/SentryNSError.m | 2 +- .../Protocol/SentryMechanismMetaTests.swift | 5 +- .../Protocol/SentryNSErrorTests.swift | 7 +- Tests/SentryTests/Protocol/TestData.swift | 2 +- Tests/SentryTests/SentryClientTests.swift | 277 +++++++++--------- 5 files changed, 144 insertions(+), 149 deletions(-) diff --git a/Sources/Sentry/SentryNSError.m b/Sources/Sentry/SentryNSError.m index eb3f7425118..6754d09a1ba 100644 --- a/Sources/Sentry/SentryNSError.m +++ b/Sources/Sentry/SentryNSError.m @@ -24,7 +24,7 @@ - (instancetype)initWithError:(NSError *)error _domain = error.domain; _code = error.code; - if (@available(iOS 14.5, *)) { + if (@available(iOS 14.5, tvOS 14.5, macOS 11.3, *)) { NSMutableArray *underlyingErrors = [NSMutableArray array]; [error.underlyingErrors enumerateObjectsUsingBlock:^( 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/Protocol/SentryNSErrorTests.swift b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift index 95757c8544c..3f7190d9dcd 100644 --- a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift +++ b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift @@ -1,14 +1,15 @@ +import SentryTestUtils import XCTest class SentryNSErrorTests: XCTestCase { func testSerialize() { - let error = SentryNSError(domain: "domain", code: 10) + let error = SentryNSError(error: NSError(domain: "domain", code: 10)) let actual = error.serialize() - XCTAssertEqual(error.domain, actual["domain"] as? String) - XCTAssertEqual(error.code, actual["code"] as? Int) + XCTAssertEqual(Dynamic(error).domain, actual["domain"] as? String) + XCTAssertEqual(Dynamic(error).code, actual["code"] as? Int) } func testSerializeWithUnderlyingNSError() { diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index bd7641355df..604dbd10d7b 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -128,7 +128,7 @@ class TestData { "code_name": "BUS_NOOP" ] - mechanismMeta.error = SentryNSError(domain: "SentrySampleDomain", code: 1) + mechanismMeta.error = SentryNSError(error: NSError(domain: "SentrySampleDomain", code: 1)) return mechanismMeta } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 12812647750..a85c1d95ce0 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -153,11 +153,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) @@ -166,13 +166,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) @@ -181,7 +181,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEvent() { + func testCaptureEvent() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message let scope = Scope() @@ -191,7 +191,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) @@ -204,7 +204,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEventWithScope_SerializedTagsAndExtraShouldMatch() { + func testCaptureEventWithScope_SerializedTagsAndExtraShouldMatch() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message let scope = Scope() @@ -216,7 +216,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]) @@ -225,7 +225,7 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEventTypeTransactionDoesNotIncludeThreadAndDebugMeta() { + func testCaptureEventTypeTransactionDoesNotIncludeThreadAndDebugMeta() throws { let event = Event(level: SentryLevel.warning) event.message = fixture.message event.type = SentryEnvelopeItemTypeTransaction @@ -236,7 +236,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) @@ -249,13 +249,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) } @@ -352,7 +352,7 @@ class SentryClientTest: XCTestCase { eventId.assertIsNotEmpty() } - func testCaptureEventWithDebugMeta_KeepsDebugMeta() { + func testCaptureEventWithDebugMeta_KeepsDebugMeta() throws { let sut = fixture.getSut(configureOptions: { options in options.attachStacktrace = true }) @@ -360,13 +360,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 }) @@ -374,13 +374,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 @@ -388,7 +388,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) @@ -396,28 +396,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, @@ -426,17 +426,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, @@ -445,7 +441,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) @@ -455,11 +451,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) @@ -469,11 +465,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) @@ -483,11 +479,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) @@ -497,11 +493,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) @@ -511,19 +507,19 @@ 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 testCaptureErrorWithSession() { + func testCaptureErrorWithSession() throws { let sessionBlockExpectation = expectation(description: "session block gets called") let eventId = fixture.getSut().captureError(error, with: Scope()) { sessionBlockExpectation.fulfill() @@ -534,7 +530,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) } } @@ -583,24 +579,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"]) @@ -611,49 +607,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 @@ -664,14 +660,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) @@ -686,11 +682,11 @@ class SentryClientTest: XCTestCase { } } - func testCaptureEvent_DeviceProperties() { #if os(iOS) + 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") @@ -700,30 +696,28 @@ class SentryClientTest: XCTestCase { let batteryLevel = actual.context?["device"]?["battery_level"] as? Int XCTAssertEqual(batteryLevel, 60) } -#endif } - func testCaptureEvent_DeviceProperties_OtherValues() { -#if os(iOS) + 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 } +#endif // os(iOS) - 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, *) { @@ -739,13 +733,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) } } @@ -754,7 +748,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) } @@ -769,7 +763,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) } @@ -784,7 +778,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") @@ -800,20 +794,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) } } @@ -843,22 +837,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) } } @@ -911,7 +905,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 @@ -922,13 +916,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 = [] @@ -938,7 +932,7 @@ class SentryClientTest: XCTestCase { options.attachStacktrace = true }).capture(message: fixture.messageAsString) - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual([], actual.debugMeta) XCTAssertEqual([], actual.threads) } @@ -1015,23 +1009,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 @@ -1043,13 +1037,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 @@ -1057,7 +1051,7 @@ class SentryClientTest: XCTestCase { }).capture(event: fixture.transaction) eventId.assertIsNotEmpty() - assertLastSentEvent { actual in + try assertLastSentEvent { actual in XCTAssertEqual(eventId, actual.eventId) } } @@ -1104,40 +1098,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" @@ -1148,12 +1142,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" @@ -1162,12 +1156,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) @@ -1179,7 +1173,7 @@ class SentryClientTest: XCTestCase { expectedIntegrations = ["ANRTracking"] + expectedIntegrations } - assertLastSentEvent { actual in + try assertLastSentEvent { actual in assertArrayEquals( expected: expectedIntegrations, actual: actual.sdk?["integrations"] as? [String] @@ -1188,14 +1182,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 @@ -1203,7 +1197,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 @@ -1216,7 +1210,7 @@ class SentryClientTest: XCTestCase { } } - func testSetSDKIntegrations_NoIntegrations() { + func testSetSDKIntegrations_NoIntegrations() throws { let expected: [String] = [] let eventId = fixture.getSut(configureOptions: { options in @@ -1224,7 +1218,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]) } } @@ -1241,49 +1235,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) @@ -1291,18 +1285,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) } } @@ -1489,17 +1483,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) } @@ -1512,29 +1506,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) From 255d07cde3e885082a36d88cfcf5d2d8a0d0358f Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 5 Sep 2023 15:55:17 -0400 Subject: [PATCH 3/7] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b31c1230856..1d8f3d42161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Enrich error events with any underlying NSErrors reported by Cocoa APIs (#3230) + ## 8.11.0 ### Features From 1d84addf4da07307e363466f723f090b522f710f Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Tue, 5 Sep 2023 15:57:06 -0400 Subject: [PATCH 4/7] watchOS version check --- Sources/Sentry/SentryNSError.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Sentry/SentryNSError.m b/Sources/Sentry/SentryNSError.m index 6754d09a1ba..2085747ee76 100644 --- a/Sources/Sentry/SentryNSError.m +++ b/Sources/Sentry/SentryNSError.m @@ -24,7 +24,7 @@ - (instancetype)initWithError:(NSError *)error _domain = error.domain; _code = error.code; - if (@available(iOS 14.5, tvOS 14.5, macOS 11.3, *)) { + if (@available(iOS 14.5, tvOS 14.5, macOS 11.3, watchOS 7.4, *)) { NSMutableArray *underlyingErrors = [NSMutableArray array]; [error.underlyingErrors enumerateObjectsUsingBlock:^( From c8c7ca8cebed693870dff3386037d4d7b0df8d35 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 20 Oct 2023 13:48:53 -0800 Subject: [PATCH 5/7] report underlying error chains as flattened lists of Sentry exception events in asending temporal order --- Sources/Sentry/Public/SentryEvent.h | 6 +-- Sources/Sentry/Public/SentryNSError.h | 17 ++++++- Sources/Sentry/SentryClient.m | 36 +++++++++++--- Sources/Sentry/SentryNSError.m | 47 ++----------------- .../Protocol/SentryNSErrorTests.swift | 6 +-- Tests/SentryTests/Protocol/TestData.swift | 2 +- Tests/SentryTests/SentryClientTests.swift | 15 ++++++ 7 files changed, 72 insertions(+), 57 deletions(-) diff --git a/Sources/Sentry/Public/SentryEvent.h b/Sources/Sentry/Public/SentryEvent.h index 56d2e00a8dd..1cb98b4ccb9 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 exception indicate a chain of + * exception encountered, starting with the oldest at the beginning of the array. */ @property (nonatomic, strong) NSArray *_Nullable exceptions; diff --git a/Sources/Sentry/Public/SentryNSError.h b/Sources/Sentry/Public/SentryNSError.h index 162d8a8d6b6..a4e6fd8d53f 100644 --- a/Sources/Sentry/Public/SentryNSError.h +++ b/Sources/Sentry/Public/SentryNSError.h @@ -10,7 +10,22 @@ NS_ASSUME_NONNULL_BEGIN @interface SentryNSError : NSObject SENTRY_NO_INIT -- (instancetype)initWithError:(NSError *)error; +/** + * The domain of an @c NSError . + */ +@property (nonatomic, copy) NSString *domain; + +/** + * The error code of an @c NSError . + */ +@property (nonatomic, assign) NSInteger code; + +/** + * Initializes @c SentryNSError and sets the domain and code. + * @param domain The domain of an @c NSError. + * @param code The error code of an @c NSError. + */ +- (instancetype)initWithDomain:(NSString *)domain code:(NSInteger)code; @end diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 99e2c12b992..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. @@ -265,7 +293,7 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error // Sentry uses the error domain and code on the mechanism for gouping SentryMechanism *mechanism = [[SentryMechanism alloc] initWithType:@"NSError"]; SentryMechanismMeta *mechanismMeta = [[SentryMechanismMeta alloc] init]; - mechanismMeta.error = [[SentryNSError alloc] initWithError:error]; + mechanismMeta.error = [[SentryNSError alloc] initWithDomain:error.domain code:error.code]; mechanism.meta = mechanismMeta; // The description of the error can be especially useful for error from swift that // use a simple enum. @@ -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/Sources/Sentry/SentryNSError.m b/Sources/Sentry/SentryNSError.m index 2085747ee76..783dd88c0c7 100644 --- a/Sources/Sentry/SentryNSError.m +++ b/Sources/Sentry/SentryNSError.m @@ -3,59 +3,20 @@ NS_ASSUME_NONNULL_BEGIN -@interface -SentryNSError () - -@property (nonatomic, copy) NSString *domain; -@property (nonatomic, assign) NSInteger code; - -/** - * @note Can be empty, but never @c nil . - */ -@property (nonatomic, copy) NSArray *underlyingErrors; - -@end - @implementation SentryNSError -- (instancetype)initWithError:(NSError *)error +- (instancetype)initWithDomain:(NSString *)domain code:(NSInteger)code { if (self = [super init]) { - _domain = error.domain; - _code = error.code; - - if (@available(iOS 14.5, tvOS 14.5, macOS 11.3, watchOS 7.4, *)) { - NSMutableArray *underlyingErrors = - [NSMutableArray array]; - [error.underlyingErrors enumerateObjectsUsingBlock:^( - NSError *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - [underlyingErrors addObject:[[SentryNSError alloc] initWithError:obj]]; - }]; - _underlyingErrors = underlyingErrors; - } else { - NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey]; - if (underlyingError == nil) { - _underlyingErrors = @[]; - } else { - _underlyingErrors = @[ [[SentryNSError alloc] initWithError:underlyingError] ]; - } - } + _domain = domain; + _code = code; } return self; } - (NSDictionary *)serialize { - NSMutableDictionary *dict = [NSMutableDictionary - dictionaryWithObjectsAndKeys:self.domain, @"domain", @(self.code), @"code", nil]; - if (self.underlyingErrors.count > 0) { - NSMutableArray *> *serializedUnderlyingErrors = - [NSMutableArray *> array]; - [_underlyingErrors enumerateObjectsUsingBlock:^(SentryNSError *_Nonnull obj, NSUInteger idx, - BOOL *_Nonnull stop) { [serializedUnderlyingErrors addObject:[obj serialize]]; }]; - dict[@"underlying_errors"] = serializedUnderlyingErrors; - } - return dict; + return @{ @"domain" : self.domain, @"code" : @(self.code) }; } @end diff --git a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift index 3f7190d9dcd..0bc1f9e1a47 100644 --- a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift +++ b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift @@ -4,12 +4,12 @@ import XCTest class SentryNSErrorTests: XCTestCase { func testSerialize() { - let error = SentryNSError(error: NSError(domain: "domain", code: 10)) + let error = SentryNSError(domain: "domain", code: 10) let actual = error.serialize() - XCTAssertEqual(Dynamic(error).domain, actual["domain"] as? String) - XCTAssertEqual(Dynamic(error).code, actual["code"] as? Int) + XCTAssertEqual(error.domain, actual["domain"] as? String) + XCTAssertEqual(error.code, actual["code"] as? Int) } func testSerializeWithUnderlyingNSError() { diff --git a/Tests/SentryTests/Protocol/TestData.swift b/Tests/SentryTests/Protocol/TestData.swift index 604dbd10d7b..bd7641355df 100644 --- a/Tests/SentryTests/Protocol/TestData.swift +++ b/Tests/SentryTests/Protocol/TestData.swift @@ -128,7 +128,7 @@ class TestData { "code_name": "BUS_NOOP" ] - mechanismMeta.error = SentryNSError(error: NSError(domain: "SentrySampleDomain", code: 1)) + mechanismMeta.error = SentryNSError(domain: "SentrySampleDomain", code: 1) return mechanismMeta } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 13e938960cd..ea6718adcc4 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -524,6 +524,21 @@ class SentryClientTest: XCTestCase { 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() throws { let sessionBlockExpectation = expectation(description: "session block gets called") From 8fda44ba03ddb3aeaa7833455d41d24e315335ba Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 20 Oct 2023 13:49:35 -0800 Subject: [PATCH 6/7] fix changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35790a205e..493d4872d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Enrich error events with any underlying NSErrors reported by Cocoa APIs (#3230) + ### Fixes - Missing `mechanism.handled` is not considered crash (#3353) From b5ee1224ee946e2ed357efe5e8043d97726ca0b9 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 20 Oct 2023 13:54:37 -0800 Subject: [PATCH 7/7] Apply suggestions from code review --- Sources/Sentry/Public/SentryEvent.h | 4 ++-- Tests/SentryTests/Protocol/SentryNSErrorTests.swift | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/Sentry/Public/SentryEvent.h b/Sources/Sentry/Public/SentryEvent.h index 1cb98b4ccb9..15f49eb380b 100644 --- a/Sources/Sentry/Public/SentryEvent.h +++ b/Sources/Sentry/Public/SentryEvent.h @@ -138,8 +138,8 @@ NS_SWIFT_NAME(Event) @property (nonatomic, strong) NSArray *_Nullable threads; /** - * General information about the @c SentryException. Multiple exception indicate a chain of - * exception encountered, starting with the oldest at the beginning of 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/Tests/SentryTests/Protocol/SentryNSErrorTests.swift b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift index 0bc1f9e1a47..95757c8544c 100644 --- a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift +++ b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift @@ -1,4 +1,3 @@ -import SentryTestUtils import XCTest class SentryNSErrorTests: XCTestCase {