Skip to content

Commit

Permalink
feat: Swift error names
Browse files Browse the repository at this point in the history
Call into Swift to get the error description for Swift errors to
get meaningful error names instead of only the error enum code.
To avoid sending PII the SDK strips parameter values and doesn't
send the swift error name for struct based Swift errors.

Fixes GH-2958
  • Loading branch information
philipphofmann committed Apr 25, 2023
1 parent a4ee85b commit 5c2bed4
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 20 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@

### Features

- Swift Error Names (#2958)

Instead of only the Swift error name and error code, the SDK now sends the Swift error name for enum-based errors.

```Swift
enum LoginError: Error {
case wrongUser
case wrongPassword
}

SentrySDK.capture(error: LoginError.wrongPassword)
```

Capturing the above Swift error will now result in the following error message in Sentry: `wrongPassword (Code: 1)` instead of only `(Code: 1)`.
[Customized error descriptions](https://docs.sentry.io/platforms/apple/usage/#customizing-error-descriptions) have precedence over this feature.
To avoid sending PII by accident, the SDK doesn't send the Swift error name for struct-based Swift errors, and the SDK drops the values of enums.

- Create User and Breadcrumb from map (#2820)

### Fixes
Expand Down
17 changes: 0 additions & 17 deletions Samples/iOS-Swift/iOS-Swift/Tools/RandomErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,6 @@ enum SampleError: Error {
case awesomeCentaur
}

extension SampleError: CustomNSError {
var errorUserInfo: [String: Any] {
func getDebugDescription() -> String {
switch self {
case SampleError.bestDeveloper:
return "bestDeveloper"
case .happyCustomer:
return "happyCustomer"
case .awesomeCentaur:
return "awesomeCentaur"
}
}

return [NSDebugDescriptionErrorKey: getDebugDescription()]
}
}

class RandomErrorGenerator {

static func generate() throws {
Expand Down
14 changes: 14 additions & 0 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#import "SentrySDK+Private.h"
#import "SentryScope+Private.h"
#import "SentryStacktraceBuilder.h"
#import "SentrySwift.h"
#import "SentryThreadInspector.h"
#import "SentryTraceContext.h"
#import "SentryTracer.h"
Expand Down Expand Up @@ -248,9 +249,22 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error

// If the error has a debug description, use that.
NSString *customExceptionValue = [[error userInfo] valueForKey:NSDebugDescriptionErrorKey];

NSString *swiftErrorDescription = nil;
// SwiftNativeNSError is the subclass of NSError used to represent bridged native Swift errors,
// see
// https://github.com/apple/swift/blob/067e4ec50147728f2cb990dbc7617d66692c1554/stdlib/public/runtime/ErrorObject.mm#L63-L73
NSString *errorClass = NSStringFromClass(error.class);
if ([errorClass containsString:@"SwiftNativeNSError"]) {
swiftErrorDescription = [SwiftDescriptor getSwiftErrorDescription:error];
}

if (customExceptionValue != nil) {
exceptionValue =
[NSString stringWithFormat:@"%@ (Code: %ld)", customExceptionValue, (long)error.code];
} else if (swiftErrorDescription != nil) {
exceptionValue =
[NSString stringWithFormat:@"%@ (Code: %ld)", swiftErrorDescription, (long)error.code];
} else {
exceptionValue = [NSString stringWithFormat:@"Code: %ld", (long)error.code];
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/Swift/SwiftDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,20 @@ public class SwiftDescriptor: NSObject {
return String(describing: type(of: object))
}

@objc
public static func getSwiftErrorDescription(_ error: Error) -> String? {
let description = String(describing: error)

// We can't reliably detect what is PII in a struct and what is not.
// Furthermore, we can't detect which property contains the error enum.
if description.contains(":") || description.contains(",") {
return nil
}

// For error enums the description could contain PII in between (). Therefore,
// we strip the data.
let index = description.firstIndex(of: "(") ?? description.endIndex
return String(description[..<index])
}

}
85 changes: 82 additions & 3 deletions Tests/SentryTests/SentryClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ class SentryClientTest: XCTestCase {
eventId.assertIsNotEmpty()
let error = TestError.invalidTest as NSError
assertLastSentEvent { actual in
assertValidErrorEvent(actual, error)
assertValidErrorEvent(actual, error, exceptionValue: "invalidTest (Code: 0)")
}
}

Expand Down Expand Up @@ -456,6 +456,63 @@ class SentryClientTest: XCTestCase {
}
}
}

func testCaptureSwiftError_UsesSwiftStringDescription() {
let eventId = fixture.getSut().capture(error: SentryClientError.someError)

eventId.assertIsNotEmpty()
assertLastSentEvent { actual in
do {
let exceptions = try XCTUnwrap(actual.exceptions)
XCTAssertEqual("someError (Code: 1)", try XCTUnwrap(exceptions.first).value)
} catch {
XCTFail("Exception expected but was nil")
}
}
}

func testCaptureSwiftErrorKind_UsesSwiftStringDescription() {
let eventId = fixture.getSut().capture(error: XMLParsingError(line: 10, column: 12, kind: .internalError))

eventId.assertIsNotEmpty()
assertLastSentEvent { actual in
do {
let exceptions = try XCTUnwrap(actual.exceptions)
XCTAssertEqual("(Code: 1)", try XCTUnwrap(exceptions.first).value)
} catch {
XCTFail("Exception expected but was nil")
}
}
}

func testCaptureSwiftErrorWithData_UsesSwiftStringDescriptionStripped() {
let eventId = fixture.getSut().capture(error: SentryClientError.invalidInput("hello"))

eventId.assertIsNotEmpty()
assertLastSentEvent { actual in
do {
let exceptions = try XCTUnwrap(actual.exceptions)
XCTAssertEqual("invalidInput (Code: 0)", try XCTUnwrap(exceptions.first).value)
} catch {
XCTFail("Exception expected but was nil")
}
}
}

func testCaptureSwiftErrorWithDebugDescription_UsesDebugDescription() {

let eventId = fixture.getSut().capture(error: SentryClientErrorWithDebugDescription.someError)

eventId.assertIsNotEmpty()
assertLastSentEvent { actual in
do {
let exceptions = try XCTUnwrap(actual.exceptions)
XCTAssertEqual("anotherError (Code: 0)", try XCTUnwrap(exceptions.first).value)
} catch {
XCTFail("Exception expected but was nil")
}
}
}

func testCaptureErrorWithComplexUserInfo() {
let url = URL(string: "https://github.com/getsentry")!
Expand Down Expand Up @@ -1464,7 +1521,7 @@ class SentryClientTest: XCTestCase {
}
}

private func assertValidErrorEvent(_ event: Event, _ error: NSError) {
private func assertValidErrorEvent(_ event: Event, _ error: NSError, exceptionValue: String? = nil) {
XCTAssertEqual(SentryLevel.error, event.level)
XCTAssertEqual(error, event.error as NSError?)

Expand All @@ -1475,7 +1532,7 @@ class SentryClientTest: XCTestCase {
let exception = exceptions[0]
XCTAssertEqual(error.domain, exception.type)

XCTAssertEqual("Code: \(error.code)", exception.value)
XCTAssertEqual(exceptionValue ?? "Code: \(error.code)", exception.value)

XCTAssertNil(exception.threadId)
XCTAssertNil(exception.stacktrace)
Expand Down Expand Up @@ -1565,4 +1622,26 @@ class SentryClientTest: XCTestCase {

}

enum SentryClientError: Error {
case someError
case invalidInput(String)
}

enum SentryClientErrorWithDebugDescription: Error {
case someError
}

extension SentryClientErrorWithDebugDescription: CustomNSError {
var errorUserInfo: [String: Any] {
func getDebugDescription() -> String {
switch self {
case .someError:
return "anotherError"
}
}

return [NSDebugDescriptionErrorKey: getDebugDescription()]
}
}

// swiftlint:enable file_length
54 changes: 54 additions & 0 deletions Tests/SentryTests/SwiftDescriptorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,61 @@ class SwiftDescriptorTests: XCTestCase {
XCTAssertEqual(name, "InnerClass")
}

func testgetSwiftErrorDescription_EnumValue() {
let actual = SwiftDescriptor.getSwiftErrorDescription(SentryTestError.someError)
XCTAssertEqual("someError", actual)
}

func testgetSwiftErrorDescription_EnumValueWithData() {
let actual = SwiftDescriptor.getSwiftErrorDescription(SentryTestError.someOhterError(10))
XCTAssertEqual("someOhterError", actual)
}

func testgetSwiftErrorDescription_StructWithData() {
let actual = SwiftDescriptor.getSwiftErrorDescription(XMLParsingError(line: 10, column: 12, kind: .internalError))
XCTAssertNil(actual)

SentrySDK.capture(error: LoginError.wrongPassword)
}

func testgetSwiftErrorDescription_StructWithOneParam() {
let actual = SwiftDescriptor.getSwiftErrorDescription(StructWithOneParam(line: 10))
XCTAssertNil(actual)
}

private func sanitize(_ name: AnyObject) -> String {
return SwiftDescriptor.getObjectClassName(name)
}
}

enum SentryTestError: Error {
case someError
case someOhterError(Int)
}

enum LoginError: Error {
case wrongUser
case wrongPassword
}

struct XMLParsingError: Error {
enum ErrorKind {
case invalidCharacter
case mismatchedTag
case internalError
}

let line: Int
let column: Int
let kind: ErrorKind
}

struct StructWithOneParam: Error {
enum ErrorKind {
case invalidCharacter
case mismatchedTag
case internalError
}

let line: Int
}

0 comments on commit 5c2bed4

Please sign in to comment.