diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index 4d14f9cfd..18eadd340 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -679,6 +679,16 @@ 01840B7325DC26E200F95648 /* BSGEventUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 01840B6E25DC26E200F95648 /* BSGEventUploader.m */; }; 01840B7425DC26E200F95648 /* BSGEventUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 01840B6E25DC26E200F95648 /* BSGEventUploader.m */; }; 01840B7525DC26E200F95648 /* BSGEventUploader.m in Sources */ = {isa = PBXBuildFile; fileRef = 01840B6E25DC26E200F95648 /* BSGEventUploader.m */; }; + 01847D962644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */; }; + 01847D972644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */; }; + 01847D982644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */; }; + 01847D992644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */; }; + 01847D9A2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */; }; + 01847D9B2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */; }; + 01847D9C2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */; }; + 01847DAC26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DAB26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m */; }; + 01847DAD26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DAB26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m */; }; + 01847DAE26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DAB26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m */; }; 0187D464255BD7B800C503D9 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 019480D42625F3EB00E833ED /* BSGAppKitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 019480D32625F3EB00E833ED /* BSGAppKitTests.m */; }; 01B14C56251CE55F00118748 /* report-react-native-promise-rejection.json in Resources */ = {isa = PBXBuildFile; fileRef = 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */; }; @@ -1321,6 +1331,9 @@ 017824BD262D65A000D18AFA /* Bugsnag.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Bugsnag.xcconfig; sourceTree = ""; }; 01840B6D25DC26E200F95648 /* BSGEventUploader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGEventUploader.h; sourceTree = ""; }; 01840B6E25DC26E200F95648 /* BSGEventUploader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGEventUploader.m; sourceTree = ""; }; + 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGInternalErrorReporter.h; sourceTree = ""; }; + 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGInternalErrorReporter.m; sourceTree = ""; }; + 01847DAB26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGInternalErrorReporterTests.m; sourceTree = ""; }; 01937CF9257A7B4C00F2DE31 /* Bugsnag+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bugsnag+Private.h"; sourceTree = ""; }; 01937D09257A7ED000F2DE31 /* BugsnagSessionTracker+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagSessionTracker+Private.h"; sourceTree = ""; }; 01937D11257A814D00F2DE31 /* BugsnagMetadata+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagMetadata+Private.h"; sourceTree = ""; }; @@ -1708,6 +1721,7 @@ 00896A3F2486DBDD00DC48C2 /* BSGConfigurationBuilderTests.m */, 008966C62486D43600DC48C2 /* BSGConnectivityTest.m */, 01BDB1CE25DEBF4600A91FAF /* BSGEventUploadKSCrashReportOperationTests.m */, + 01847DAB26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m */, CBCF77AA250142E0004AF22A /* BSGJSONSerializationTests.m */, 008966C82486D43600DC48C2 /* BSGOutOfMemoryTests.m */, CB6419AA25A73E8C00613D25 /* BSGStorageMigratorTests.m */, @@ -1838,6 +1852,8 @@ 010FF28225ED2A8D00E4F2B0 /* BSGAppHangDetector.h */, 010FF28325ED2A8D00E4F2B0 /* BSGAppHangDetector.m */, 019480C42625EE9800E833ED /* BSGAppKit.h */, + 01847D942644140F00ADA4C7 /* BSGInternalErrorReporter.h */, + 01847D952644140F00ADA4C7 /* BSGInternalErrorReporter.m */, CBCF77A125010648004AF22A /* BSGJSONSerialization.h */, CBCF77A225010648004AF22A /* BSGJSONSerialization.m */, 008968112486DA5600DC48C2 /* BSGSerialization.h */, @@ -2022,6 +2038,7 @@ 0126F7AB25DD5118008483C2 /* BSGEventUploadFileOperation.h in Headers */, 3A700A9A24A63AC60068CD1B /* BSG_KSCrashReportWriter.h in Headers */, 3A700A9B24A63AC60068CD1B /* BugsnagErrorTypes.h in Headers */, + 01847D962644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */, 01840B6F25DC26E200F95648 /* BSGEventUploader.h in Headers */, 3A700A9C24A63AC60068CD1B /* BugsnagEvent.h in Headers */, 3A700A9D24A63AC60068CD1B /* BugsnagClient.h in Headers */, @@ -2125,6 +2142,7 @@ 0126F7AC25DD5118008483C2 /* BSGEventUploadFileOperation.h in Headers */, 3A700AAE24A63CFD0068CD1B /* BSG_KSCrashReportWriter.h in Headers */, 3A700AAF24A63CFD0068CD1B /* BugsnagErrorTypes.h in Headers */, + 01847D972644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */, 01840B7025DC26E200F95648 /* BSGEventUploader.h in Headers */, 3A700AB024A63CFD0068CD1B /* BugsnagEvent.h in Headers */, 3A700AB124A63CFD0068CD1B /* BugsnagClient.h in Headers */, @@ -2228,6 +2246,7 @@ 0126F7AD25DD5118008483C2 /* BSGEventUploadFileOperation.h in Headers */, 3A700AC224A63D110068CD1B /* BSG_KSCrashReportWriter.h in Headers */, 3A700AC324A63D110068CD1B /* BugsnagErrorTypes.h in Headers */, + 01847D982644140F00ADA4C7 /* BSGInternalErrorReporter.h in Headers */, 01840B7125DC26E200F95648 /* BSGEventUploader.h in Headers */, 3A700AC424A63D110068CD1B /* BugsnagEvent.h in Headers */, 3A700AC524A63D110068CD1B /* BugsnagClient.h in Headers */, @@ -2624,6 +2643,7 @@ 0126F79E25DD510E008483C2 /* BSGEventUploadObjectOperation.m in Sources */, 0089682B2486DA5600DC48C2 /* BSGSerialization.m in Sources */, 011ADCE626049A3600B20D72 /* BugsnagClient+OutOfMemory.m in Sources */, + 01847D992644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, 008968E92486DAB800DC48C2 /* BugsnagSessionFileStore.m in Sources */, 00896A172486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, 008969CF2486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, @@ -2715,6 +2735,7 @@ 01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */, 008967AB2486D43700DC48C2 /* KSMach_Tests.m in Sources */, 0089672A2486D43700DC48C2 /* BugsnagStacktraceTest.m in Sources */, + 01847DAC26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */, 0163BF5925823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */, 008967392486D43700DC48C2 /* BugsnagEventFromKSCrashReportTest.m in Sources */, 008967182486D43700DC48C2 /* BugsnagErrorTest.m in Sources */, @@ -2794,6 +2815,7 @@ 0126F79F25DD510E008483C2 /* BSGEventUploadObjectOperation.m in Sources */, 0089682C2486DA5600DC48C2 /* BSGSerialization.m in Sources */, 011ADCE726049A3600B20D72 /* BugsnagClient+OutOfMemory.m in Sources */, + 01847D9A2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, 008968EA2486DAB800DC48C2 /* BugsnagSessionFileStore.m in Sources */, 00896A182486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, 008969D02486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, @@ -2857,6 +2879,7 @@ 008967792486D43700DC48C2 /* KSMachHeader_Tests.m in Sources */, 0089675E2486D43700DC48C2 /* BugsnagSessionTrackingPayloadTest.m in Sources */, 008967A92486D43700DC48C2 /* KSCrashIdentifierTests.m in Sources */, + 01847DAD26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */, 0089672B2486D43700DC48C2 /* BugsnagStacktraceTest.m in Sources */, 008966F22486D43700DC48C2 /* BugsnagMetadataRedactionTest.m in Sources */, CBDD6D0725AC3EFF00A2E12B /* BSGStorageMigratorTests.m in Sources */, @@ -2963,6 +2986,7 @@ 0126F7A025DD510E008483C2 /* BSGEventUploadObjectOperation.m in Sources */, 0089682D2486DA5600DC48C2 /* BSGSerialization.m in Sources */, 011ADCE826049A3600B20D72 /* BugsnagClient+OutOfMemory.m in Sources */, + 01847D9B2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, 008968EB2486DAB800DC48C2 /* BugsnagSessionFileStore.m in Sources */, 00896A192486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.mm in Sources */, 008969D12486DAD100DC48C2 /* BSG_KSCrashState.m in Sources */, @@ -3058,6 +3082,7 @@ 008967772486D43700DC48C2 /* XCTestCase+KSCrash.m in Sources */, 008967322486D43700DC48C2 /* BugsnagStateEventTest.m in Sources */, CBA2249D251E429C00B87416 /* TestSupport.m in Sources */, + 01847DAE26441A5E00ADA4C7 /* BSGInternalErrorReporterTests.m in Sources */, 004E35372487AFF2007FBAE4 /* BugsnagHandledStateTest.m in Sources */, 016875C8258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */, 01C17AE92542ED7F00C102C9 /* KSCrashReportWriterTests.m in Sources */, @@ -3147,6 +3172,7 @@ 0126F79125DD508C008483C2 /* BSGEventUploadOperation.m in Sources */, 008967EB2486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 008967D62486DA2D00DC48C2 /* BugsnagEndpointConfiguration.m in Sources */, + 01847D9C2644140F00ADA4C7 /* BSGInternalErrorReporter.m in Sources */, 00AD1F262486A17900A27979 /* Bugsnag.m in Sources */, 0127149825F6171000D3500A /* BugsnagClient+AppHangs.m in Sources */, 008968CE2486DA9600DC48C2 /* BugsnagThread.m in Sources */, diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 98102d376..98375da82 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -31,6 +31,7 @@ #import "BSGConnectivity.h" #import "BSGEventUploader.h" #import "BSGFileLocations.h" +#import "BSGInternalErrorReporter.h" #import "BSGJSONSerialization.h" #import "BSGNotificationBreadcrumbs.h" #import "BSGSerialization.h" @@ -219,7 +220,7 @@ void BSGWriteSessionCrashData(BugsnagSession *session) { // MARK: - BugsnagClient // ============================================================================= -@interface BugsnagClient () +@interface BugsnagClient () @property (nonatomic) BSGNotificationBreadcrumbs *notificationBreadcrumbs; @@ -318,6 +319,8 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { if (self.user.id == nil) { // populate with an autogenerated ID if no value set [self setUser:[BSG_KSSystemInfo deviceAndAppHash] withEmail:configuration.user.email andName:configuration.user.name]; } + + BSGInternalErrorReporter.sharedInstance = [[BSGInternalErrorReporter alloc] initWithDataSource:self]; } return self; } diff --git a/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m b/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m index 493f76c84..7028778de 100644 --- a/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m +++ b/Bugsnag/Delivery/BSGEventUploadKSCrashReportOperation.m @@ -8,6 +8,7 @@ #import "BSGEventUploadKSCrashReportOperation.h" +#import "BSGInternalErrorReporter.h" #import "BSGJSONSerialization.h" #import "BSG_KSCrashDoctor.h" #import "BSG_KSCrashReportFields.h" @@ -22,17 +23,41 @@ @implementation BSGEventUploadKSCrashReportOperation - (BugsnagEvent *)loadEventAndReturnError:(NSError * __autoreleasing *)errorPtr { - id json = [BSGJSONSerialization JSONObjectWithContentsOfFile:self.file options:0 error:errorPtr]; - if (!json) { + NSError *error = nil; + + NSData *data = [NSData dataWithContentsOfFile:self.file options:0 error:&error]; + if (!data) { + [BSGInternalErrorReporter.sharedInstance reportErrorWithClass:@"File reading error" + message:BSGErrorDescription(error) + diagnostics:error.userInfo]; + if (errorPtr) { + *errorPtr = error; + } return nil; } - json = [self fixupCrashReport:json]; + id json = [BSGJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (!json) { + NSMutableDictionary *diagnostics = [NSMutableDictionary dictionary]; + diagnostics[@"data"] = [data base64EncodedStringWithOptions:0]; + [BSGInternalErrorReporter.sharedInstance reportErrorWithClass:@"JSON parsing error" + message:BSGErrorDescription(error) + diagnostics:diagnostics]; + if (errorPtr) { + *errorPtr = error; + } + return nil; + } + + NSDictionary *crashReport = [self fixupCrashReport:json]; + if (!crashReport) { + [BSGInternalErrorReporter.sharedInstance reportErrorWithClass:@"Unexpected JSON payload" + message:@"-fixupCrashReport: returned nil" + diagnostics:@{@"json": json}]; return nil; } - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:json]; + BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:crashReport]; if (!event.app.type) { // Use current value for crashes from older notifier versions that didn't persist config.appType diff --git a/Bugsnag/Delivery/BugsnagApiClient.h b/Bugsnag/Delivery/BugsnagApiClient.h index b487b48a1..05016f72f 100644 --- a/Bugsnag/Delivery/BugsnagApiClient.h +++ b/Bugsnag/Delivery/BugsnagApiClient.h @@ -40,7 +40,7 @@ typedef NS_ENUM(NSInteger, BugsnagApiClientDeliveryStatus) { toURL:(NSURL *)url completionHandler:(void (^)(BugsnagApiClientDeliveryStatus status, NSError * _Nullable error))completionHandler; -- (NSString *)SHA1HashStringWithData:(NSData *)data; ++ (NSString *)SHA1HashStringWithData:(NSData *)data; @property (readonly, nonatomic) NSOperationQueue *sendQueue; diff --git a/Bugsnag/Delivery/BugsnagApiClient.m b/Bugsnag/Delivery/BugsnagApiClient.m index e3ede19bb..e68a55aa1 100644 --- a/Bugsnag/Delivery/BugsnagApiClient.m +++ b/Bugsnag/Delivery/BugsnagApiClient.m @@ -90,7 +90,7 @@ - (void)sendJSONPayload:(NSDictionary *)payload } NSMutableDictionary *mutableHeaders = [headers mutableCopy]; - mutableHeaders[BugsnagHTTPHeaderNameIntegrity] = [NSString stringWithFormat:@"sha1 %@", [self SHA1HashStringWithData:data]]; + mutableHeaders[BugsnagHTTPHeaderNameIntegrity] = [NSString stringWithFormat:@"sha1 %@", [BugsnagApiClient SHA1HashStringWithData:data]]; NSMutableURLRequest *request = [self prepareRequest:url headers:mutableHeaders]; bsg_log_debug(@"Sending %lu byte payload to %@", (unsigned long)data.length, url); @@ -150,7 +150,7 @@ - (NSMutableURLRequest *)prepareRequest:(NSURL *)url return request; } -- (NSString *)SHA1HashStringWithData:(NSData *)data { ++ (NSString *)SHA1HashStringWithData:(NSData *)data { if (!data) { return nil; } diff --git a/Bugsnag/Helpers/BSGInternalErrorReporter.h b/Bugsnag/Helpers/BSGInternalErrorReporter.h new file mode 100644 index 000000000..881640bcc --- /dev/null +++ b/Bugsnag/Helpers/BSGInternalErrorReporter.h @@ -0,0 +1,66 @@ +// +// BSGInternalErrorReporter.h +// Bugsnag +// +// Created by Nick Dowell on 06/05/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import + +@class BugsnagAppWithState; +@class BugsnagConfiguration; +@class BugsnagDeviceWithState; +@class BugsnagEvent; +@class BugsnagNotifier; + +NS_ASSUME_NONNULL_BEGIN + +/// Returns a concise desription of the error including its domain, code, and debug description or localizedDescription. +FOUNDATION_EXPORT NSString *BSGErrorDescription(NSError *error); + +// MARK: - + +@protocol BSGInternalErrorReporterDataSource + +@property (readonly, nonatomic) BugsnagConfiguration *configuration; + +@property (readonly, nonatomic) BugsnagNotifier *notifier; + +- (BugsnagAppWithState *)generateAppWithState:(NSDictionary *)systemInfo; + +- (BugsnagDeviceWithState *)generateDeviceWithState:(NSDictionary *)systemInfo; + +@end + +// MARK: - + +@interface BSGInternalErrorReporter : NSObject + +@property (class, nonatomic) BSGInternalErrorReporter *sharedInstance; + +- (instancetype)initWithDataSource:(id)dataSource NS_DESIGNATED_INITIALIZER; + +- (instancetype)init UNAVAILABLE_ATTRIBUTE; + +/// Reports an error to Bugsnag's internal bugsnag-cocoa project dashboard. +/// @param errorClass The class of error which occurred. This field is used to group the errors together so should not contain any contextual +/// information that would prevent correct grouping. This would ordinarily be the Exception name when dealing with an exception. +/// @param message The error message associated with the error. Usually this will contain some information about this specific instance of the error +/// and is not used to group the errors. +/// @param diagnostics JSON compatible information to include in the `BugsnagDiagnostics` metadata section. +- (void)reportErrorWithClass:(NSString *)errorClass + message:(nullable NSString *)message + diagnostics:(nullable NSDictionary *)diagnostics; + +// Private + +- (nullable BugsnagEvent *)eventWithErrorClass:(NSString *)errorClass + message:(nullable NSString *)message + diagnostics:(nullable NSDictionary *)diagnostics; + +- (nullable NSURLRequest *)requestForEvent:(BugsnagEvent *)event error:(NSError * __autoreleasing *)errorPtr; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Helpers/BSGInternalErrorReporter.m b/Bugsnag/Helpers/BSGInternalErrorReporter.m new file mode 100644 index 000000000..a5aa6beaa --- /dev/null +++ b/Bugsnag/Helpers/BSGInternalErrorReporter.m @@ -0,0 +1,177 @@ +// +// BSGInternalErrorReporter.m +// Bugsnag +// +// Created by Nick Dowell on 06/05/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import "BSGInternalErrorReporter.h" + +#import "BSG_KSSystemInfo.h" +#import "BSG_RFC3339DateTool.h" +#import "BugsnagApiClient.h" +#import "BugsnagAppWithState+Private.h" +#import "BugsnagCollections.h" +#import "BugsnagConfiguration+Private.h" +#import "BugsnagDeviceWithState+Private.h" +#import "BugsnagError+Private.h" +#import "BugsnagEvent+Private.h" +#import "BugsnagHandledState.h" +#import "BugsnagKeys.h" +#import "BugsnagLogger.h" +#import "BugsnagMetadata+Private.h" +#import "BugsnagNotifier.h" +#import "BugsnagStackframe+Private.h" +#import "BugsnagUser+Private.h" + +static NSString * const EventPayloadVersion = @"4.0"; + +static NSString * const BugsnagDiagnosticsKey = @"BugsnagDiagnostics"; + +BugsnagHTTPHeaderName const BugsnagHTTPHeaderNameInternalError = @"Bugsnag-Internal-Error"; + + +NSString *BSGErrorDescription(NSError *error) { + return [NSString stringWithFormat:@"%@ %ld: %@", error.domain, (long)error.code, + error.userInfo[NSDebugDescriptionErrorKey] ?: error.localizedDescription]; +} + + +// MARK: - + +@interface BSGInternalErrorReporter () + +@property (weak, nullable, nonatomic) id dataSource; +@property (nonatomic) NSURLSession *session; + +@end + + +@implementation BSGInternalErrorReporter + +static BSGInternalErrorReporter *sharedInstance_; + ++ (BSGInternalErrorReporter *)sharedInstance { + return sharedInstance_; +} + ++ (void)setSharedInstance:(BSGInternalErrorReporter *)sharedInstance { + sharedInstance_ = sharedInstance; +} + +- (instancetype)initWithDataSource:(id)dataSource { + if ((self = [super init])) { + _dataSource = dataSource; + _session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration]; + } + return self; +} + +// MARK: Public API + +- (void)reportErrorWithClass:(NSString *)errorClass + message:(nullable NSString *)message + diagnostics:(nullable NSDictionary *)diagnostics { + @try { + BugsnagEvent *event = [self eventWithErrorClass:errorClass message:message diagnostics:diagnostics]; + if (event) { + [self sendEvent:event]; + } + } @catch (NSException *exception) { + bsg_log_err(@"%@", exception); + } +} + +// MARK: Private API + +- (nullable BugsnagEvent *)eventWithErrorClass:(NSString *)errorClass + message:(nullable NSString *)message + diagnostics:(nullable NSDictionary *)diagnostics { + id dataSource = self.dataSource; + if (!dataSource) { + return nil; + } + + BugsnagMetadata *metadata = [[BugsnagMetadata alloc] init]; + if (diagnostics) { + [metadata addMetadata:(NSDictionary * _Nonnull)diagnostics toSection:BugsnagDiagnosticsKey]; + } + [metadata addMetadata:dataSource.configuration.apiKey withKey:BSGKeyApiKey toSection:BugsnagDiagnosticsKey]; + + NSArray *stacktrace = [BugsnagStackframe stackframesWithCallStackReturnAddresses: + BSGArraySubarrayFromIndex(NSThread.callStackReturnAddresses, 2)]; + + BugsnagError *error = + [[BugsnagError alloc] initWithErrorClass:errorClass + errorMessage:message + errorType:BSGErrorTypeCocoa + stacktrace:stacktrace]; + + NSDictionary *systemInfo = [BSG_KSSystemInfo systemInfo]; + + BugsnagEvent *event = + [[BugsnagEvent alloc] initWithApp:[dataSource generateAppWithState:systemInfo] + device:[dataSource generateDeviceWithState:systemInfo] + handledState:[BugsnagHandledState handledStateWithSeverityReason:HandledError] + user:[[BugsnagUser alloc] init] + metadata:metadata + breadcrumbs:@[] + errors:@[error] + threads:@[] + session:nil]; + + return event; +} + +- (NSURLRequest *)requestForEvent:(nonnull BugsnagEvent *)event error:(NSError * __autoreleasing *)errorPtr { + id dataSource = self.dataSource; + if (!dataSource) { + return nil; + } + + NSURL *url = dataSource.configuration.notifyURL; + if (!url) { + if (errorPtr) { + *errorPtr = [NSError errorWithDomain:@"BugsnagConfigurationErrorDomain" code:0 + userInfo:@{NSLocalizedDescriptionKey: @"Missing notify URL"}]; + } + return nil; + } + + NSMutableDictionary *requestPayload = [NSMutableDictionary dictionary]; + requestPayload[BSGKeyEvents] = @[[event toJsonWithRedactedKeys:nil]]; + requestPayload[BSGKeyNotifier] = [dataSource.notifier toDict]; + requestPayload[BSGKeyPayloadVersion] = EventPayloadVersion; + + NSData *data = [NSJSONSerialization dataWithJSONObject:requestPayload options:0 error:errorPtr]; + if (!data) { + return nil; + } + + NSMutableDictionary *headers = [NSMutableDictionary dictionary]; + headers[@"Content-Type"] = @"application/json"; + headers[BugsnagHTTPHeaderNameIntegrity] = [NSString stringWithFormat:@"sha1 %@", [BugsnagApiClient SHA1HashStringWithData:data]]; + headers[BugsnagHTTPHeaderNameInternalError] = @"bugsnag-cocoa"; + headers[BugsnagHTTPHeaderNamePayloadVersion] = EventPayloadVersion; + headers[BugsnagHTTPHeaderNameSentAt] = [BSG_RFC3339DateTool stringFromDate:[NSDate date]]; + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.allHTTPHeaderFields = headers; + request.HTTPBody = data; + request.HTTPMethod = @"POST"; + + return request; +} + +- (void)sendEvent:(nonnull BugsnagEvent *)event { + NSError *error = nil; + NSURLRequest *request = [self requestForEvent:event error:&error]; + if (!request) { + bsg_log_err(@"%@", error); + return; + } + [[self.session dataTaskWithRequest:request] resume]; +} + +@end diff --git a/Bugsnag/Payload/BugsnagError+Private.h b/Bugsnag/Payload/BugsnagError+Private.h index a9ec9ebf5..1aba3cb7f 100644 --- a/Bugsnag/Payload/BugsnagError+Private.h +++ b/Bugsnag/Payload/BugsnagError+Private.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithKSCrashReport:(NSDictionary *)event stacktrace:(NSArray *)stacktrace; - (instancetype)initWithErrorClass:(NSString *)errorClass - errorMessage:(NSString *)errorMessage + errorMessage:(nullable NSString *)errorMessage errorType:(BSGErrorType)errorType stacktrace:(nullable NSArray *)stacktrace; diff --git a/Tests/BSGInternalErrorReporterTests.m b/Tests/BSGInternalErrorReporterTests.m new file mode 100644 index 000000000..d5d2001a3 --- /dev/null +++ b/Tests/BSGInternalErrorReporterTests.m @@ -0,0 +1,81 @@ +// +// BSGInternalErrorReporterTests.m +// Bugsnag +// +// Created by Nick Dowell on 06/05/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import + +#import + +#import "BSGInternalErrorReporter.h" +#import "BugsnagEvent+Private.h" +#import "BugsnagNotifier.h" + +@interface BSGInternalErrorReporterTests : XCTestCase + +@property (nonatomic) BugsnagConfiguration *configuration; +@property (nonatomic) BugsnagNotifier *notifier; + +@end + +@implementation BSGInternalErrorReporterTests + +- (void)setUp { + self.configuration = [[BugsnagConfiguration alloc] initWithApiKey:@"0192837465afbecd0192837465afbecd"]; + self.notifier = [[BugsnagNotifier alloc] init]; +} + +- (void)testEventForError { + BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:@"0192837465afbecd0192837465afbecd"]; + BSGInternalErrorReporter *reporter = [[BSGInternalErrorReporter alloc] initWithDataSource:self]; + + BugsnagEvent *event = [reporter eventWithErrorClass:@"Internal error" message:@"Something went wrong" diagnostics:@{}]; + XCTAssertEqualObjects(event.errors[0].errorClass, @"Internal error"); + XCTAssertEqualObjects(event.errors[0].errorMessage, @"Something went wrong"); + XCTAssertNil(event.apiKey); + + NSDictionary *diagnostics = [event.metadata getMetadataFromSection:@"BugsnagDiagnostics"]; + XCTAssertEqualObjects(diagnostics[@"apiKey"], configuration.apiKey); +} + +- (void)testRequestForEvent { + self.configuration.endpoints.notify = @"https://notify.example.com"; + // endpoints.notify is only applied to notifyURL when the configuration is copied o_0 + self.configuration = [self.configuration copy]; + + BugsnagNotifier *notifier = [[BugsnagNotifier alloc] init]; + BSGInternalErrorReporter *reporter = [[BSGInternalErrorReporter alloc] initWithDataSource:self]; + + BugsnagEvent *event = [[BugsnagEvent alloc] init]; + + NSURLRequest *request = [reporter requestForEvent:event error:NULL]; + XCTAssertEqualObjects(request.URL, [NSURL URLWithString:self.configuration.endpoints.notify]); + XCTAssertEqualObjects(request.HTTPMethod, @"POST"); + + XCTAssertEqualObjects([request valueForHTTPHeaderField:@"Bugsnag-Internal-Error"], @"bugsnag-cocoa"); + XCTAssertNil([request valueForHTTPHeaderField:@"Bugsnag-Api-Key"]); + XCTAssertNil([request valueForHTTPHeaderField:@"Bugsnag-Stacktrace-Types"]); + XCTAssertNotNil([request valueForHTTPHeaderField:@"Bugsnag-Integrity"]); + XCTAssertNotNil([request valueForHTTPHeaderField:@"Bugsnag-Sent-At"]); + + NSDictionary *payload = [NSJSONSerialization JSONObjectWithData:(NSData * _Nonnull)request.HTTPBody options:0 error:NULL]; + XCTAssertEqualObjects(payload[@"events"], @[[event toJsonWithRedactedKeys:nil]]); + XCTAssertEqualObjects(payload[@"notifier"], [notifier toDict]); + XCTAssertEqualObjects(payload[@"payloadVersion"], @"4.0"); + XCTAssertNil(payload[@"apiKey"]); +} + +// MARK: - BSGInternalErrorReporterDataSource + +- (BugsnagAppWithState *)generateAppWithState:(nonnull NSDictionary *)systemInfo { + return [[BugsnagAppWithState alloc] init]; +} + +- (BugsnagDeviceWithState *)generateDeviceWithState:(nonnull NSDictionary *)systemInfo { + return [[BugsnagDeviceWithState alloc] init]; +} + +@end diff --git a/Tests/BugsnagApiClientTest.m b/Tests/BugsnagApiClientTest.m index c5e8a7ab2..38b7c8f75 100644 --- a/Tests/BugsnagApiClientTest.m +++ b/Tests/BugsnagApiClientTest.m @@ -81,12 +81,11 @@ - (void)testNotConnectedToInternetError { } - (void)testSHA1HashStringWithData { - BugsnagApiClient *client = [[BugsnagApiClient alloc] init]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" - XCTAssertNil([client SHA1HashStringWithData:nil]); + XCTAssertNil([BugsnagApiClient SHA1HashStringWithData:nil]); #pragma clang diagnostic pop - XCTAssertEqualObjects([client SHA1HashStringWithData:[@"{\"foo\":\"bar\"}" dataUsingEncoding:NSUTF8StringEncoding]], @"a5e744d0164540d33b1d7ea616c28f2fa97e754a"); + XCTAssertEqualObjects([BugsnagApiClient SHA1HashStringWithData:[@"{\"foo\":\"bar\"}" dataUsingEncoding:NSUTF8StringEncoding]], @"a5e744d0164540d33b1d7ea616c28f2fa97e754a"); } @end diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj index 4db5b2f2c..d24a12507 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 01018BA025E40ADD000312C6 /* AsyncSafeMallocScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 01018B9F25E40ADD000312C6 /* AsyncSafeMallocScenario.m */; }; 0104085F258CA0A100933C60 /* DispatchCrashScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0104085E258CA0A100933C60 /* DispatchCrashScenario.swift */; }; 0163BFA72583B3CF008DC28B /* DiscardClassesScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */; }; + 01847DD626453D4E00ADA4C7 /* InternalErrorReportingScenarios.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DD526453D4E00ADA4C7 /* InternalErrorReportingScenarios.m */; }; 01AF6A53258A112F00FFC803 /* BareboneTestScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */; }; 01B6BB7525D5748800FC4DE6 /* LastRunInfoScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6BB7425D5748800FC4DE6 /* LastRunInfoScenarios.swift */; }; 01B6BBB625DA82B800FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6BBB525DA82B700FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift */; }; @@ -171,6 +172,7 @@ 01018B9F25E40ADD000312C6 /* AsyncSafeMallocScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncSafeMallocScenario.m; sourceTree = ""; }; 0104085E258CA0A100933C60 /* DispatchCrashScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchCrashScenario.swift; sourceTree = ""; }; 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardClassesScenarios.swift; sourceTree = ""; }; + 01847DD526453D4E00ADA4C7 /* InternalErrorReportingScenarios.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InternalErrorReportingScenarios.m; sourceTree = ""; }; 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BareboneTestScenarios.swift; sourceTree = ""; }; 01B6BB7425D5748800FC4DE6 /* LastRunInfoScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LastRunInfoScenarios.swift; sourceTree = ""; }; 01B6BBB525DA82B700FC4DE6 /* SendLaunchCrashesSynchronouslyScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendLaunchCrashesSynchronouslyScenario.swift; sourceTree = ""; }; @@ -572,6 +574,7 @@ 01F1474325F282E600C2DC65 /* AppHangScenarios.swift */, 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */, 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */, + 01847DD526453D4E00ADA4C7 /* InternalErrorReportingScenarios.m */, 01E5EAD025B713990066EA8A /* OOMScenario.h */, 01E5EAD125B713990066EA8A /* OOMScenario.m */, 8AB1081823301FE600672818 /* ReleaseStageScenarios.swift */, @@ -964,6 +967,7 @@ A1117E552535A59100014FDA /* OOMLoadScenario.swift in Sources */, 8A840FBA21AF5C450041DBFA /* SwiftAssertion.swift in Sources */, E753F24824927412001FB671 /* OnSendErrorCallbackCrashScenario.swift in Sources */, + 01847DD626453D4E00ADA4C7 /* InternalErrorReportingScenarios.m in Sources */, 001E5502243B8FDA0009E31D /* AutoCaptureRunScenario.m in Sources */, 0104085F258CA0A100933C60 /* DispatchCrashScenario.swift in Sources */, E700EE55247D3204008CFFB6 /* OnSendOverwriteScenario.swift in Sources */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index 370960ce4..394a3aef8 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 0176C0B6254AE81B0066E0F3 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0176C0B4254AE81B0066E0F3 /* MainMenu.xib */; }; 017FBFB8254B09C300809042 /* MainWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 017FBFB6254B09C300809042 /* MainWindowController.m */; }; 017FBFB9254B09C300809042 /* MainWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 017FBFB7254B09C300809042 /* MainWindowController.xib */; }; + 01847DCD26443DF000ADA4C7 /* InternalErrorReportingScenarios.m in Sources */ = {isa = PBXBuildFile; fileRef = 01847DCC26443DF000ADA4C7 /* InternalErrorReportingScenarios.m */; }; 01AF6A50258A00DE00FFC803 /* BareboneTestScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A4F258A00DE00FFC803 /* BareboneTestScenarios.swift */; }; 01AF6A84258BB38A00FFC803 /* DispatchCrashScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A83258BB38A00FFC803 /* DispatchCrashScenario.swift */; }; 01B6BB7225D56CBF00FC4DE6 /* LastRunInfoScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenarios.swift */; }; @@ -168,6 +169,7 @@ 017FBFB5254B09C300809042 /* MainWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MainWindowController.h; sourceTree = ""; }; 017FBFB6254B09C300809042 /* MainWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MainWindowController.m; sourceTree = ""; }; 017FBFB7254B09C300809042 /* MainWindowController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainWindowController.xib; sourceTree = ""; }; + 01847DCC26443DF000ADA4C7 /* InternalErrorReportingScenarios.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InternalErrorReportingScenarios.m; sourceTree = ""; }; 01AF6A4F258A00DE00FFC803 /* BareboneTestScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BareboneTestScenarios.swift; sourceTree = ""; }; 01AF6A83258BB38A00FFC803 /* DispatchCrashScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchCrashScenario.swift; sourceTree = ""; }; 01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastRunInfoScenarios.swift; sourceTree = ""; }; @@ -417,6 +419,7 @@ 01F47C4E254B1B2D00B184AD /* HandledErrorScenario.swift */, 01F47C28254B1B2C00B184AD /* HandledExceptionScenario.swift */, 01F47C55254B1B2E00B184AD /* HandledInternalNotifyScenario.swift */, + 01847DCC26443DF000ADA4C7 /* InternalErrorReportingScenarios.m */, 01B6BB7125D56CBF00FC4DE6 /* LastRunInfoScenarios.swift */, 01F47C23254B1B2C00B184AD /* LoadConfigFromFileAutoScenario.swift */, 01F47C94254B1B2F00B184AD /* LoadConfigFromFileScenario.swift */, @@ -679,6 +682,7 @@ 01F47CE9254B1B3100B184AD /* SIGTRAPScenario.m in Sources */, 01F47CC8254B1B3100B184AD /* AutoSessionScenario.m in Sources */, 01E0DB0625E8E95700A740ED /* AppDurationScenario.swift in Sources */, + 01847DCD26443DF000ADA4C7 /* InternalErrorReportingScenarios.m in Sources */, 01F47D2D254B1B3100B184AD /* OnErrorOverwriteScenario.swift in Sources */, 01F47CC7254B1B3100B184AD /* MetadataRedactionDefaultScenario.swift in Sources */, 01F47CEC254B1B3100B184AD /* SessionCallbackCrashScenario.swift in Sources */, diff --git a/features/fixtures/shared/scenarios/InternalErrorReportingScenarios.m b/features/fixtures/shared/scenarios/InternalErrorReportingScenarios.m new file mode 100644 index 000000000..a95922bb0 --- /dev/null +++ b/features/fixtures/shared/scenarios/InternalErrorReportingScenarios.m @@ -0,0 +1,34 @@ +// +// InternalErrorReportingScenarios.m +// macOSTestApp +// +// Created by Nick Dowell on 07/05/2021. +// Copyright © 2021 Bugsnag Inc. All rights reserved. +// + +#import "Scenario.h" + +@interface InternalErrorReportingScenarios_KSCrashReport : Scenario + +@end + +static void InternalErrorReportingScenarios_KSCrashReport_CrashHandler() { + // Terminate the process without running atexit handlers. This should leave + // a partically written KSCrashReport which will fail to parse as JSON. + _exit(0); +} + +@implementation InternalErrorReportingScenarios_KSCrashReport + +- (void)startBugsnag { + self.config.autoTrackSessions = NO; + self.config.onCrashHandler = InternalErrorReportingScenarios_KSCrashReport_CrashHandler; + + [super startBugsnag]; +} + +- (void)run { + __builtin_trap(); +} + +@end diff --git a/features/internal_error_reporting.feature b/features/internal_error_reporting.feature new file mode 100644 index 000000000..d4ac51fb1 --- /dev/null +++ b/features/internal_error_reporting.feature @@ -0,0 +1,18 @@ +Feature: Internal error reporting + + Background: + Given I clear all persistent data + + Scenario: An internal error report is sent for invalid KSCrashReport files + When I run "InternalErrorReportingScenarios_KSCrashReport" and relaunch the app + And I configure Bugsnag for "InternalErrorReportingScenarios_KSCrashReport" + And I wait to receive an error + And the error "Bugsnag-Api-Key" header is null + And the error "Bugsnag-Internal-Error" header equals "bugsnag-cocoa" + And the error payload field "events.0.threads" is an array with 0 elements + And the event "apiKey" is null + And the event "metaData.BugsnagDiagnostics.apiKey" equals "12312312312312312312312312312312" + And the event "metaData.BugsnagDiagnostics.data" is not null + And the event "unhandled" is false + And the exception "errorClass" equals "JSON parsing error" + And the exception "message" equals "NSCocoaErrorDomain 3840: Unexpected end of file while parsing object." diff --git a/features/steps/header_steps.rb b/features/steps/header_steps.rb new file mode 100644 index 000000000..2761177e5 --- /dev/null +++ b/features/steps/header_steps.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @!group Header steps + +Then('the {word} {string} header is null') do |request_type, header_name| + assert_nil(Maze::Server.list_for(request_type).current[:request][header_name], + "The #{request_type} '#{header_name}' header should be null") +end