Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
### Fixes

- Fix rate limiting all data categories when data category rate-limit is active. (#8324)
- Add `userInfo` context for unhandled `NSExceptions` (#8332)

### Features

Expand Down
71 changes: 71 additions & 0 deletions Sources/Sentry/SentryCrashReportConverter.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#import "SentryLogC.h"
#import "SentryMechanism.h"
#import "SentryMechanismContext.h"
#import "SentrySanitizerUtils.h"
#import "SentryStacktrace.h"
#import "SentrySwift.h"
#import "SentryThread.h"
Expand Down Expand Up @@ -137,6 +138,7 @@ - (SentryEvent *_Nullable)convertReportToEvent
appContext[@"in_foreground"] = self.applicationStats[@"application_in_foreground"];
appContext[@"is_active"] = self.applicationStats[@"application_active"];
mutableContext[@"app"] = appContext;
[self addNSExceptionUserInfoToContext:mutableContext];
event.context = mutableContext;

event.extra = self.userContext[@"extra"];
Expand Down Expand Up @@ -506,6 +508,75 @@ - (SentryDebugMeta *)debugMetaFromBinaryImageDictionary:(NSDictionary *)sourceIm
return @[ exception ];
}

- (void)addNSExceptionUserInfoToContext:(NSMutableDictionary *)context
{
if (![self.exceptionContext[@"type"] isEqualToString:@"nsexception"]) {
return;
}

NSDictionary *userInfo = [self
contextUserInfoFromNSExceptionUserInfo:self.exceptionContext[@"nsexception"][@"userInfo"]];
if (userInfo.count == 0) {
return;
}

NSMutableDictionary *userInfoContext;
if ([context[@"user info"] isKindOfClass:NSDictionary.class]) {
userInfoContext = [context[@"user info"] mutableCopy];
} else {
userInfoContext = [[NSMutableDictionary alloc] init];
}
[userInfoContext addEntriesFromDictionary:userInfo];
context[@"user info"] = userInfoContext;
}

- (NSDictionary *_Nullable)contextUserInfoFromNSExceptionUserInfo:(id)userInfo
{
if ([userInfo isKindOfClass:NSDictionary.class]) {
return sentry_sanitize_dictionary(userInfo);
}

// SentryCrash/KSCrash write NSException.userInfo with NSString stringWithFormat:@"%@",
// which produces an OpenStep-style property list for NSDictionary values. Convert it back to
// a dictionary so unhandled NSExceptions match handled NSExceptions from SentryClient as much
// as the raw crash report allows.
if (![userInfo isKindOfClass:NSString.class]) {
return nil;
}

NSString *userInfoString = userInfo;
if (userInfoString.length == 0 || [userInfoString isEqualToString:@"(null)"]) {
return nil;
}

NSDictionary *parsedUserInfo = nil;
if ([self parseNSExceptionUserInfoString:userInfoString intoDictionary:&parsedUserInfo]) {
return sentry_sanitize_dictionary(parsedUserInfo);
}

return @{ @"NSException.userInfo" : userInfoString };
}

- (BOOL)parseNSExceptionUserInfoString:(NSString *)userInfoString
intoDictionary:(NSDictionary *_Nullable *_Nonnull)parsedUserInfo
{
NSData *data = [userInfoString dataUsingEncoding:NSUTF8StringEncoding];
if (data == nil) {
return NO;
}

id propertyList = [NSPropertyListSerialization propertyListWithData:data
options:NSPropertyListImmutable
format:nil
error:nil];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h: We are silently ignoring the error here. We should properly propagate it to the caller and at least add a SentrySDKLog.warning/error statement

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did c3d8ddc resolve this for you?

if (![propertyList isKindOfClass:NSDictionary.class]) {
return NO;
}

*parsedUserInfo = propertyList;
return YES;
}

- (SentryException *)parseNSException
{
NSString *reason = nil;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation

//swiftlint:disable:next type_body_length
enum EventAssertions {
private static let binaryImageMarkerFileName = "crash-e2e-binary-images.json"

Expand Down Expand Up @@ -29,6 +30,7 @@ enum EventAssertions {
firstException: firstException,
mechanism: mechanism,
debugImages: debugImages,
eventContext: dictionary(event["contexts"]),
cacheRoot: cacheRoot
)
}
Expand Down Expand Up @@ -94,6 +96,7 @@ enum EventAssertions {
firstException: [String: Any],
mechanism: [String: Any],
debugImages: [[String: Any]],
eventContext: [String: Any],
cacheRoot: URL) throws {
switch scenario {
case .signal, .binaryImages, .managedRuntimeSignalChain, .managedRuntimePreSDKSignal,
Expand All @@ -109,6 +112,7 @@ enum EventAssertions {
case .nsException:
try assert(string(firstException["type"]) == "CrashE2ENSException",
"Expected NSException type for \(platform)/ns-exception")
try assertNSExceptionUserInfo(eventContext: eventContext, platform: platform)

case .cppExceptionV1, .cppExceptionV2, .swiftAsyncCPPExceptionV2Off:
try assertCPPException(firstException, mechanism: mechanism, platform: platform,
Expand All @@ -131,6 +135,12 @@ enum EventAssertions {
}
}

private static func assertNSExceptionUserInfo(eventContext: [String: Any], platform: String) throws {
let userInfoContext = dictionary(eventContext["user info"])
try assert(string(userInfoContext["scenario"]) == "ns-exception",
"Expected NSException userInfo scenario for \(platform)/ns-exception")
}

private static func assertBinaryImageScenario(_ debugImages: [[String: Any]], platform: String,
cacheRoot: URL) throws {
let markerURL = cacheRoot.appendingPathComponent(binaryImageMarkerFileName)
Expand Down Expand Up @@ -289,8 +299,4 @@ enum EventAssertions {
if let value = value as? String { return Int(value) }
return nil
}

private static func isNull(_ value: Any?) -> Bool {
value == nil || value is NSNull
}
}
36 changes: 36 additions & 0 deletions Tests/SentryTests/SentryCrashReportConverterTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,42 @@ - (void)testNSException_whenCrashInfoMessagePresent_shouldPreserveOriginalReason
XCTAssertEqualObjects(messages.firstObject, unrelatedCrashInfo);
}

- (void)testNSExceptionWithUserInfo_shouldAddUserInfoToEventContext
{
// -- Arrange --
NSString *userInfo = @"{\n scenario = ns-exception;\n customer = sentry;\n}";
NSDictionary *mockReport = @{
@"crash" : @ {
@"threads" : @[ @{
@"index" : @0,
@"crashed" : @YES,
@"current_thread" : @YES,
@"backtrace" : @ { @"contents" : @[] }
} ],
@"error" : @ {
@"type" : @"nsexception",
@"nsexception" : @ {
@"name" : @"NSInvalidArgumentException",
@"reason" : @"Crash E2E uncaught NSException",
@"userInfo" : userInfo
}
}
},
@"binary_images" : @[],
@"system" : @ { @"application_stats" : @ { @"application_in_foreground" : @YES } }
};

// -- Act --
SentryCrashReportConverter *reportConverter =
[[SentryCrashReportConverter alloc] initWithReport:mockReport inAppLogic:self.inAppLogic];
SentryEvent *event = [reportConverter convertReportToEvent];

// -- Assert --
NSDictionary *userInfoContext = event.context[@"user info"];
XCTAssertEqualObjects(userInfoContext[@"scenario"], @"ns-exception");
XCTAssertEqualObjects(userInfoContext[@"customer"], @"sentry");
}

- (void)testNSException_whenNoCrashInfoMessage_shouldKeepReason
{
// -- Arrange --
Expand Down
Loading