Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add underlying error info to reported NSErrors #3230

Merged
merged 10 commits into from
Oct 23, 2023
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Enrich error events with any underlying NSErrors reported by Cocoa APIs (#3230)

## 8.11.0

### Features
Expand Down
17 changes: 1 addition & 16 deletions Sources/Sentry/Public/SentryNSError.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface SentryNSError : NSObject <SentrySerializable>
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

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,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.
Expand Down
47 changes: 43 additions & 4 deletions Sources/Sentry/SentryNSError.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<SentryNSError *> *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, tvOS 14.5, macOS 11.3, watchOS 7.4, *)) {
NSMutableArray<SentryNSError *> *underlyingErrors =
[NSMutableArray<SentryNSError *> array];
[error.underlyingErrors enumerateObjectsUsingBlock:^(
NSError *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
[underlyingErrors addObject:[[SentryNSError alloc] initWithError:obj]];

Check warning on line 32 in Sources/Sentry/SentryNSError.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryNSError.m#L32

Added line #L32 was not covered by tests
}];
_underlyingErrors = underlyingErrors;
} else {
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
if (underlyingError == nil) {
_underlyingErrors = @[];
} else {
_underlyingErrors = @[ [[SentryNSError alloc] initWithError:underlyingError] ];

Check warning on line 40 in Sources/Sentry/SentryNSError.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryNSError.m#L40

Added line #L40 was not covered by tests
}
}
}
return self;
}

- (NSDictionary<NSString *, id> *)serialize
{
return @{ @"domain" : self.domain, @"code" : @(self.code) };
NSMutableDictionary<NSString *, id> *dict = [NSMutableDictionary<NSString *, id>
dictionaryWithObjectsAndKeys:self.domain, @"domain", @(self.code), @"code", nil];
if (self.underlyingErrors.count > 0) {
NSMutableArray<NSDictionary<NSString *, id> *> *serializedUnderlyingErrors =
[NSMutableArray<NSDictionary<NSString *, id> *> array];
[_underlyingErrors enumerateObjectsUsingBlock:^(SentryNSError *_Nonnull obj, NSUInteger idx,

Check warning on line 54 in Sources/Sentry/SentryNSError.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryNSError.m#L52-L54

Added lines #L52 - L54 were not covered by tests
BOOL *_Nonnull stop) { [serializedUnderlyingErrors addObject:[obj serialize]]; }];
dict[@"underlying_errors"] = serializedUnderlyingErrors;

Check warning on line 56 in Sources/Sentry/SentryNSError.m

View check run for this annotation

Codecov / codecov/patch

Sources/Sentry/SentryNSError.m#L56

Added line #L56 was not covered by tests
}
return dict;
}

@end
Expand Down
5 changes: 3 additions & 2 deletions Tests/SentryTests/Protocol/SentryMechanismMetaTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import SentryTestUtils
import XCTest

class SentryMechanismMetaTests: XCTestCase {
Expand All @@ -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")
Expand Down
7 changes: 4 additions & 3 deletions Tests/SentryTests/Protocol/SentryNSErrorTests.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import SentryTestUtils
armcknight marked this conversation as resolved.
Show resolved Hide resolved
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() {
Expand Down
2 changes: 1 addition & 1 deletion Tests/SentryTests/Protocol/TestData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading