Skip to content

Commit

Permalink
feat: add underlying error info to reported NSErrors (#3230)
Browse files Browse the repository at this point in the history
We now flatten the recursive definitions of underlying errors into a list ordered from oldest to newest per the docs on exceptions so we can hopefully use the same grouping mechanisms. 

see also: https://develop.sentry.dev/sdk/event-payloads/exception/ and https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#sentry-issue-grouping
  • Loading branch information
armcknight authored Oct 23, 2023
1 parent 2ce582e commit 5e78d2b
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 153 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Sentry/Public/SentryEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -138,8 +138,8 @@ NS_SWIFT_NAME(Event)
@property (nonatomic, strong) NSArray<SentryThread *> *_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<SentryException *> *_Nullable exceptions;

Expand Down
34 changes: 29 additions & 5 deletions Sources/Sentry/SentryClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSError *> *errors = [NSMutableArray<NSError *> arrayWithObject:error];
NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
while (underlyingError != nil) {
[errors addObject:underlyingError];
underlyingError = underlyingError.userInfo[NSUnderlyingErrorKey];
}

NSMutableArray<SentryException *> *exceptions = [NSMutableArray<SentryException *> 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.
Expand Down Expand Up @@ -274,12 +302,8 @@ - (SentryEvent *)buildErrorEvent:(NSError *)error
NSDictionary<NSString *, id> *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
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
Loading

0 comments on commit 5e78d2b

Please sign in to comment.