From e849ff293a4303aca52ba0fbbe1f9b203e59a843 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 19 Feb 2019 10:24:00 +0000 Subject: [PATCH] feat: Parse file metadata into reports when incomplete This change formats report file names to include minimal metadata: * Add accessor for report contents indexed by filename * Add async-safe customized filepath generator * Update the callbacks to explicitly include the report count Incomplete reports have been corrupted or otherwise could not be written, and thus have no content in the report data dictionary. Fallback values are parsed from the file name metadata and used as needed without depending on an individual value to determine "incomplete-ness". --- CHANGELOG.md | 8 ++ OSX/Bugsnag.xcodeproj/project.pbxproj | 4 + Source/BugsnagApiClient.h | 12 +-- Source/BugsnagApiClient.m | 18 ++--- Source/BugsnagCrashReport.h | 16 ++++ Source/BugsnagCrashReport.m | 73 +++++++++++++++++-- Source/BugsnagErrorReportApiClient.m | 4 +- Source/BugsnagFileStore.h | 6 ++ Source/BugsnagFileStore.m | 10 ++- Source/BugsnagKeys.h | 1 + Source/BugsnagSessionTrackingApiClient.m | 10 +-- Source/BugsnagSink.m | 29 ++++---- .../Source/KSCrash/Recording/BSG_KSCrash.m | 21 ++++-- .../KSCrash/Recording/BSG_KSCrashAdvanced.h | 6 +- .../Source/KSCrash/Recording/BSG_KSCrashC.c | 52 ++++++++++++- .../Source/KSCrash/Recording/BSG_KSCrashC.h | 1 + .../Recording/Sentry/BSG_KSCrashSentry.c | 4 +- .../Recording/Sentry/BSG_KSCrashSentry.h | 4 +- .../Sentry/BSG_KSCrashSentry_CPPException.mm | 4 +- .../Sentry/BSG_KSCrashSentry_Deadlock.m | 2 +- .../Sentry/BSG_KSCrashSentry_MachException.c | 4 +- .../Sentry/BSG_KSCrashSentry_NSException.m | 4 +- .../Sentry/BSG_KSCrashSentry_Signal.c | 13 +++- .../Recording/Sentry/BSG_KSCrashSentry_User.c | 5 +- .../Recording/Sentry/BSG_KSCrashSentry_User.h | 1 + .../Tools/BSG_KSCrashCallCompletion.h | 4 +- .../Tools/BSG_KSCrashCallCompletion.m | 4 +- .../Filters/BSG_KSCrashReportFilter.h | 2 +- .../BSG_KSCrashReportFilterCompletion.h | 5 +- Tests/BSGFilepathTests.m | 64 ++++++++++++++++ Tests/BugsnagCrashReportTests.m | 41 ++++++++++- Tests/KSCrash/KSCrashSentry_Tests.m | 2 +- features/crashprobe.feature | 12 +++ features/steps/ios_steps.rb | 9 +++ iOS/Bugsnag.xcodeproj/project.pbxproj | 4 + tvOS/Bugsnag.xcodeproj/project.pbxproj | 4 + 36 files changed, 386 insertions(+), 77 deletions(-) create mode 100644 Tests/BSGFilepathTests.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d38ade7..14ee3759e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Changelog ========= +## TBD + +### Enhancements + +* Capture basic report diagnostics in the file path in case of crash report + content corruption + [#327](https://github.com/bugsnag/bugsnag-cocoa/pull/327) + ## 5.17.3 (2018-12-19) ### Bug Fixes diff --git a/OSX/Bugsnag.xcodeproj/project.pbxproj b/OSX/Bugsnag.xcodeproj/project.pbxproj index eb63a4c0f..875a33720 100644 --- a/OSX/Bugsnag.xcodeproj/project.pbxproj +++ b/OSX/Bugsnag.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8A12006C221C50F40008C9C3 /* BSGFilepathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A12006B221C50F40008C9C3 /* BSGFilepathTests.m */; }; 8A2C8FAC1C6BC1F700846019 /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C8FA11C6BC1F700846019 /* Bugsnag.framework */; }; 8A2C8FCD1C6BC2C800846019 /* Bugsnag.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8FBB1C6BC2C800846019 /* Bugsnag.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8FCE1C6BC2C800846019 /* Bugsnag.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8FBC1C6BC2C800846019 /* Bugsnag.m */; }; @@ -188,6 +189,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8A12006B221C50F40008C9C3 /* BSGFilepathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGFilepathTests.m; sourceTree = ""; }; 8A2C8FA11C6BC1F700846019 /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8A2C8FA61C6BC1F700846019 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8A2C8FAB1C6BC1F700846019 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -469,6 +471,7 @@ 8A2C8FAF1C6BC1F700846019 /* Tests */ = { isa = PBXGroup; children = ( + 8A12006B221C50F40008C9C3 /* BSGFilepathTests.m */, E7CE78861FD94E40001D07E0 /* KSCrash */, E791482D1FD82B0C003EFEBF /* BugsnagSessionTest.m */, E791482B1FD82B0C003EFEBF /* BugsnagSessionTrackerTest.m */, @@ -933,6 +936,7 @@ 8ACF0F752018136200173809 /* BugsnagCrashReportTests.m in Sources */, E7CE78CC1FD94E77001D07E0 /* KSSystemInfo_Tests.m in Sources */, E7CE78C91FD94E77001D07E0 /* KSSignalInfo_Tests.m in Sources */, + 8A12006C221C50F40008C9C3 /* BSGFilepathTests.m in Sources */, E7CE78C61FD94E77001D07E0 /* KSMach_Tests.m in Sources */, E7CE78D61FD94E9E001D07E0 /* FileBasedTestCase.m in Sources */, E7CE78D51FD94E93001D07E0 /* XCTestCase+KSCrash.m in Sources */, diff --git a/Source/BugsnagApiClient.h b/Source/BugsnagApiClient.h index 1c37a3054..d1cca8eab 100644 --- a/Source/BugsnagApiClient.h +++ b/Source/BugsnagApiClient.h @@ -7,7 +7,7 @@ @class BugsnagConfiguration; -typedef void (^RequestCompletion)(id data, BOOL success, NSError *error); +typedef void (^RequestCompletion)(NSUInteger reportCount, BOOL success, NSError *error); @interface BugsnagApiClient : NSObject @@ -21,11 +21,11 @@ typedef void (^RequestCompletion)(id data, BOOL success, NSError *error); - (NSOperation *)deliveryOperation; -- (void)sendData:(id)data - withPayload:(NSDictionary *)payload - toURL:(NSURL *)url - headers:(NSDictionary *)headers - onCompletion:(RequestCompletion)onCompletion; +- (void)sendItems:(NSUInteger)count + withPayload:(NSDictionary *)payload + toURL:(NSURL *)url + headers:(NSDictionary *)headers + onCompletion:(RequestCompletion)onCompletion; @property(readonly) NSOperationQueue *sendQueue; @property(readonly) BugsnagConfiguration *config; diff --git a/Source/BugsnagApiClient.m b/Source/BugsnagApiClient.m index 8550286c2..2463844f4 100644 --- a/Source/BugsnagApiClient.m +++ b/Source/BugsnagApiClient.m @@ -49,11 +49,11 @@ - (NSOperation *)deliveryOperation { #pragma mark - Delivery -- (void)sendData:(id)data - withPayload:(NSDictionary *)payload - toURL:(NSURL *)url - headers:(NSDictionary *)headers - onCompletion:(RequestCompletion)onCompletion { +- (void)sendItems:(NSUInteger)count + withPayload:(NSDictionary *)payload + toURL:(NSURL *)url + headers:(NSDictionary *)headers + onCompletion:(RequestCompletion)onCompletion { @try { NSError *error = nil; @@ -64,7 +64,7 @@ - (void)sendData:(id)data if (jsonData == nil) { if (onCompletion) { - onCompletion(data, NO, error); + onCompletion(0, NO, error); } return; } @@ -79,7 +79,7 @@ - (void)sendData:(id)data NSURLResponse *_Nullable response, NSError *_Nullable requestErr) { if (onCompletion) { - onCompletion(data, requestErr == nil, requestErr); + onCompletion(count, requestErr == nil, requestErr); } }]; [task resume]; @@ -92,13 +92,13 @@ - (void)sendData:(id)data returningResponse:&response error:&error]; if (onCompletion) { - onCompletion(data, error == nil, error); + onCompletion(count, error == nil, error); } #pragma clang diagnostic pop } } @catch (NSException *exception) { if (onCompletion) { - onCompletion(data, NO, + onCompletion(count, NO, [NSError errorWithDomain:exception.reason code:420 userInfo:@{BSGKeyException: exception}]); diff --git a/Source/BugsnagCrashReport.h b/Source/BugsnagCrashReport.h index 6d82b7a55..027a9fde1 100644 --- a/Source/BugsnagCrashReport.h +++ b/Source/BugsnagCrashReport.h @@ -39,6 +39,18 @@ NSString *_Nonnull BSGFormatSeverity(BSGSeverity severity); @interface BugsnagCrashReport : NSObject +/** + * Create a new crash report from a JSON crash report generated by + * BugsnagCrashSentry + * + * @param report a BugsnagCrashSentry JSON report + * @param metadata additional report info encoded as a string + * + * @return a Bugsnag crash report + */ +- (instancetype _Nonnull)initWithKSReport:(NSDictionary *_Nonnull)report + fileMetadata:(NSString *_Nonnull)metadata; + /** * Create a new crash report from a JSON crash report generated by * BugsnagCrashSentry @@ -193,6 +205,10 @@ __deprecated_msg("Use toJson: instead."); */ @property(readwrite, copy, nullable) NSDictionary *appState; +/** + * If YES, a complete report was not able to be obtained at generation time + */ +@property (readonly, nonatomic, getter=isIncomplete) BOOL incomplete; /** * Returns the enhanced error message for the thread, or nil if none exists. diff --git a/Source/BugsnagCrashReport.m b/Source/BugsnagCrashReport.m index 00eb36769..87c9aa5d7 100644 --- a/Source/BugsnagCrashReport.m +++ b/Source/BugsnagCrashReport.m @@ -70,7 +70,8 @@ } NSString *_Nonnull BSGParseErrorClass(NSDictionary *error, - NSString *errorType) { + NSString *errorType, + NSString *fallbackValue) { NSString *errorClass; if ([errorType isEqualToString:BSGKeyCppException]) { @@ -86,7 +87,7 @@ } if (!errorClass) { // use a default value - errorClass = @"Exception"; + errorClass = fallbackValue.length > 0 ? fallbackValue : @"Exception"; } return errorClass; } @@ -184,6 +185,13 @@ + (instancetype)errorDataFromThreads:(NSArray *)threads; - (instancetype)initWithClass:(NSString *_Nonnull)errorClass message:(NSString *_Nonnull)errorMessage NS_DESIGNATED_INITIALIZER; @end +@interface FallbackReportData : NSObject +@property (nonatomic, strong) NSString *errorClass; +@property (nonatomic, getter=isUnhandled) BOOL unhandled; +@property (nonatomic) BSGSeverity severity; +- (instancetype)initWithMetadata:(NSString *)metadata; +@end + @interface BugsnagCrashReport () /** @@ -212,25 +220,33 @@ @interface BugsnagCrashReport () @property(nonatomic, readwrite, copy, nullable) NSDictionary *customException; @property(nonatomic) BugsnagSession *session; +@property (nonatomic, readwrite, getter=isIncomplete) BOOL incomplete; @end @implementation BugsnagCrashReport - (instancetype)initWithKSReport:(NSDictionary *)report { + return [self initWithKSReport:report fileMetadata:@""]; +} + +- (instancetype)initWithKSReport:(NSDictionary *)report + fileMetadata:(NSString *)metadata { if (self = [super init]) { + FallbackReportData *fallback = [[FallbackReportData alloc] initWithMetadata:metadata]; _notifyReleaseStages = [report valueForKeyPath:@"user.config.notifyReleaseStages"]; _releaseStage = BSGParseReleaseStage(report); _error = [report valueForKeyPath:@"crash.error"]; + _incomplete = report.count == 0; _errorType = _error[BSGKeyType]; _threads = [report valueForKeyPath:@"crash.threads"]; RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:_threads]; if (data) { - _errorClass = data.errorClass; + _errorClass = data.errorClass ?: fallback.errorClass; _errorMessage = data.errorMessage; } else { - _errorClass = BSGParseErrorClass(_error, _errorType); + _errorClass = BSGParseErrorClass(_error, _errorType, fallback.errorClass); _errorMessage = BSGParseErrorMessage(report, _error, _errorType); } _binaryImages = report[@"binary_images"]; @@ -259,7 +275,7 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { // only makes sense to use serialised value for handled exceptions _depth = [[report valueForKeyPath:@"user.state.crash.depth"] unsignedIntegerValue]; - } else { // the event was unhandled. + } else if (_errorType != nil) { // the event was unhandled. BOOL isSignal = [BSGKeySignal isEqualToString:_errorType]; SeverityReasonType severityReason = isSignal ? Signal : UnhandledException; @@ -268,6 +284,11 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { severity:BSGSeverityError attrValue:_errorClass]; _depth = 0; + } else { // Incomplete report + SeverityReasonType severityReason = [fallback isUnhandled] ? UnhandledException : HandledError; + _handledState = [BugsnagHandledState handledStateWithSeverityReason:severityReason + severity:fallback.severity + attrValue:nil]; } _severity = _handledState.currentSeverity; @@ -478,7 +499,11 @@ - (NSDictionary *)toJson { BSGDictSetSafeObject(event, BSGFormatSeverity(self.severity), BSGKeySeverity); BSGDictSetSafeObject(event, [self breadcrumbs], BSGKeyBreadcrumbs); BSGDictSetSafeObject(event, metaData, BSGKeyMetaData); - + + if ([self isIncomplete]) { + BSGDictSetSafeObject(event, @YES, BSGKeyIncomplete); + } + NSDictionary *device = [self.device bsg_mergedInto:self.deviceState]; BSGDictSetSafeObject(event, device, BSGKeyDevice); @@ -620,6 +645,42 @@ - (NSString *_Nullable)enhancedErrorMessageForThread:(NSDictionary *_Nullable)th @end +@implementation FallbackReportData + +- (instancetype)initWithMetadata:(NSString *)metadata { + if (self = [super init]) { + NSString *separator = @"-"; + NSString *location = metadata; + NSRange range = [location rangeOfString:separator options:NSBackwardsSearch]; + if (range.location != NSNotFound) { + _errorClass = [location substringFromIndex:range.location + 1]; + location = [location substringToIndex:range.location]; + } + range = [location rangeOfString:separator options:NSBackwardsSearch]; + if (range.location != NSNotFound) { + NSString *value = [location substringFromIndex:range.location + 1]; + _unhandled = ![value isEqualToString:@"h"]; + location = [location substringToIndex:range.location + 1]; + } else { + _unhandled = YES; + } + range = [location rangeOfString:separator options:NSBackwardsSearch]; + if (range.location != NSNotFound) { + NSString *value = [location substringFromIndex:range.location]; + if ([value isEqualToString:@"w"]) { + _severity = BSGSeverityWarning; + } else if ([value isEqualToString:@"i"]) { + _severity = BSGSeverityInfo; + } else { + _severity = BSGSeverityError; + } + } + } + return self; +} + +@end + @implementation RegisterErrorData + (instancetype)errorDataFromThreads:(NSArray *)threads { for (NSDictionary *thread in threads) { diff --git a/Source/BugsnagErrorReportApiClient.m b/Source/BugsnagErrorReportApiClient.m index 0bf3b80b8..18b2d1cef 100644 --- a/Source/BugsnagErrorReportApiClient.m +++ b/Source/BugsnagErrorReportApiClient.m @@ -30,11 +30,11 @@ - (void)main { @autoreleasepool { @try { [[BSG_KSCrash sharedInstance] - sendAllReportsWithCompletion:^(NSArray *filteredReports, + sendAllReportsWithCompletion:^(NSUInteger sentReportCount, BOOL completed, NSError *error) { if (error) { bsg_log_warn(@"Failed to send reports: %@", error); - } else if (filteredReports.count > 0) { + } else if (sentReportCount > 0) { bsg_log_info(@"Reports sent."); } }]; diff --git a/Source/BugsnagFileStore.h b/Source/BugsnagFileStore.h index 48a10510f..fad1e36aa 100644 --- a/Source/BugsnagFileStore.h +++ b/Source/BugsnagFileStore.h @@ -40,6 +40,12 @@ */ - (NSArray *)allFiles; +/** Get a list of all files by filename. + * + * @return A collection of file contents indexed by filename. + */ +- (NSDictionary *)allFilesByName; + /** Delete a file. * * @param fileId The file ID. diff --git a/Source/BugsnagFileStore.m b/Source/BugsnagFileStore.m index 5fadd7a0e..1eee97744 100644 --- a/Source/BugsnagFileStore.m +++ b/Source/BugsnagFileStore.m @@ -128,13 +128,17 @@ - (NSUInteger)fileCount { } - (NSArray *)allFiles { + return [[self allFilesByName] allValues]; +} + +- (NSDictionary *)allFilesByName { NSArray *fileIds = [self fileIds]; - NSMutableArray *files = - [NSMutableArray arrayWithCapacity:[fileIds count]]; + NSMutableDictionary *files = + [NSMutableDictionary dictionaryWithCapacity:[fileIds count]]; for (NSString *fileId in fileIds) { NSDictionary *fileContents = [self fileWithId:fileId]; if (fileContents != nil) { - [files addObject:fileContents]; + [files setObject:fileContents forKey:fileId]; } } diff --git a/Source/BugsnagKeys.h b/Source/BugsnagKeys.h index 54a89693f..aabd15608 100644 --- a/Source/BugsnagKeys.h +++ b/Source/BugsnagKeys.h @@ -19,6 +19,7 @@ static NSString *const BSGKeyName = @"name"; static NSString *const BSGKeyTimestamp = @"timestamp"; static NSString *const BSGKeyType = @"type"; static NSString *const BSGKeyMetaData = @"metaData"; +static NSString *const BSGKeyIncomplete = @"incomplete"; static NSString *const BSGKeyId = @"id"; static NSString *const BSGKeyUser = @"user"; static NSString *const BSGKeyEmail = @"email"; diff --git a/Source/BugsnagSessionTrackingApiClient.m b/Source/BugsnagSessionTrackingApiClient.m index b140ecb7d..e63719e7e 100644 --- a/Source/BugsnagSessionTrackingApiClient.m +++ b/Source/BugsnagSessionTrackingApiClient.m @@ -47,11 +47,11 @@ - (void)deliverSessionsInStore:(BugsnagSessionFileStore *)store { @"Bugsnag-API-Key": apiKey, @"Bugsnag-Sent-At": [BSG_RFC3339DateTool stringFromDate:[NSDate new]] }; - [self sendData:payload - withPayload:[payload toJson] - toURL:sessionURL - headers:HTTPHeaders - onCompletion:^(id data, BOOL success, NSError *error) { + [self sendItems:sessions.count + withPayload:[payload toJson] + toURL:sessionURL + headers:HTTPHeaders + onCompletion:^(NSUInteger sentCount, BOOL success, NSError *error) { if (success && error == nil) { bsg_log_info(@"Sent %lu sessions to Bugsnag", (unsigned long) sessionCount); diff --git a/Source/BugsnagSink.m b/Source/BugsnagSink.m index 0fc2517a1..f81920713 100644 --- a/Source/BugsnagSink.m +++ b/Source/BugsnagSink.m @@ -56,15 +56,18 @@ - (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient { // - the report-specific `notifyReleaseStages` property is unset and the global // `notifyReleaseStages` property // and it contains the current stage -- (void)filterReports:(NSArray *)reports +- (void)filterReports:(NSDictionary *)reports onCompletion:(BSG_KSCrashReportFilterCompletion)onCompletion { NSMutableArray *bugsnagReports = [NSMutableArray new]; BugsnagConfiguration *configuration = [Bugsnag configuration]; - for (NSDictionary *report in reports) { - BugsnagCrashReport *bugsnagReport = [[BugsnagCrashReport alloc] initWithKSReport:report]; - BOOL incompleteReport = (![@"standard" isEqualToString:[report valueForKeyPath:@"report.type"]] || - [[report objectForKey:@"incomplete"] boolValue]); + for (NSString *fileKey in reports) { + NSDictionary *report = reports[fileKey]; + BugsnagCrashReport *bugsnagReport = [[BugsnagCrashReport alloc] initWithKSReport:report + fileMetadata:fileKey]; + BOOL incompleteReport = ([bugsnagReport isIncomplete] + || ![@"standard" isEqualToString:[report valueForKeyPath:@"report.type"]] + || [[report objectForKey:@"incomplete"] boolValue]); if (incompleteReport) { // append app/device data as this is unlikely to change between sessions NSDictionary *sysInfo = [BSG_KSSystemInfo systemInfo]; @@ -109,7 +112,7 @@ - (void)filterReports:(NSArray *)reports if (bugsnagReports.count == 0) { if (onCompletion) { - onCompletion(bugsnagReports, YES, nil); + onCompletion(bugsnagReports.count, YES, nil); } return; } @@ -120,7 +123,7 @@ - (void)filterReports:(NSArray *)reports #pragma clang diagnostic ignored "-Wdeprecated-declarations" for (BugsnagBeforeNotifyHook hook in configuration.beforeNotifyHooks) { if (reportData) { - reportData = hook(reports, reportData); + reportData = hook(bugsnagReports, reportData); } else { break; } @@ -129,16 +132,16 @@ - (void)filterReports:(NSArray *)reports if (reportData == nil) { if (onCompletion) { - onCompletion(@[], YES, nil); + onCompletion(0, YES, nil); } return; } - [self.apiClient sendData:bugsnagReports - withPayload:reportData - toURL:configuration.notifyURL - headers:[configuration errorApiHeaders] - onCompletion:onCompletion]; + [self.apiClient sendItems:bugsnagReports.count + withPayload:reportData + toURL:configuration.notifyURL + headers:[configuration errorApiHeaders] + onCompletion:onCompletion]; } diff --git a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m index deb835d17..39e977e65 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m +++ b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m @@ -320,12 +320,12 @@ - (void)sendAllReportsWithCompletion: (BSG_KSCrashReportFilterCompletion)onCompletion { [self.crashReportStore pruneFilesLeaving:self.maxStoredReports]; - NSArray *reports = [self allReports]; + NSDictionary *reports = [self allReportsByFilename]; BSG_KSLOG_INFO(@"Sending %d crash reports", [reports count]); [self sendReports:reports - onCompletion:^(NSArray *filteredReports, BOOL completed, + onCompletion:^(NSUInteger sentReportCount, BOOL completed, NSError *error) { BSG_KSLOG_DEBUG(@"Process finished with completion: %d", completed); if (error != nil) { @@ -336,7 +336,7 @@ - (void)sendAllReportsWithCompletion: self.deleteBehaviorAfterSendAll == BSG_KSCDeleteAlways) { [self deleteAllReports]; } - bsg_kscrash_i_callCompletion(onCompletion, filteredReports, + bsg_kscrash_i_callCompletion(onCompletion, sentReportCount, completed, error); }]; } @@ -357,6 +357,7 @@ - (void)reportUserException:(NSString *)name const char *cName = [name cStringUsingEncoding:NSUTF8StringEncoding]; const char *cReason = [reason cStringUsingEncoding:NSUTF8StringEncoding]; bsg_kscrash_reportUserException(cName, cReason, + [handledState[@"severity"] UTF8String], [self encodeAsJSONString:handledState], [self encodeAsJSONString:overrides], [self encodeAsJSONString:metadata], @@ -404,16 +405,16 @@ - (NSString *)crashReportsPath { return self.crashReportStore.path; } -- (void)sendReports:(NSArray *)reports +- (void)sendReports:(NSDictionary *)reports onCompletion:(BSG_KSCrashReportFilterCompletion)onCompletion { if ([reports count] == 0) { - bsg_kscrash_i_callCompletion(onCompletion, reports, YES, nil); + bsg_kscrash_i_callCompletion(onCompletion, 0, YES, nil); return; } if (self.sink == nil) { bsg_kscrash_i_callCompletion( - onCompletion, reports, NO, + onCompletion, 0, NO, [NSError bsg_errorWithDomain:[[self class] description] code:0 description:@"No sink set. Crash reports not sent."]); @@ -421,9 +422,9 @@ - (void)sendReports:(NSArray *)reports } [self.sink filterReports:reports - onCompletion:^(NSArray *filteredReports, BOOL completed, + onCompletion:^(NSUInteger sentReportCount, BOOL completed, NSError *error) { - bsg_kscrash_i_callCompletion(onCompletion, filteredReports, + bsg_kscrash_i_callCompletion(onCompletion, sentReportCount, completed, error); }]; } @@ -432,6 +433,10 @@ - (NSArray *)allReports { return [self.crashReportStore allFiles]; } +- (NSDictionary *)allReportsByFilename { + return [self.crashReportStore allFilesByName]; +} + - (BOOL)redirectConsoleLogsToFile:(NSString *)fullPath overwrite:(BOOL)overwrite { if (bsg_kslog_setLogFilename([fullPath UTF8String], overwrite)) { diff --git a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashAdvanced.h b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashAdvanced.h index 82692373e..24682d038 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashAdvanced.h +++ b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashAdvanced.h @@ -85,6 +85,10 @@ typedef enum { */ - (NSArray *)allReports; +/** Get all reports as dictionaries, indexed by file name. + */ +- (NSDictionary *)allReportsByFilename; + #pragma mark - Configuration - /** Init BSG_KSCrash instance with custom report files directory path. */ @@ -156,7 +160,7 @@ typedef enum { * @param reports The reports to send. * @param onCompletion Called when sending is complete (nil = ignore). */ -- (void)sendReports:(NSArray *)reports +- (void)sendReports:(NSDictionary *)reports onCompletion:(BSG_KSCrashReportFilterCompletion)onCompletion; @end diff --git a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c index 1ebc3a93b..0165b7de5 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c +++ b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.c @@ -63,11 +63,53 @@ static char *bsg_g_stateFilePath; // ============================================================================ #pragma mark - Utility - // ============================================================================ +static const int bsg_filepath_len = 512; +static const int bsg_error_class_filepath_len = 21; +static const char bsg_filepath_context_sep = '-'; static inline BSG_KSCrash_Context *crashContext(void) { return &bsg_g_crashReportContext; } +int bsg_create_filepath(char *base, char filepath[bsg_filepath_len], char severity, char error_class[bsg_error_class_filepath_len]) { + int length; + for (length = 0; length < bsg_filepath_len; length++) { + if (base[length] == '\0') { + break; + } + filepath[length] = base[length]; + } + if (length > 5) // Remove initial .json from path + length -= 5; + + // append contextual info + BSG_KSCrash_Context *context = crashContext(); + filepath[length++] = bsg_filepath_context_sep; + filepath[length++] = severity; + filepath[length++] = bsg_filepath_context_sep; + // 'h' for handled vs 'u'nhandled + filepath[length++] = context->crash.crashType == BSG_KSCrashTypeUserReported ? 'h' : 'u'; + filepath[length++] = bsg_filepath_context_sep; + for (int i = 0; error_class != NULL && i < bsg_error_class_filepath_len; i++) { + char c = error_class[i]; + if (c == '\0') + break; + else if (c == 47 || c > 126 || c <= 0) + // disallow '/' and characters outside of the ascii range + continue; + filepath[length++] = c; + } + // add suffix + filepath[length++] = '.'; + filepath[length++] = 'j'; + filepath[length++] = 's'; + filepath[length++] = 'o'; + filepath[length++] = 'n'; + filepath[length++] = '\0'; + + return length; +} + // ============================================================================ #pragma mark - Callbacks - // ============================================================================ @@ -78,7 +120,7 @@ static inline BSG_KSCrash_Context *crashContext(void) { * * This function gets passed as a callback to a crash handler. */ -void bsg_kscrash_i_onCrash(void) { +void bsg_kscrash_i_onCrash(char severity, char *errorClass) { BSG_KSLOG_DEBUG("Updating application state to note crash."); bsg_kscrashstate_notifyAppCrash(); @@ -92,8 +134,9 @@ void bsg_kscrash_i_onCrash(void) { bsg_kscrashreport_writeMinimalReport(context, bsg_g_recrashReportFilePath); } else { - bsg_kscrashreport_writeStandardReport(context, - bsg_g_crashReportFilePath); + char filepath[bsg_filepath_len]; + bsg_create_filepath(bsg_g_crashReportFilePath, filepath, severity, errorClass); + bsg_kscrashreport_writeStandardReport(context, filepath); } } @@ -242,6 +285,7 @@ void bsg_kscrash_setCrashNotifyCallback( } void bsg_kscrash_reportUserException(const char *name, const char *reason, + const char *severity, const char *handledState, const char *overrides, const char *metadata, @@ -249,7 +293,7 @@ void bsg_kscrash_reportUserException(const char *name, const char *reason, const char *config, int discardDepth, bool terminateProgram) { - bsg_kscrashsentry_reportUserException(name, reason, handledState, overrides, + bsg_kscrashsentry_reportUserException(name, reason, severity, handledState, overrides, metadata, appState, config, discardDepth, terminateProgram); } diff --git a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.h b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.h index 9df924549..2aeb81cc8 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.h +++ b/Source/KSCrash/Source/KSCrash/Recording/BSG_KSCrashC.h @@ -187,6 +187,7 @@ void bsg_kscrash_setCrashNotifyCallback( * Terminate the program instead. */ void bsg_kscrash_reportUserException(const char *name, const char *reason, + const char *severity, const char *handledState, const char *overrides, const char *metadata, diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.c b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.c index a2cc2d98c..50b0bd1cd 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.c +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.c @@ -96,7 +96,7 @@ static bool bsg_g_threads_are_running = true; BSG_KSCrashType bsg_kscrashsentry_installWithContext(BSG_KSCrash_SentryContext *context, BSG_KSCrashType crashTypes, - void (*onCrash)(void)) { + void (*onCrash)(char, char *)) { if (bsg_ksmachisBeingTraced()) { if (context->reportWhenDebuggerIsAttached) { BSG_KSLOG_WARN("KSCrash: App is running in a debugger. Crash " @@ -206,7 +206,7 @@ void bsg_kscrashsentry_resumeThreads(void) { } void bsg_kscrashsentry_clearContext(BSG_KSCrash_SentryContext *context) { - void (*onCrash)(void) = context->onCrash; + void (*onCrash)(char, char *) = context->onCrash; bool threadTracingEnabled = context->threadTracingEnabled; bool reportWhenDebuggerIsAttached = context->reportWhenDebuggerIsAttached; bool suspendThreadsForUserReported = context->suspendThreadsForUserReported; diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.h b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.h index 1e8e73311..b6d51008e 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.h +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry.h @@ -51,7 +51,7 @@ typedef struct BSG_KSCrash_SentryContext { // Caller defined values. Caller must fill these out prior to installation. /** Called by the crash handler when a crash is detected. */ - void (*onCrash)(void); + void (*onCrash)(char, char[21]); /** If true, will suspend threads for user reported exceptions. */ bool suspendThreadsForUserReported; @@ -162,7 +162,7 @@ typedef struct BSG_KSCrash_SentryContext { BSG_KSCrashType bsg_kscrashsentry_installWithContext(BSG_KSCrash_SentryContext *context, BSG_KSCrashType crashTypes, - void (*onCrash)(void)); + void (*onCrash)(char, char *)); /** Uninstall crash sentry. * diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_CPPException.mm b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_CPPException.mm index f7de5f039..1456c83d2 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_CPPException.mm +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_CPPException.mm @@ -179,7 +179,9 @@ static void CPPExceptionTerminate(void) { bsg_g_context->crashReason = description; BSG_KSLOG_DEBUG(@"Calling main crash handler."); - bsg_g_context->onCrash(); + char errorClass[21]; + strncpy(errorClass, bsg_g_context->CPPException.name, sizeof(errorClass)); + bsg_g_context->onCrash('e', errorClass); BSG_KSLOG_DEBUG( @"Crash handling complete. Restoring original handlers."); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Deadlock.m b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Deadlock.m index cb7e5b0bf..29706dd53 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Deadlock.m +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Deadlock.m @@ -114,7 +114,7 @@ - (void)handleDeadlock { bsg_g_context->registersAreValid = false; BSG_KSLOG_DEBUG(@"Calling main crash handler."); - bsg_g_context->onCrash(); + bsg_g_context->onCrash('e', ""); BSG_KSLOG_DEBUG(@"Crash handling complete. Restoring original handlers."); bsg_kscrashsentry_uninstall(BSG_KSCrashTypeAll); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_MachException.c b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_MachException.c index 146ee01d6..5ce551ba9 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_MachException.c +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_MachException.c @@ -285,7 +285,9 @@ void *ksmachexc_i_handleExceptions(void *const userData) { bsg_g_context->mach.subcode = exceptionMessage.code[1]; BSG_KSLOG_DEBUG("Calling main crash handler."); - bsg_g_context->onCrash(); + char errorClass[21]; + strncpy(errorClass, bsg_ksmachexceptionName(bsg_g_context->mach.type), sizeof(errorClass)); + bsg_g_context->onCrash('e', errorClass); BSG_KSLOG_DEBUG( "Crash handling complete. Restoring original handlers."); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_NSException.m b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_NSException.m index b72084eb4..10a46fb0c 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_NSException.m +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_NSException.m @@ -128,7 +128,9 @@ void bsg_recordException(NSException *exception) { bsg_g_context->stackTraceLength = (int)numFrames; BSG_KSLOG_DEBUG(@"Calling main crash handler."); - bsg_g_context->onCrash(); + char errorClass[21]; + strncpy(errorClass, bsg_g_context->NSException.name, sizeof(errorClass)); + bsg_g_context->onCrash('e', errorClass); } } diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Signal.c b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Signal.c index 51bf8a2ea..1cc26f8eb 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Signal.c +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_Signal.c @@ -107,7 +107,18 @@ void bsg_kssighndl_i_handleSignal(int sigNum, siginfo_t *signalInfo, bsg_g_context->signal.signalInfo = signalInfo; BSG_KSLOG_DEBUG("Calling main crash handler."); - bsg_g_context->onCrash(); + char errorClass[21]; + const char *sigName = bsg_kssignal_signalName(sigNum); + if (sigName != NULL) { + for (int i = 0; i < sizeof(errorClass); i++) { + char c = sigName[i]; + if (c == '\0') { + break; + } + errorClass[i] = c; + } + } + bsg_g_context->onCrash('e', errorClass); BSG_KSLOG_DEBUG( "Crash handling complete. Restoring original handlers."); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.c b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.c index fddd69917..aa247955c 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.c +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.c @@ -48,6 +48,7 @@ void bsg_kscrashsentry_uninstallUserExceptionHandler(void) { } void bsg_kscrashsentry_reportUserException(const char *name, const char *reason, + const char *severity, const char *handledState, const char *overrides, const char *metadata, @@ -92,7 +93,9 @@ void bsg_kscrashsentry_reportUserException(const char *name, const char *reason, bsg_g_context->userException.state = appState; BSG_KSLOG_DEBUG("Calling main crash handler."); - bsg_g_context->onCrash(); + char errorClass[21]; + strncpy(errorClass, bsg_g_context->userException.name, sizeof(errorClass)); + bsg_g_context->onCrash(strlen(severity) > 0 ? severity[0] : 'w', errorClass); if (terminateProgram) { bsg_kscrashsentry_uninstall(BSG_KSCrashTypeAll); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.h b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.h index 103f33823..f68f583e9 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.h +++ b/Source/KSCrash/Source/KSCrash/Recording/Sentry/BSG_KSCrashSentry_User.h @@ -67,6 +67,7 @@ void bsg_kscrashsentry_uninstallUserExceptionHandler(void); * Terminate the program instead. */ void bsg_kscrashsentry_reportUserException(const char *name, const char *reason, + const char *severity, const char *handledState, const char *overrides, const char *metadata, diff --git a/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.h b/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.h index d212ea4c7..bc0471720 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.h +++ b/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.h @@ -29,10 +29,10 @@ /** Conditionally call a completion method if it's not nil. * * @param onCompletion The completion block. If nil, this function does nothing. - * @param filteredReports The parameter to send as "filteredReports". + * @param sentReportCount The number of reports successfully handled. * @param completed The parameter to send as "completed". * @param error The parameter to send as "error". */ void bsg_kscrash_i_callCompletion( - BSG_KSCrashReportFilterCompletion onCompletion, NSArray *filteredReports, + BSG_KSCrashReportFilterCompletion onCompletion, NSUInteger sentReportCount, BOOL completed, NSError *error); diff --git a/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.m b/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.m index 7a7f2027d..181bdbb02 100644 --- a/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.m +++ b/Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSCrashCallCompletion.m @@ -27,9 +27,9 @@ #import "BSG_KSCrashCallCompletion.h" void bsg_kscrash_i_callCompletion( - BSG_KSCrashReportFilterCompletion onCompletion, NSArray *filteredReports, + BSG_KSCrashReportFilterCompletion onCompletion, NSUInteger reportCount, BOOL completed, NSError *error) { if (onCompletion) { - onCompletion(filteredReports, completed, error); + onCompletion(reportCount, completed, error); } } diff --git a/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilter.h b/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilter.h index f09806c51..7f73fa20c 100644 --- a/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilter.h +++ b/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilter.h @@ -37,7 +37,7 @@ * @param reports The reports to process. * @param onCompletion Block to call when processing is complete. */ -- (void)filterReports:(NSArray *)reports +- (void)filterReports:(NSDictionary *)reports onCompletion:(BSG_KSCrashReportFilterCompletion)onCompletion; @end diff --git a/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilterCompletion.h b/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilterCompletion.h index 9462707dd..19177ad85 100644 --- a/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilterCompletion.h +++ b/Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilterCompletion.h @@ -28,13 +28,12 @@ /** Callback for filter operations. * - * @param filteredReports The filtered reports (may be incomplete if "completed" - * is false). + * @param sentReportCount The number of reports successfully sent. * @param completed True if filtering completed. * Can be false due to a non-erroneous condition (such as a * user cancelling the operation). * @param error Non-nil if an error occurred. */ -typedef void (^BSG_KSCrashReportFilterCompletion)(NSArray *filteredReports, +typedef void (^BSG_KSCrashReportFilterCompletion)(NSUInteger sentReportCount, BOOL completed, NSError *error); diff --git a/Tests/BSGFilepathTests.m b/Tests/BSGFilepathTests.m new file mode 100644 index 000000000..31416eec1 --- /dev/null +++ b/Tests/BSGFilepathTests.m @@ -0,0 +1,64 @@ +// +// BSGFilepathTests.m +// Tests +// +// Created by Delisa on 2/19/19. +// Copyright © 2019 Bugsnag. All rights reserved. +// + +#import +#include + + +int bsg_create_filepath(char *base, char filepath[512], char severity, char error_class[21]); + +@interface BSGFilepathTests : XCTestCase + +@end + +@implementation BSGFilepathTests + +- (void)testEncodeCharacters { + char *base = "/path/to/it/imagine this is a UUID.json"; + char filepath[512]; + bsg_create_filepath(base, filepath, 'w', "😃 HappyError"); + XCTAssertEqual(0, strcmp(filepath, "/path/to/it/imagine this is a UUID-w-u- HappyError.json")); +} + +- (void)testTruncateUnicodeCharacters { + char *base = "/path/to/it/imagine this is a UUID.json"; + char filepath[512]; + // The char limit is not on a character boundary + bsg_create_filepath(base, filepath, 'e', "AnExtremelyLongLong😃"); + XCTAssertEqual(0, strcmp(filepath, "/path/to/it/imagine this is a UUID-e-u-AnExtremelyLongLong.json")); +} + +- (void)testNullErrorClass { + char *base = "/path/to/it/imagine this is a UUID.json"; + char filepath[512]; + bsg_create_filepath(base, filepath, 'e', NULL); + XCTAssertEqual(0, strcmp(filepath, "/path/to/it/imagine this is a UUID-e-u-.json")); +} + +- (void)testEmptyErrorClass { + char *base = "/path/to/it/imagine this is a UUID.json"; + char filepath[512]; + bsg_create_filepath(base, filepath, 'e', ""); + XCTAssertEqual(0, strcmp(filepath, "/path/to/it/imagine this is a UUID-e-u-.json")); +} + +- (void)testEmptyErrorClassFromUnicode { + char *base = "/path/to/it/imagine this is a UUID.json"; + char filepath[512]; + bsg_create_filepath(base, filepath, 'e', "🀦🀨🁺😃"); + XCTAssertEqual(0, strcmp(filepath, "/path/to/it/imagine this is a UUID-e-u-.json")); +} + +- (void)testErrorClassLength { + char *base = "imagine this is a UUID.json"; + char filepath[512]; + bsg_create_filepath(base, filepath, 'i', "AnExtremelyLongLongErrorNameOmg"); + XCTAssertEqual(0, strcmp(filepath, "imagine this is a UUID-i-u-AnExtremelyLongLongEr.json")); +} + +@end diff --git a/Tests/BugsnagCrashReportTests.m b/Tests/BugsnagCrashReportTests.m index 54cfebbbd..b69fb9bd4 100644 --- a/Tests/BugsnagCrashReportTests.m +++ b/Tests/BugsnagCrashReportTests.m @@ -14,6 +14,7 @@ #import "BugsnagHandledState.h" #import "BugsnagSession.h" + @interface BugsnagCrashReportTests : XCTestCase @property BugsnagCrashReport *report; @end @@ -178,6 +179,45 @@ - (void)testDefaultErrorMessageNil { payload[@"exceptions"][0][@"message"]); } +- (void)testIncomplete { + XCTAssertTrue([[[BugsnagCrashReport alloc] initWithKSReport:@{}] isIncomplete]); + XCTAssertFalse([[[BugsnagCrashReport alloc] initWithKSReport:@{@"foo": @"bar"}] isIncomplete]); +} + +- (void)testFallbackValues { + BugsnagCrashReport *report = + [[BugsnagCrashReport alloc] initWithKSReport:@{} fileMetadata:@"w-h-SomeErr thing"]; + XCTAssertTrue([report isIncomplete]); + NSDictionary *payload = [report toJson]; + XCTAssertEqualObjects(@"SomeErr thing", payload[@"exceptions"][0][@"errorClass"]); + XCTAssertEqualObjects(@"warning", payload[@"severity"]); + XCTAssertEqualObjects(@NO, payload[@"unhandled"]); +} + +- (void)testUnneededFallbackValues { + BugsnagHandledState *state = [BugsnagHandledState handledStateWithSeverityReason:UserCallbackSetSeverity + severity:BSGSeverityInfo + attrValue:nil]; + NSDictionary *dict = @{@"user.handledState": [state toJson]}; + BugsnagCrashReport *report = + [[BugsnagCrashReport alloc] initWithKSReport:dict fileMetadata:@"w-h-SomeErr thing"]; + XCTAssertFalse([report isIncomplete]); + NSDictionary *payload = [report toJson]; + XCTAssertEqualObjects(@"SomeErr thing", payload[@"exceptions"][0][@"errorClass"]); + XCTAssertEqualObjects(@"info", payload[@"severity"]); + XCTAssertEqualObjects(@NO, payload[@"unhandled"]); +} + +- (void)testUnhandledFallbackValues { + BugsnagCrashReport *report = + [[BugsnagCrashReport alloc] initWithKSReport:@{} fileMetadata:@"foofoo-e-u-SomeErr thing"]; + XCTAssertTrue([report isIncomplete]); + NSDictionary *payload = [report toJson]; + XCTAssertEqualObjects(@"SomeErr thing", payload[@"exceptions"][0][@"errorClass"]); + XCTAssertEqualObjects(@"error", payload[@"severity"]); + XCTAssertEqualObjects(@YES, payload[@"unhandled"]); +} + - (void)testDefaultErrorMessageNilForEmptyThreads { BugsnagCrashReport *report = [[BugsnagCrashReport alloc] initWithKSReport:@{ @"threads" : @[] @@ -533,5 +573,4 @@ - (void)testAppVersionOverride { XCTAssertEqualObjects(@"1.2.3", dictionary[@"app"][@"version"]); } - @end diff --git a/Tests/KSCrash/KSCrashSentry_Tests.m b/Tests/KSCrash/KSCrashSentry_Tests.m index 6c0f1829a..1b165b79d 100755 --- a/Tests/KSCrash/KSCrashSentry_Tests.m +++ b/Tests/KSCrash/KSCrashSentry_Tests.m @@ -30,7 +30,7 @@ #import "BSG_KSCrashSentry.h" #import "BSG_KSCrashSentry_Private.h" -static void onCrash(void) +static void onCrash(char severity, char *errorClass) { // Do nothing } diff --git a/features/crashprobe.feature b/features/crashprobe.feature index d119c9612..d8b054959 100644 --- a/features/crashprobe.feature +++ b/features/crashprobe.feature @@ -218,3 +218,15 @@ Scenario: Access a non-object as an object And the exception "message" equals "Attempted to dereference garbage pointer 0x10." And the exception "errorClass" equals "EXC_BAD_ACCESS" And the "method" of stack frame 0 equals "objc_msgSend" + +Scenario: Crash report file corruption + When I set environment variable "BUGSNAG_API_KEY" to "a35a2a72bd230ac0aa0f52715bbdc6aa" + And I crash the app using "AccessNonObjectScenario" + And I corrupt all reports on disk + And I relaunch the app + Then I should receive a request + And the request is a valid for the error reporting API + And the exception "errorClass" equals "EXC_BAD_ACCESS" + And the event "unhandled" is true + And the event "incomplete" is true + And the event "severity" equals "error" diff --git a/features/steps/ios_steps.rb b/features/steps/ios_steps.rb index 9766630f3..e71eda56f 100644 --- a/features/steps/ios_steps.rb +++ b/features/steps/ios_steps.rb @@ -43,6 +43,15 @@ assert_not_equal(value1, value2) end +When("I corrupt all reports on disk") do + app_path = `xcrun simctl get_app_container booted com.bugsnag.iOSTestApp`.chomp + app_path.gsub!(/(.*Containers).*/, '\1') + files = Dir.glob("#{app_path}/**/KSCrashReports/iOSTestApp/*.json") + files.each do |path| + File.open(path, 'w') {|file| file.truncate(0) } + end +end + Then("each event in the payload for request {int} matches one of:") do |request_index, table| events = read_key_path(find_request(request_index)[:body], "events") table.hashes.each do |values| diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index d8cc3ea44..e9d60fcb2 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8A12006A221C36420008C9C3 /* BSGFilepathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A120069221C36420008C9C3 /* BSGFilepathTests.m */; }; 8A2C8F231C6BBD2300846019 /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C8F181C6BBD2300846019 /* Bugsnag.framework */; }; 8A2C8F4F1C6BBE3C00846019 /* Bugsnag.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8F3D1C6BBE3C00846019 /* Bugsnag.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8F501C6BBE3C00846019 /* Bugsnag.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8F3E1C6BBE3C00846019 /* Bugsnag.m */; }; @@ -410,6 +411,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8A120069221C36420008C9C3 /* BSGFilepathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BSGFilepathTests.m; path = ../../Tests/BSGFilepathTests.m; sourceTree = ""; }; 8A2C8F181C6BBD2300846019 /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8A2C8F1D1C6BBD2300846019 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; }; 8A2C8F221C6BBD2300846019 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -732,6 +734,7 @@ F429554A50F3ABE60537F70E /* BugsnagKSCrashSysInfoParserTest.m */, F42954B7D892334E7551F0F3 /* RegisterErrorDataTest.m */, F429551527EAE3AFE1F605FE /* BugsnagThreadTest.m */, + 8A120069221C36420008C9C3 /* BSGFilepathTests.m */, ); name = Tests; path = BugsnagTests; @@ -1244,6 +1247,7 @@ E78C1EF11FCC2F1700B976D3 /* BugsnagSessionTrackerTest.m in Sources */, F4295995C3259BF7D9730BC4 /* BugsnagKSCrashSysInfoParserTest.m in Sources */, F4295F017754324FD52CCE46 /* RegisterErrorDataTest.m in Sources */, + 8A12006A221C36420008C9C3 /* BSGFilepathTests.m in Sources */, F42952D83435C02F8D891C40 /* BugsnagThreadTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/tvOS/Bugsnag.xcodeproj/project.pbxproj b/tvOS/Bugsnag.xcodeproj/project.pbxproj index 5f1f9aeb8..e1d3c867b 100644 --- a/tvOS/Bugsnag.xcodeproj/project.pbxproj +++ b/tvOS/Bugsnag.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 8A12006E221C51550008C9C3 /* BSGFilepathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A12006D221C51550008C9C3 /* BSGFilepathTests.m */; }; 8A48EF291EAA824100B70024 /* BugsnagLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A48EF281EAA824100B70024 /* BugsnagLogger.h */; }; 8A627CD51EC3B69300F7C04E /* BSGSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A627CD31EC3B69300F7C04E /* BSGSerialization.h */; }; 8A627CD61EC3B69300F7C04E /* BSGSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A627CD41EC3B69300F7C04E /* BSGSerialization.m */; }; @@ -187,6 +188,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 8A12006D221C51550008C9C3 /* BSGFilepathTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSGFilepathTests.m; path = ../../Tests/BSGFilepathTests.m; sourceTree = ""; }; 8A48EF281EAA824100B70024 /* BugsnagLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagLogger.h; path = ../Source/BugsnagLogger.h; sourceTree = ""; }; 8A627CD31EC3B69300F7C04E /* BSGSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSGSerialization.h; path = ../Source/BSGSerialization.h; sourceTree = ""; }; 8A627CD41EC3B69300F7C04E /* BSGSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSGSerialization.m; path = ../Source/BSGSerialization.m; sourceTree = ""; }; @@ -465,6 +467,7 @@ isa = PBXGroup; children = ( E7CE78D71FD94EF1001D07E0 /* KSCrash */, + 8A12006D221C51550008C9C3 /* BSGFilepathTests.m */, E791488C1FD82E77003EFEBF /* BugsnagSessionTest.m */, E791488A1FD82E77003EFEBF /* BugsnagSessionTrackerTest.m */, E791488B1FD82E77003EFEBF /* BugsnagSessionTrackingPayloadTest.m */, @@ -941,6 +944,7 @@ E7CE79051FD94F1B001D07E0 /* KSDynamicLinker_Tests.m in Sources */, E7CE78F61FD94F1B001D07E0 /* FileBasedTestCase.m in Sources */, E7CE79031FD94F1B001D07E0 /* KSCrashReportConverter_Tests.m in Sources */, + 8A12006E221C51550008C9C3 /* BSGFilepathTests.m in Sources */, E79148911FD82E77003EFEBF /* BugsnagUserTest.m in Sources */, E791488E1FD82E77003EFEBF /* BugsnagSessionTrackerTest.m in Sources */, E79148901FD82E77003EFEBF /* BugsnagSessionTest.m in Sources */,