Skip to content

Commit

Permalink
Merge 7bdffea into 9454d5d
Browse files Browse the repository at this point in the history
  • Loading branch information
philipphofmann authored May 2, 2023
2 parents 9454d5d + 7bdffea commit 51283b0
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 20 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## Unreleased

- Swift Error Names (#2960)

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.
This change has no impact on grouping of the issues in Sentry.

## 8.6.0

### Features
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 testCaptureSwiftErrorStruct_DoesNotUseDescriptionToAvoidPII() {
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 51283b0

Please sign in to comment.