From 4f844debadba68259b925c3f05cc6200d77dda4b Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Mon, 6 Apr 2020 15:15:34 +0100 Subject: [PATCH 1/3] feat: add structured class for stackframe --- CHANGELOG.md | 3 +++ iOS/Bugsnag.xcodeproj/project.pbxproj | 4 ++++ tvOS/Bugsnag.xcodeproj/project.pbxproj | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f4abbdc..2032268bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Bugsnag Notifiers on other platforms. ## Enhancements +* Create structured `BugsnagThread` class + [#532](https://github.com/bugsnag/bugsnag-cocoa/pull/532) + * Convert `event.device` from `NSDictionary` to a structured class [#526](https://github.com/bugsnag/bugsnag-cocoa/pull/526) diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index 3406418d8..087a54a0c 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -699,6 +699,8 @@ E7D2E669243B8F48005A3041 /* BugsnagStacktrace.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BugsnagStacktrace.h; path = ../Source/BugsnagStacktrace.h; sourceTree = ""; }; E7D2E66A243B8F48005A3041 /* BugsnagStacktrace.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagStacktrace.m; path = ../Source/BugsnagStacktrace.m; sourceTree = ""; }; E7D2E66F243B8F8D005A3041 /* BugsnagStacktraceTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagStacktraceTest.m; path = ../../Tests/BugsnagStacktraceTest.m; sourceTree = ""; }; + E7EB06721FCDAF2000C076A6 /* BugsnagKSCrashSysInfoParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BugsnagKSCrashSysInfoParser.h; path = ../Source/BugsnagKSCrashSysInfoParser.h; sourceTree = ""; }; + E7EB06731FCDAF2000C076A6 /* BugsnagKSCrashSysInfoParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagKSCrashSysInfoParser.m; path = ../Source/BugsnagKSCrashSysInfoParser.m; sourceTree = ""; }; E7EC040C1F4CC6A000C2E9D5 /* libc++.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libc++.1.dylib"; path = "../../../../../usr/lib/libc++.1.dylib"; sourceTree = ""; }; E7EC040D1F4CC6A000C2E9D5 /* libz.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.1.dylib; path = ../../../../../usr/lib/libz.1.dylib; sourceTree = ""; }; E7EC04101F4CC82800C2E9D5 /* libstdc++.6.0.9.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.6.0.9.dylib"; path = "../../../../../usr/lib/libstdc++.6.0.9.dylib"; sourceTree = ""; }; @@ -831,6 +833,8 @@ E78C1EF51FCC61EA00B976D3 /* BugsnagSessionTrackingPayload.m */, E7D2E669243B8F48005A3041 /* BugsnagStacktrace.h */, E7D2E66A243B8F48005A3041 /* BugsnagStacktrace.m */, + 8A2C8F4D1C6BBE3C00846019 /* BugsnagSink.h */, + 8A2C8F4E1C6BBE3C00846019 /* BugsnagSink.m */, E72BF77D1FC86A7A004BE82F /* BugsnagUser.h */, E72BF77E1FC86A7A004BE82F /* BugsnagUser.m */, 8A2C8F1D1C6BBD2300846019 /* Info.plist */, diff --git a/tvOS/Bugsnag.xcodeproj/project.pbxproj b/tvOS/Bugsnag.xcodeproj/project.pbxproj index 69e65c4c9..768c546df 100644 --- a/tvOS/Bugsnag.xcodeproj/project.pbxproj +++ b/tvOS/Bugsnag.xcodeproj/project.pbxproj @@ -504,6 +504,8 @@ 8A6C6FAB2257882400E8EF24 /* BSGOutOfMemoryWatchdog.m */, 8A627CD31EC3B69300F7C04E /* BSGSerialization.h */, 8A627CD41EC3B69300F7C04E /* BSGSerialization.m */, + E7D2E679243B8FD2005A3041 /* BugsnagStacktrace.h */, + E7D2E67A243B8FD2005A3041 /* BugsnagStacktrace.m */, 8AB1512A1D41366400C9B218 /* BugsnagCollections.h */, 8AB1512B1D41366400C9B218 /* BugsnagCollections.m */, 8AB1512C1D41366400C9B218 /* BugsnagConfiguration.h */, @@ -553,6 +555,9 @@ 00D7ACA523E98A5D00FBE4A7 /* BugsnagTestConstants.h */, E722105F243B6A220083CF15 /* BugsnagStackframeTest.m */, E7D2E677243B8FC8005A3041 /* BugsnagStacktraceTest.m */, + E722105F243B6A220083CF15 /* BugsnagStackframeTest.m */, + E7D2E677243B8FC8005A3041 /* BugsnagStacktraceTest.m */, + E791488D1FD82E77003EFEBF /* BugsnagUserTest.m */, 00F9393D23FD2DDB008C7073 /* BugsnagTestsDummyClass.h */, 00F9393E23FD2DDB008C7073 /* BugsnagTestsDummyClass.m */, 4B3CD2B922C5676700DBFF33 /* BugsnagThreadTest.m */, From f8db02cff762a4255e73c46484ef6c7f99832109 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 7 Apr 2020 13:50:30 +0100 Subject: [PATCH 2/3] feat: add structured class for Thread --- OSX/Bugsnag.xcodeproj/project.pbxproj | 10 +- Source/BugsnagEvent.h | 6 + Source/BugsnagEvent.m | 112 ++++------ Source/BugsnagThread.h | 35 ++- Source/BugsnagThread.m | 96 ++++++++ Tests/BugsnagThreadTest.m | 208 ++++++++++++++++++ UPGRADING.md | 1 + iOS/Bugsnag.xcodeproj/project.pbxproj | 12 +- .../BugsnagEventFromKSCrashReportTest.m | 70 +++--- ...est.m => BugsnagThreadSerializationTest.m} | 23 +- tvOS/Bugsnag.xcodeproj/project.pbxproj | 6 +- 11 files changed, 457 insertions(+), 122 deletions(-) create mode 100644 Tests/BugsnagThreadTest.m rename iOS/BugsnagTests/{BugsnagThreadTest.m => BugsnagThreadSerializationTest.m} (79%) diff --git a/OSX/Bugsnag.xcodeproj/project.pbxproj b/OSX/Bugsnag.xcodeproj/project.pbxproj index 9f86c7118..8986e3dc9 100644 --- a/OSX/Bugsnag.xcodeproj/project.pbxproj +++ b/OSX/Bugsnag.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ E7433AD31F4F64F400C082D1 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C8FF31C6BC3AE00846019 /* libc++.tbd */; }; E7529F8E243C8EBF006B4932 /* RegisterErrorData.h in Headers */ = {isa = PBXBuildFile; fileRef = E7529F8C243C8EBF006B4932 /* RegisterErrorData.h */; }; E7529F8F243C8EBF006B4932 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F8D243C8EBF006B4932 /* RegisterErrorData.m */; }; + E7529FA0243CAE35006B4932 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F9F243CAE34006B4932 /* BugsnagThreadTest.m */; }; + E7529FA2243CAE3F006B4932 /* BugsnagThreadSerializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529FA1243CAE3F006B4932 /* BugsnagThreadSerializationTest.m */; }; E762E9F91F73F7F300E82B43 /* BugsnagHandledStateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E762E9F71F73F7E900E82B43 /* BugsnagHandledStateTest.m */; }; E762E9FC1F73F80200E82B43 /* BugsnagHandledState.h in Headers */ = {isa = PBXBuildFile; fileRef = E762E9FA1F73F80200E82B43 /* BugsnagHandledState.h */; }; E762E9FD1F73F80200E82B43 /* BugsnagHandledState.m in Sources */ = {isa = PBXBuildFile; fileRef = E762E9FB1F73F80200E82B43 /* BugsnagHandledState.m */; }; @@ -279,6 +281,8 @@ E72AE1F8241A4E7500ED8972 /* BugsnagPluginClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagPluginClient.h; path = ../Source/BugsnagPluginClient.h; sourceTree = ""; }; E7529F8C243C8EBF006B4932 /* RegisterErrorData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegisterErrorData.h; path = ../Source/RegisterErrorData.h; sourceTree = ""; }; E7529F8D243C8EBF006B4932 /* RegisterErrorData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegisterErrorData.m; path = ../Source/RegisterErrorData.m; sourceTree = ""; }; + E7529F9F243CAE34006B4932 /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagThreadTest.m; sourceTree = ""; }; + E7529FA1243CAE3F006B4932 /* BugsnagThreadSerializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagThreadSerializationTest.m; path = ../iOS/BugsnagTests/BugsnagThreadSerializationTest.m; sourceTree = ""; }; E762E9F71F73F7E900E82B43 /* BugsnagHandledStateTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagHandledStateTest.m; path = ../Tests/BugsnagHandledStateTest.m; sourceTree = SOURCE_ROOT; }; E762E9FA1F73F80200E82B43 /* BugsnagHandledState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagHandledState.h; path = ../Source/BugsnagHandledState.h; sourceTree = SOURCE_ROOT; }; E762E9FB1F73F80200E82B43 /* BugsnagHandledState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagHandledState.m; path = ../Source/BugsnagHandledState.m; sourceTree = SOURCE_ROOT; }; @@ -548,9 +552,11 @@ E791482E1FD82B0C003EFEBF /* BugsnagUserTest.m */, E7D2E675243B8FB6005A3041 /* BugsnagStacktraceTest.m */, E722105D243B6A0E0083CF15 /* BugsnagStackframeTest.m */, - E791482E1FD82B0C003EFEBF /* BugsnagUserTest.m */, 00F9393B23FD2D9B008C7073 /* BugsnagTestsDummyClass.h */, 00F9393A23FD2D9B008C7073 /* BugsnagTestsDummyClass.m */, + E7529FA1243CAE3F006B4932 /* BugsnagThreadSerializationTest.m */, + E7529F9F243CAE34006B4932 /* BugsnagThreadTest.m */, + E791482E1FD82B0C003EFEBF /* BugsnagUserTest.m */, 8A2C8FE41C6BC38200846019 /* report.json */, 8A2C8FB21C6BC1F700846019 /* TestsInfo.plist */, E7CE78861FD94E40001D07E0 /* KSCrash */, @@ -1135,6 +1141,7 @@ E790C42324324528006FFB26 /* BugsnagClientMirrorTest.m in Sources */, E7CE78D01FD94E77001D07E0 /* RFC3339DateTool_Tests.m in Sources */, E79148621FD82BB7003EFEBF /* BugsnagSessionTrackingPayloadTest.m in Sources */, + E7529FA2243CAE3F006B4932 /* BugsnagThreadSerializationTest.m in Sources */, E79148631FD82BB7003EFEBF /* BugsnagUserTest.m in Sources */, 8A2C8FEA1C6BC38900846019 /* BugsnagBreadcrumbsTest.m in Sources */, E7CE78BB1FD94E77001D07E0 /* KSCrashReportConverter_Tests.m in Sources */, @@ -1152,6 +1159,7 @@ E762E9F91F73F7F300E82B43 /* BugsnagHandledStateTest.m in Sources */, E7CE78C51FD94E77001D07E0 /* KSLogger_Tests.m in Sources */, E7CE78C11FD94E77001D07E0 /* KSCrashState_Tests.m in Sources */, + E7529FA0243CAE35006B4932 /* BugsnagThreadTest.m in Sources */, E7AB4B9E2423E184004F015A /* BugsnagOnBreadcrumbTest.m in Sources */, E7CE78C31FD94E77001D07E0 /* KSFileUtils_Tests.m in Sources */, E7CE78BC1FD94E77001D07E0 /* KSCrashReportStore_Tests.m in Sources */, diff --git a/Source/BugsnagEvent.h b/Source/BugsnagEvent.h index 04def7872..de60e7276 100644 --- a/Source/BugsnagEvent.h +++ b/Source/BugsnagEvent.h @@ -16,6 +16,7 @@ @class BugsnagAppWithState; @class BugsnagDeviceWithState; @class BugsnagMetadata; +@class BugsnagThread; typedef NS_ENUM(NSUInteger, BSGSeverity) { BSGSeverityError, @@ -132,5 +133,10 @@ initWithErrorName:(NSString *_Nonnull)name */ @property(readonly) BOOL unhandled; +/** + * Thread traces for the error that occurred, if collection was enabled. + */ +@property(readonly, nonnull) NSMutableArray *threads; + @end diff --git a/Source/BugsnagEvent.m b/Source/BugsnagEvent.m index c046c5f7b..551123ca9 100644 --- a/Source/BugsnagEvent.m +++ b/Source/BugsnagEvent.m @@ -26,6 +26,7 @@ #import "BugsnagDeviceWithState.h" #import "BugsnagClient.h" #import "BugsnagStacktrace.h" +#import "BugsnagThread.h" #import "RegisterErrorData.h" static NSString *const DEFAULT_EXCEPTION_TYPE = @"cocoa"; @@ -72,6 +73,26 @@ + (BugsnagStackframe *)frameFromDict:(NSDictionary *)dict withImages:(NSArray *)binaryImages; @end +@interface BugsnagThread () +@property BugsnagStacktrace *trace; +- (NSDictionary *)toDict; + +- (instancetype)initWithThread:(NSDictionary *)thread + binaryImages:(NSArray *)binaryImages; + ++ (NSMutableArray *)threadsFromArray:(NSArray *)threads + binaryImages:(NSArray *)binaryImages + depth:(NSUInteger)depth + errorType:(NSString *)errorType; + ++ (NSMutableArray *)serializeThreads:(NSMutableDictionary *)event + threads:(NSArray *)threads; +@end + +@interface BugsnagStacktrace () +- (NSArray *)toArray; +@end + // MARK: - KSCrashReport parsing NSString *_Nonnull BSGParseErrorClass(NSDictionary *error, @@ -216,14 +237,6 @@ @interface BugsnagEvent () * A unique hash identifying this device for the application or vendor */ @property(nonatomic, readonly, copy, nullable) NSString *deviceAppHash; -/** - * Binary images used to identify application symbols - */ -@property(nonatomic, readonly, copy, nullable) NSArray *binaryImages; -/** - * Thread information captured at the time of the error - */ -@property(nonatomic, readonly, copy, nullable) NSArray *threads; /** * User-provided exception metadata */ @@ -300,6 +313,9 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { _releaseStage = [report valueForKeyPath:@"user.state.oom.app.releaseStage"]; _handledState = [BugsnagHandledState handledStateWithSeverityReason:LikelyOutOfMemory]; _deviceAppHash = [report valueForKeyPath:@"user.state.oom.device.id"]; + + // no threads or metadata captured for OOMs + _threads = [NSMutableArray new]; self.metadata = [BugsnagMetadata new]; NSDictionary *sessionData = [report valueForKeyPath:@"user.state.oom.session"]; @@ -313,8 +329,10 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { } else { _enabledReleaseStages = BSGLoadConfigValue(report, BSGKeyEnabledReleaseStages); _releaseStage = BSGParseReleaseStage(report); - _threads = [report valueForKeyPath:@"crash.threads"]; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:_threads]; + NSArray *binaryImages = report[@"binary_images"]; + NSArray *threadDict = [report valueForKeyPath:@"crash.threads"]; + + RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:threadDict]; if (data) { _errorClass = data.errorClass ; _errorMessage = data.errorMessage; @@ -322,7 +340,6 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { _errorClass = BSGParseErrorClass(_error, _errorType); _errorMessage = BSGParseErrorMessage(report, _error, _errorType); } - _binaryImages = report[@"binary_images"]; _breadcrumbs = BSGParseBreadcrumbs(report); _deviceAppHash = [report valueForKeyPath:@"system.device_app_hash"]; @@ -370,6 +387,12 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { if (report[@"user"][@"id"]) { _session = [[BugsnagSession alloc] initWithDictionary:report[@"user"]]; } + + // generate threads last, relies on depth/errorType properties being calculated first + _threads = [BugsnagThread threadsFromArray:threadDict + binaryImages:binaryImages + depth:self.depth + errorType:self.errorType]; } } return self; @@ -404,6 +427,7 @@ - (instancetype _Nonnull)initWithErrorName:(NSString *_Nonnull)name _handledState = handledState; _severity = handledState.currentSeverity; _session = session; + _threads = [NSMutableArray new]; } return self; } @@ -539,8 +563,6 @@ - (NSDictionary *)toJson { if (self.customException) { BSGDictSetSafeObject(event, @[ self.customException ], BSGKeyExceptions); - BSGDictSetSafeObject(event, [self serializeThreadsWithException:nil], - BSGKeyThreads); } else { NSMutableDictionary *exception = [NSMutableDictionary dictionary]; BSGDictSetSafeObject(exception, [self errorClass], BSGKeyErrorClass); @@ -548,9 +570,16 @@ - (NSDictionary *)toJson { BSGDictInsertIfNotNil(exception, DEFAULT_EXCEPTION_TYPE, BSGKeyType); BSGDictSetSafeObject(event, @[ exception ], BSGKeyExceptions); - BSGDictSetSafeObject( - event, [self serializeThreadsWithException:exception], BSGKeyThreads); + // set the stacktrace for the exception from the threads + for (BugsnagThread *thread in self.threads) { + if (thread.errorReportingThread) { + BSGDictSetSafeObject(exception, [thread.trace toArray], BSGKeyStacktrace); + } + } } + + BSGDictSetSafeObject(event, [BugsnagThread serializeThreads:event threads:self.threads], BSGKeyThreads); + // Build Event BSGDictSetSafeObject(event, BSGFormatSeverity(self.severity), BSGKeySeverity); BSGDictSetSafeObject(event, [self serializeBreadcrumbs], BSGKeyBreadcrumbs); @@ -614,59 +643,6 @@ - (NSDictionary *)generateSessionDict { return sessionJson; } -// Build all stacktraces for threads and the error -- (NSArray *)serializeThreadsWithException:(NSMutableDictionary *)exception { - NSMutableArray *bugsnagThreads = [NSMutableArray array]; - - for (NSDictionary *thread in self.threads) { - NSArray *backtrace = thread[@"backtrace"][@"contents"]; - BOOL stackOverflow = [thread[@"stack"][@"overflow"] boolValue]; - BOOL isReportingThread = [thread[@"crashed"] boolValue]; - - if (isReportingThread) { - NSUInteger seen = 0; - NSMutableArray *stacktrace = [NSMutableArray array]; - - for (NSDictionary *frame in backtrace) { - NSMutableDictionary *mutableFrame = [frame mutableCopy]; - if (seen++ >= [self depth]) { - // Mark the frame so we know where it came from - if (seen == 1 && !stackOverflow) { - BSGDictSetSafeObject(mutableFrame, @YES, BSGKeyIsPC); - } - if (seen == 2 && !stackOverflow && - [@[ BSGKeySignal, BSGKeyMach ] - containsObject:[self errorType]]) { - BSGDictSetSafeObject(mutableFrame, @YES, BSGKeyIsLR); - } - BSGArrayInsertIfNotNil(stacktrace, mutableFrame); - } - } - BugsnagStacktrace *trace = [[BugsnagStacktrace alloc] initWithTrace:stacktrace binaryImages:self.binaryImages]; - BSGDictSetSafeObject(exception, [trace toArray], BSGKeyStacktrace); - } - [self serialiseThread:bugsnagThreads thread:thread backtrace:backtrace reportingThread:isReportingThread]; - } - return bugsnagThreads; -} - -- (void)serialiseThread:(NSMutableArray *)bugsnagThreads - thread:(NSDictionary *)thread - backtrace:(NSArray *)backtrace - reportingThread:(BOOL)isReportingThread { - BugsnagStacktrace *stacktrace = [[BugsnagStacktrace alloc] initWithTrace:backtrace binaryImages:self.binaryImages]; - NSMutableDictionary *threadDict = [NSMutableDictionary dictionary]; - BSGDictSetSafeObject(threadDict, thread[@"index"], BSGKeyId); - BSGDictSetSafeObject(threadDict, [stacktrace toArray], BSGKeyStacktrace); - BSGDictSetSafeObject(threadDict, DEFAULT_EXCEPTION_TYPE, BSGKeyType); - - if (isReportingThread) { - BSGDictSetSafeObject(threadDict, @YES, @"errorReportingThread"); - } - - BSGArrayAddSafeObject(bugsnagThreads, threadDict); -} - - (BOOL)unhandled { return self.handledState.unhandled; } diff --git a/Source/BugsnagThread.h b/Source/BugsnagThread.h index d5e15b754..0aca41205 100644 --- a/Source/BugsnagThread.h +++ b/Source/BugsnagThread.h @@ -8,13 +8,42 @@ #import -NS_ASSUME_NONNULL_BEGIN +typedef NS_OPTIONS(NSUInteger, BSGThreadType) { + BSGThreadTypeCocoa = 0, + BSGThreadTypeReactNativeJs = 1 << 1 +}; + +@class BugsnagStackframe; /** * A representation of thread information recorded as part of a BugsnagEvent. */ @interface BugsnagThread : NSObject -@end +/** + * A unique ID which identifies this thread + */ +@property(nullable) NSString *id; -NS_ASSUME_NONNULL_END +/** + * The name which identifies this thread + */ +@property(nullable) NSString *name; + +/** + * Whether this thread was the thread that triggered the captured error + */ +@property BOOL errorReportingThread; + +/** + * Sets a representation of this thread's stacktrace + */ +@property(readonly, nonnull) NSArray *stacktrace; + +/** + * Determines the type of thread based on the originating platform + * (intended for internal use only) + */ +@property BSGThreadType type; + +@end diff --git a/Source/BugsnagThread.m b/Source/BugsnagThread.m index 52dfc1144..75c4ba042 100644 --- a/Source/BugsnagThread.m +++ b/Source/BugsnagThread.m @@ -7,7 +7,103 @@ // #import "BugsnagThread.h" +#import "BugsnagCollections.h" +#import "BugsnagStacktrace.h" +#import "BugsnagKeys.h" + +@interface BugsnagStacktrace () +- (NSArray *)toArray; +@end + +@interface BugsnagThread () +@property BugsnagStacktrace *trace; +@end + +@interface BugsnagStacktrace () +@property NSMutableArray *trace; +@end @implementation BugsnagThread +- (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)binaryImages { + if (self = [super init]) { + self.errorReportingThread = [thread[@"crashed"] boolValue]; + self.id = [thread[@"index"] stringValue]; + self.type = BSGThreadTypeCocoa; + + NSArray *backtrace = thread[@"backtrace"][@"contents"]; + self.trace = [[BugsnagStacktrace alloc] initWithTrace:backtrace binaryImages:binaryImages]; + } + return self; +} + +- (NSMutableArray *)stacktrace { + return self.trace.trace; +} + +- (NSDictionary *)toDict { + NSMutableDictionary *dict = [NSMutableDictionary new]; + BSGDictInsertIfNotNil(dict, self.id, @"id"); + BSGDictInsertIfNotNil(dict, self.name, @"name"); + BSGDictSetSafeObject(dict, @(self.errorReportingThread), @"errorReportingThread"); + BSGDictSetSafeObject(dict, self.type == BSGThreadTypeCocoa ? @"cocoa" : @"reactnativejs", @"type"); + BSGDictSetSafeObject(dict, @(self.errorReportingThread), @"errorReportingThread"); + BSGDictSetSafeObject(dict, [self.trace toArray], @"stacktrace"); + return dict; +} + ++ (NSMutableArray *)serializeThreads:(NSMutableDictionary *)event threads:(NSArray *)threads { + NSMutableArray *threadArray = [NSMutableArray new]; + for (BugsnagThread *thread in threads) { + [threadArray addObject:[thread toDict]]; + } + return threadArray; +} + ++ (NSMutableArray *)threadsFromArray:(NSArray *)threads + binaryImages:(NSArray *)binaryImages + depth:(NSUInteger)depth + errorType:(NSString *)errorType { + NSMutableArray *bugsnagThreads = [NSMutableArray new]; + + for (NSDictionary *thread in threads) { + NSDictionary *threadInfo = [self enhanceThreadInfo:thread depth:depth errorType:errorType]; + BugsnagThread *obj = [[BugsnagThread alloc] initWithThread:threadInfo binaryImages:binaryImages]; + [bugsnagThreads addObject:obj]; + } + return bugsnagThreads; +} + ++ (NSDictionary *)enhanceThreadInfo:(NSDictionary *)thread + depth:(NSUInteger)depth + errorType:(NSString *)errorType { + NSArray *backtrace = thread[@"backtrace"][@"contents"]; + BOOL isReportingThread = [thread[@"crashed"] boolValue]; + + if (isReportingThread) { + BOOL stackOverflow = [thread[@"stack"][@"overflow"] boolValue]; + NSUInteger seen = 0; + NSMutableArray *stacktrace = [NSMutableArray array]; + + for (NSDictionary *frame in backtrace) { + NSMutableDictionary *mutableFrame = (NSMutableDictionary *) [frame mutableCopy]; + if (seen++ >= depth) { + // Mark the frame so we know where it came from + if (seen == 1 && !stackOverflow) { + BSGDictSetSafeObject(mutableFrame, @YES, BSGKeyIsPC); + } + if (seen == 2 && !stackOverflow && [@[BSGKeySignal, BSGKeyMach] containsObject:errorType]) { + BSGDictSetSafeObject(mutableFrame, @YES, BSGKeyIsLR); + } + BSGArrayInsertIfNotNil(stacktrace, mutableFrame); + } + } + NSMutableDictionary *copy = [NSMutableDictionary dictionaryWithDictionary:thread]; + copy[@"backtrace"] = [NSMutableDictionary dictionaryWithDictionary:copy[@"backtrace"]]; + copy[@"backtrace"][@"contents"] = stacktrace; + return copy; + } + return thread; +} + @end diff --git a/Tests/BugsnagThreadTest.m b/Tests/BugsnagThreadTest.m new file mode 100644 index 000000000..a97e8dbbb --- /dev/null +++ b/Tests/BugsnagThreadTest.m @@ -0,0 +1,208 @@ +// +// BugsnagThreadTest.m +// Tests +// +// Created by Jamie Lynch on 07/04/2020. +// Copyright © 2020 Bugsnag. All rights reserved. +// + +#import + +#import "BugsnagStackframe.h" +#import "BugsnagThread.h" + +@interface BugsnagThread () +- (NSDictionary *)toDict; + +- (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)binaryImages; + ++ (NSDictionary *)enhanceThreadInfo:(NSDictionary *)thread + depth:(NSUInteger)depth + errorType:(NSString *)errorType; +@end + +@interface BugsnagThreadTest : XCTestCase +@property NSArray *binaryImages; +@property NSDictionary *thread; +@end + +@implementation BugsnagThreadTest + +- (void)setUp { + self.thread = @{ + @"current_thread": @YES, + @"crashed": @YES, + @"index": @4, + @"backtrace": @{ + @"skipped": @0, + @"contents": @[ + @{ + @"symbol_name": @"kscrashsentry_reportUserException", + @"symbol_addr": @4491038467, + @"instruction_addr": @4491038575, + @"object_name": @"CrashProbeiOS", + @"object_addr": @4490747904 + } + ] + }, + }; + self.binaryImages = @[@{ + @"uuid": @"D0A41830-4FD2-3B02-A23B-0741AD4C7F52", + @"image_vmaddr": @4294967296, + @"image_addr": @4490747904, + @"image_size": @483328, + @"name": @"/Users/joesmith/foo", + }]; +} + +- (void)testThreadFromDict { + BugsnagThread *thread = [[BugsnagThread alloc] initWithThread:self.thread binaryImages:self.binaryImages]; + XCTAssertNotNil(thread); + + XCTAssertEqualObjects(@"4", thread.id); + XCTAssertNil(thread.name); + XCTAssertEqual(BSGThreadTypeCocoa, thread.type); + XCTAssertTrue(thread.errorReportingThread); + + // validate stacktrace + XCTAssertEqual(1, [thread.stacktrace count]); + BugsnagStackframe *frame = thread.stacktrace[0]; + XCTAssertEqualObjects(@"kscrashsentry_reportUserException", frame.method); + XCTAssertEqualObjects(@"CrashProbeiOS", frame.machoFile); + XCTAssertEqualObjects(@"D0A41830-4FD2-3B02-A23B-0741AD4C7F52", frame.machoUuid); +} + +- (void)testThreadToDict { + BugsnagThread *thread = [[BugsnagThread alloc] initWithThread:self.thread binaryImages:self.binaryImages]; + thread.name = @"bugsnag-thread-1"; + + NSDictionary *dict = [thread toDict]; + XCTAssertEqualObjects(@"4", dict[@"id"]); + XCTAssertEqualObjects(@"bugsnag-thread-1", dict[@"name"]); + XCTAssertEqualObjects(@"cocoa", dict[@"type"]); + XCTAssertTrue([dict[@"errorReportingThread"] boolValue]); + + // validate stacktrace + XCTAssertEqual(1, [dict[@"stacktrace"] count]); + NSDictionary *frame = dict[@"stacktrace"][0]; + XCTAssertEqualObjects(@"kscrashsentry_reportUserException", frame[@"method"]); + XCTAssertEqualObjects(@"CrashProbeiOS", frame[@"machoFile"]); + XCTAssertEqualObjects(@"D0A41830-4FD2-3B02-A23B-0741AD4C7F52", frame[@"machoUUID"]); +} + +/** + * Dictionary info not enhanced if not an error reporting thread + */ +- (void)testThreadEnhancementNotCrashed { + NSDictionary *dict = @{ + @"backtrace": @{ + @"contents": @[ + @{}, + @{}, + @{}, + ] + }, + @"crashed": @NO + }; + NSDictionary *thread = [BugsnagThread enhanceThreadInfo:dict depth:0 errorType:nil]; + XCTAssertEqual(dict, thread); +} + +/** + * Dictionary info enhanced if an error reporting thread + */ +- (void)testThreadEnhancementCrashed { + NSDictionary *dict = @{ + @"backtrace": @{ + @"contents": @[ + @{}, + @{}, + @{}, + ] + }, + @"crashed": @YES + }; + NSDictionary *thread = [BugsnagThread enhanceThreadInfo:dict depth:0 errorType:nil]; + XCTAssertNotEqual(dict, thread); + NSArray *trace = thread[@"backtrace"][@"contents"]; + XCTAssertEqual(3, [trace count]); +} + +/** + * Frames enhanced with lc/pr info + */ +- (void)testThreadEnhancementLcPr { + NSDictionary *dict = @{ + @"backtrace": @{ + @"contents": @[ + @{}, + @{}, + @{} + ] + }, + @"crashed": @YES + }; + NSDictionary *thread = [BugsnagThread enhanceThreadInfo:dict depth:0 errorType:@"signal"]; + XCTAssertNotEqual(dict, thread); + NSArray *trace = thread[@"backtrace"][@"contents"]; + XCTAssertEqual(3, [trace count]); + + XCTAssertEqual(1, [trace[0] count]); + XCTAssertTrue(trace[0][@"isPC"]); + XCTAssertEqual(1, [trace[1] count]); + XCTAssertTrue(trace[1][@"isLR"]); + XCTAssertEqual(0, [trace[2] count]); +} + +/** + * Frames enhanced with lc/pr info, wrong error type + */ +- (void)testThreadEnhancementWrongErrorType { + NSDictionary *dict = @{ + @"backtrace": @{ + @"contents": @[ + @{}, + @{}, + @{} + ] + }, + @"crashed": @YES + }; + NSDictionary *thread = [BugsnagThread enhanceThreadInfo:dict depth:0 errorType:@"NSException"]; + XCTAssertNotEqual(dict, thread); + NSArray *trace = thread[@"backtrace"][@"contents"]; + XCTAssertEqual(3, [trace count]); + + XCTAssertEqual(1, [trace[0] count]); + XCTAssertTrue(trace[0][@"isPC"]); + XCTAssertEqual(0, [trace[1] count]); + XCTAssertEqual(0, [trace[2] count]); +} + +/** + * Frames not enhanced with lc/pr info in stack overflow + */ +- (void)testThreadEnhancementStackoverflow { + NSDictionary *dict = @{ + @"backtrace": @{ + @"contents": @[ + @{}, + @{}, + @{} + ] + }, + @"crashed": @YES, + @"stack": @{ + @"overflow": @YES + } + }; + NSDictionary *thread = [BugsnagThread enhanceThreadInfo:dict depth:0 errorType:@"signal"]; + XCTAssertNotEqual(dict, thread); + NSArray *trace = thread[@"backtrace"][@"contents"]; + XCTAssertEqual(3, [trace count]); + XCTAssertEqual(0, [trace[0] count]); + XCTAssertEqual(0, [trace[1] count]); + XCTAssertEqual(0, [trace[2] count]); +} + +@end diff --git a/UPGRADING.md b/UPGRADING.md index 26cd82c15..07d15ff70 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -226,6 +226,7 @@ This is now BugsnagEvent. `event.device` is now a structured class with properties for each value, rather than an `NSDictionary`. `event.app` is now a structured class with properties for each value, rather than an `NSDictionary`. +`event.threads` is now an array containing a structured class with properties for each `BugsnagThread` value. #### Renames diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index 087a54a0c..c31df348e 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -289,6 +289,8 @@ E7529F8A243C8DA2006B4932 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F88243C8DA2006B4932 /* RegisterErrorData.m */; }; E7529F8B243C8DA2006B4932 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F88243C8DA2006B4932 /* RegisterErrorData.m */; }; E7529F94243C8EF2006B4932 /* RegisterErrorData.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = E7529F87243C8DA2006B4932 /* RegisterErrorData.h */; }; + E7529F9C243CAE02006B4932 /* BugsnagThreadSerializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F9B243CAE02006B4932 /* BugsnagThreadSerializationTest.m */; }; + E7529F9E243CAE0D006B4932 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F9D243CAE0D006B4932 /* BugsnagThreadTest.m */; }; E77316E31F73E89E00A14F06 /* BugsnagHandledStateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E77316E11F73B46600A14F06 /* BugsnagHandledStateTest.m */; }; E77526BA242D0AE50077A42F /* BugsnagBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = E77526B8242D0AE50077A42F /* BugsnagBreadcrumbs.h */; }; E77526BB242D0AE50077A42F /* BugsnagBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = E77526B9242D0AE50077A42F /* BugsnagBreadcrumbs.m */; }; @@ -360,7 +362,6 @@ E7EC041A1F4CC97200C2E9D5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E79FEBE61F4CB1320048FAD6 /* Foundation.framework */; }; F4295168CDC7A77A832C9475 /* BugsnagApiClient.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295E2778677786239F2B28 /* BugsnagApiClient.m */; }; F429522FC5D3D54364D51F3F /* BugsnagApiClient.h in Headers */ = {isa = PBXBuildFile; fileRef = F42950F4A741305B77E95389 /* BugsnagApiClient.h */; }; - F42952D83435C02F8D891C40 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F429551527EAE3AFE1F605FE /* BugsnagThreadTest.m */; }; F42953297D561ACAB878DEB8 /* BugsnagFileStore.m in Sources */ = {isa = PBXBuildFile; fileRef = F42958B2E67C338E3086EAC2 /* BugsnagFileStore.m */; }; F42954D219B725C18DA1084F /* BugsnagSessionTrackingApiClient.m in Sources */ = {isa = PBXBuildFile; fileRef = F4295FBD23F478FC6216A006 /* BugsnagSessionTrackingApiClient.m */; }; F42954D2337A7CAE1FE9B308 /* BugsnagFileStore.m in Sources */ = {isa = PBXBuildFile; fileRef = F42958B2E67C338E3086EAC2 /* BugsnagFileStore.m */; }; @@ -658,6 +659,8 @@ E74D8E92243B3DF000F2A630 /* ORGANIZATION.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = ORGANIZATION.md; path = ../ORGANIZATION.md; sourceTree = ""; }; E7529F87243C8DA2006B4932 /* RegisterErrorData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RegisterErrorData.h; path = ../Source/RegisterErrorData.h; sourceTree = ""; }; E7529F88243C8DA2006B4932 /* RegisterErrorData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RegisterErrorData.m; path = ../Source/RegisterErrorData.m; sourceTree = ""; }; + E7529F9B243CAE02006B4932 /* BugsnagThreadSerializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagThreadSerializationTest.m; sourceTree = ""; }; + E7529F9D243CAE0D006B4932 /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagThreadTest.m; path = ../../Tests/BugsnagThreadTest.m; sourceTree = ""; }; E77316E11F73B46600A14F06 /* BugsnagHandledStateTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagHandledStateTest.m; path = ../Tests/BugsnagHandledStateTest.m; sourceTree = SOURCE_ROOT; }; E77526B8242D0AE50077A42F /* BugsnagBreadcrumbs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BugsnagBreadcrumbs.h; path = ../Source/BugsnagBreadcrumbs.h; sourceTree = ""; }; E77526B9242D0AE50077A42F /* BugsnagBreadcrumbs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagBreadcrumbs.m; path = ../Source/BugsnagBreadcrumbs.m; sourceTree = ""; }; @@ -710,7 +713,6 @@ F42954ACC6FFDDE3C8471495 /* BugsnagSessionFileStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagSessionFileStore.m; path = ../Source/BugsnagSessionFileStore.m; sourceTree = SOURCE_ROOT; }; F42954B7D892334E7551F0F3 /* RegisterErrorDataTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegisterErrorDataTest.m; sourceTree = ""; }; F42955025DBE1DCEFD928CAA /* BugsnagSessionFileStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagSessionFileStore.h; path = ../Source/BugsnagSessionFileStore.h; sourceTree = SOURCE_ROOT; }; - F429551527EAE3AFE1F605FE /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagThreadTest.m; sourceTree = ""; }; F42958B2E67C338E3086EAC2 /* BugsnagFileStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagFileStore.m; path = ../Source/BugsnagFileStore.m; sourceTree = SOURCE_ROOT; }; F4295E2778677786239F2B28 /* BugsnagApiClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagApiClient.m; path = ../Source/BugsnagApiClient.m; sourceTree = SOURCE_ROOT; }; F4295FBD23F478FC6216A006 /* BugsnagSessionTrackingApiClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagSessionTrackingApiClient.m; path = ../Source/BugsnagSessionTrackingApiClient.m; sourceTree = SOURCE_ROOT; }; @@ -878,7 +880,8 @@ 00D7ACAC23E9C63000FBE4A7 /* BugsnagTests.m */, 00F9393723FC4F63008C7073 /* BugsnagTestsDummyClass.h */, 00F9393823FC4F64008C7073 /* BugsnagTestsDummyClass.m */, - F429551527EAE3AFE1F605FE /* BugsnagThreadTest.m */, + E7529F9B243CAE02006B4932 /* BugsnagThreadSerializationTest.m */, + E7529F9D243CAE0D006B4932 /* BugsnagThreadTest.m */, E78C1EFD1FCC778700B976D3 /* BugsnagUserTest.m */, F42954B7D892334E7551F0F3 /* RegisterErrorDataTest.m */, 000E6E9C23D8690E009D8194 /* Tests-Bridging-Header.h */, @@ -1482,6 +1485,7 @@ 8A70D9CD2253C484006B696F /* BSGOutOfMemoryWatchdogTests.m in Sources */, E7B970341FD7031500590C27 /* XCTestCase+KSCrash.m in Sources */, 000E6E9E23D8690F009D8194 /* BugsnagSwiftConfigurationTests.swift in Sources */, + E7529F9E243CAE0D006B4932 /* BugsnagThreadTest.m in Sources */, E70EE07E1FD703D600FA745C /* NSError+SimpleConstructor_Tests.m in Sources */, E70EE0931FD706C700FA745C /* KSFileUtils_Tests.m in Sources */, E7AB4B9C2423E16C004F015A /* BugsnagOnBreadcrumbTest.m in Sources */, @@ -1498,6 +1502,7 @@ E7A9E56B2436363900D99F8A /* BugsnagDeviceTest.m in Sources */, E7D2E670243B8F8D005A3041 /* BugsnagStacktraceTest.m in Sources */, E7B970311FD702DA00590C27 /* KSLogger_Tests.m in Sources */, + E7529F9C243CAE02006B4932 /* BugsnagThreadSerializationTest.m in Sources */, 8AA661AD23D8C1F50031ECC8 /* BSGConnectivityTest.m in Sources */, E70EE0921FD706C700FA745C /* KSDynamicLinker_Tests.m in Sources */, E78C1EFC1FCC759B00B976D3 /* BugsnagSessionTest.m in Sources */, @@ -1528,7 +1533,6 @@ 00F9393923FC4F64008C7073 /* BugsnagTestsDummyClass.m in Sources */, F4295F017754324FD52CCE46 /* RegisterErrorDataTest.m in Sources */, 00D7ACAF23EABBC800FBE4A7 /* BugsnagSwiftTests.swift in Sources */, - F42952D83435C02F8D891C40 /* BugsnagThreadTest.m in Sources */, 4B47970A22A9AE1F00FF9C2E /* BugsnagEventFromKSCrashReportTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS/BugsnagTests/BugsnagEventFromKSCrashReportTest.m b/iOS/BugsnagTests/BugsnagEventFromKSCrashReportTest.m index e0ca6f158..b52f9cd53 100644 --- a/iOS/BugsnagTests/BugsnagEventFromKSCrashReportTest.m +++ b/iOS/BugsnagTests/BugsnagEventFromKSCrashReportTest.m @@ -7,10 +7,10 @@ // @import XCTest; -@import Bugsnag; +#import "Bugsnag.h" @interface BugsnagEventFromKSCrashReportTest : XCTestCase -@property BugsnagEvent *report; +@property BugsnagEvent *event; @end @interface BugsnagEvent () @@ -33,35 +33,35 @@ - (void)setUp { JSONObjectWithData:[contents dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; - self.report = [[BugsnagEvent alloc] initWithKSReport:dictionary]; + self.event = [[BugsnagEvent alloc] initWithKSReport:dictionary]; } - (void)tearDown { [super tearDown]; - self.report = nil; + self.event = nil; } - (void)testReportDepth { - XCTAssertEqual(7, self.report.depth); + XCTAssertEqual(7, self.event.depth); } - (void)testReadReleaseStage { - XCTAssertEqualObjects(self.report.app.releaseStage, @"production"); + XCTAssertEqualObjects(self.event.app.releaseStage, @"production"); } - (void)testReadEnabledReleaseStages { - XCTAssertEqualObjects(self.report.enabledReleaseStages, + XCTAssertEqualObjects(self.event.enabledReleaseStages, (@[ @"production", @"development" ])); } - (void)testReadEnabledReleaseStagesSends { - XCTAssertTrue([self.report shouldBeSent]); + XCTAssertTrue([self.event shouldBeSent]); } - (void)testAddMetadataAddsNewTab { NSDictionary *metadata = @{@"color" : @"blue", @"beverage" : @"tea"}; - [self.report addMetadata:metadata toSection:@"user prefs"]; - NSDictionary *prefs = [self.report getMetadataFromSection:@"user prefs"]; + [self.event addMetadata:metadata toSection:@"user prefs"]; + NSDictionary *prefs = [self.event getMetadataFromSection:@"user prefs"]; XCTAssertEqual(@"blue", prefs[@"color"]); XCTAssertEqual(@"tea", prefs[@"beverage"]); XCTAssert([prefs count] == 2); @@ -69,10 +69,10 @@ - (void)testAddMetadataAddsNewTab { - (void)testAddMetadataMergesExistingTab { NSDictionary *oldMetadata = @{@"color" : @"red", @"food" : @"carrots"}; - [self.report addMetadata:oldMetadata toSection:@"user prefs"]; + [self.event addMetadata:oldMetadata toSection:@"user prefs"]; NSDictionary *metadata = @{@"color" : @"blue", @"beverage" : @"tea"}; - [self.report addMetadata:metadata toSection:@"user prefs"]; - NSDictionary *prefs = [self.report getMetadataFromSection:@"user prefs"]; + [self.event addMetadata:metadata toSection:@"user prefs"]; + NSDictionary *prefs = [self.event getMetadataFromSection:@"user prefs"]; XCTAssertEqual(@"blue", prefs[@"color"]); XCTAssertEqual(@"tea", prefs[@"beverage"]); XCTAssertEqual(@"carrots", prefs[@"food"]); @@ -80,39 +80,45 @@ - (void)testAddMetadataMergesExistingTab { } - (void)testAddMetadataAddsNewSection { - [self.report addMetadata:@"blue" - withKey:@"color" - toSection:@"prefs"]; - NSDictionary *prefs = [self.report getMetadataFromSection:@"prefs"]; + [self.event addMetadata:@"blue" + withKey:@"color" + toSection:@"prefs"]; + NSDictionary *prefs = [self.event getMetadataFromSection:@"prefs"]; XCTAssertEqual(@"blue", prefs[@"color"]); } - (void)testAddMetadataOverridesExistingValue { - [self.report addMetadata:@"red" - withKey:@"color" - toSection:@"prefs"]; - [self.report addMetadata:@"blue" - withKey:@"color" - toSection:@"prefs"]; - NSDictionary *prefs = [self.report getMetadataFromSection:@"prefs"]; + [self.event addMetadata:@"red" + withKey:@"color" + toSection:@"prefs"]; + [self.event addMetadata:@"blue" + withKey:@"color" + toSection:@"prefs"]; + NSDictionary *prefs = [self.event getMetadataFromSection:@"prefs"]; XCTAssertEqual(@"blue", prefs[@"color"]); } - (void)testAddMetadataRemovesValue { - [self.report addMetadata:@"prefs" - withKey:@"color" - toSection:@"red"]; - [self.report addMetadata:nil - withKey:@"color" - toSection:@"prefs"]; - NSDictionary *prefs = [self.report getMetadataFromSection:@"prefs"]; + [self.event addMetadata:@"prefs" + withKey:@"color" + toSection:@"red"]; + [self.event addMetadata:nil + withKey:@"color" + toSection:@"prefs"]; + NSDictionary *prefs = [self.event getMetadataFromSection:@"prefs"]; XCTAssertNil(prefs[@"color"]); } - (void)testAppVersion { - NSDictionary *dictionary = [self.report toJson]; + NSDictionary *dictionary = [self.event toJson]; XCTAssertEqualObjects(@"1.0", dictionary[@"app"][@"version"]); XCTAssertEqualObjects(@"1", dictionary[@"app"][@"bundleVersion"]); } +- (void)testThreadsPopulated { + XCTAssertEqual(9, [self.event.threads count]); + BugsnagThread *thread = self.event.threads[0]; + XCTAssertEqualObjects(@"0", thread.id); +} + @end diff --git a/iOS/BugsnagTests/BugsnagThreadTest.m b/iOS/BugsnagTests/BugsnagThreadSerializationTest.m similarity index 79% rename from iOS/BugsnagTests/BugsnagThreadTest.m rename to iOS/BugsnagTests/BugsnagThreadSerializationTest.m index 13f9277ff..72c4d991d 100644 --- a/iOS/BugsnagTests/BugsnagThreadTest.m +++ b/iOS/BugsnagTests/BugsnagThreadSerializationTest.m @@ -8,20 +8,18 @@ #import "BugsnagEvent.h" -@interface BugsnagThreadTest : XCTestCase +@interface BugsnagThreadSerializationTest : XCTestCase @end @interface BugsnagEvent () -- (NSArray *)serializeThreadsWithException:(NSMutableDictionary *)exception; - -@property(nonatomic, readonly, copy, nullable) NSArray *threads; +- (NSDictionary *)toJson; @end -@implementation BugsnagThreadTest +@implementation BugsnagThreadSerializationTest - (void)testEmptyThreads { - BugsnagEvent *report = [self generateReportWithThreads:@[]]; - NSArray *threads = [report serializeThreadsWithException:nil]; + BugsnagEvent *event = [self generateReportWithThreads:@[]]; + NSArray *threads = [event toJson][@"threads"]; XCTAssertTrue(threads.count == 0); } @@ -63,28 +61,27 @@ - (void)testThreadSerialisation { }, ]; - BugsnagEvent *report = [self generateReportWithThreads:trace]; - NSArray *threads = [report serializeThreadsWithException:nil]; + BugsnagEvent *event = [self generateReportWithThreads:trace]; + NSArray *threads = [event toJson][@"threads"]; XCTAssertTrue(threads.count == 2); // first thread is crashed, should be serialised and contain 'errorReportingThread' flag NSDictionary *firstThread = threads[0]; - XCTAssertEqualObjects(@0, firstThread[@"id"]); + XCTAssertEqualObjects(@"0", firstThread[@"id"]); XCTAssertEqualObjects(@"cocoa", firstThread[@"type"]); XCTAssertNotNil(firstThread[@"stacktrace"]); XCTAssertTrue(firstThread[@"errorReportingThread"]); // second thread is not crashed, should not contain 'errorReportingThread' flag NSDictionary *secondThread = threads[1]; - XCTAssertEqualObjects(@1, secondThread[@"id"]); + XCTAssertEqualObjects(@"1", secondThread[@"id"]); XCTAssertEqualObjects(@"cocoa", secondThread[@"type"]); XCTAssertNotNil(secondThread[@"stacktrace"]); - XCTAssertNil(secondThread[@"errorReportingThread"]); + XCTAssertFalse([secondThread[@"errorReportingThread"] boolValue]); } - (BugsnagEvent *)generateReportWithThreads:(NSArray *)threads { return [[BugsnagEvent alloc] initWithKSReport:@{@"crash": @{@"threads": threads}}]; } - @end diff --git a/tvOS/Bugsnag.xcodeproj/project.pbxproj b/tvOS/Bugsnag.xcodeproj/project.pbxproj index 768c546df..d762d14c6 100644 --- a/tvOS/Bugsnag.xcodeproj/project.pbxproj +++ b/tvOS/Bugsnag.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ E7433AD81F4F651200C082D1 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = E7433AD71F4F651200C082D1 /* libz.tbd */; }; E7529F92243C8ED4006B4932 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529F90243C8ED4006B4932 /* RegisterErrorData.m */; }; E7529F93243C8ED4006B4932 /* RegisterErrorData.h in Headers */ = {isa = PBXBuildFile; fileRef = E7529F91243C8ED4006B4932 /* RegisterErrorData.h */; }; + E7529FA6243CAE6A006B4932 /* BugsnagThreadSerializationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7529FA5243CAE6A006B4932 /* BugsnagThreadSerializationTest.m */; }; E762E9F01F73F6CF00E82B43 /* BugsnagHandledStateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E762E9EE1F73F6CF00E82B43 /* BugsnagHandledStateTest.m */; }; E762E9F51F73F7A100E82B43 /* BugsnagHandledState.h in Headers */ = {isa = PBXBuildFile; fileRef = E762E9F21F73F6DE00E82B43 /* BugsnagHandledState.h */; }; E762E9F61F73F7A400E82B43 /* BugsnagHandledState.m in Sources */ = {isa = PBXBuildFile; fileRef = E762E9F31F73F6DE00E82B43 /* BugsnagHandledState.m */; }; @@ -239,7 +240,7 @@ 00F9393D23FD2DDB008C7073 /* BugsnagTestsDummyClass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagTestsDummyClass.h; path = ../../Tests/BugsnagTestsDummyClass.h; sourceTree = ""; }; 00F9393E23FD2DDB008C7073 /* BugsnagTestsDummyClass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagTestsDummyClass.m; path = ../../Tests/BugsnagTestsDummyClass.m; sourceTree = ""; }; 4B3CD2B722C5676700DBFF33 /* BSGOutOfMemoryWatchdogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSGOutOfMemoryWatchdogTests.m; path = ../../iOS/BugsnagTests/BSGOutOfMemoryWatchdogTests.m; sourceTree = ""; }; - 4B3CD2B922C5676700DBFF33 /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagThreadTest.m; path = ../../iOS/BugsnagTests/BugsnagThreadTest.m; sourceTree = ""; }; + 4B3CD2B922C5676700DBFF33 /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagThreadTest.m; path = ../../Tests/BugsnagThreadTest.m; sourceTree = ""; }; 4B3CD2BA22C5676800DBFF33 /* RegisterErrorDataTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegisterErrorDataTest.m; path = ../../iOS/BugsnagTests/RegisterErrorDataTest.m; sourceTree = ""; }; 4B406C1222CAD94100464D1D /* BugsnagCollectionsBSGDictSetSafeObjectTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagCollectionsBSGDictSetSafeObjectTest.m; path = ../../Tests/BugsnagCollectionsBSGDictSetSafeObjectTest.m; sourceTree = ""; }; 4B406C1322CAD94100464D1D /* BugsnagCollectionsBSGDictMergeTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagCollectionsBSGDictMergeTest.m; path = ../../Tests/BugsnagCollectionsBSGDictMergeTest.m; sourceTree = ""; }; @@ -284,6 +285,7 @@ E7433AD71F4F651200C082D1 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; E7529F90243C8ED4006B4932 /* RegisterErrorData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegisterErrorData.m; path = ../Source/RegisterErrorData.m; sourceTree = ""; }; E7529F91243C8ED4006B4932 /* RegisterErrorData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegisterErrorData.h; path = ../Source/RegisterErrorData.h; sourceTree = ""; }; + E7529FA5243CAE6A006B4932 /* BugsnagThreadSerializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagThreadSerializationTest.m; path = ../../iOS/BugsnagTests/BugsnagThreadSerializationTest.m; sourceTree = ""; }; E762E9EE1F73F6CF00E82B43 /* BugsnagHandledStateTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagHandledStateTest.m; path = ../Tests/BugsnagHandledStateTest.m; sourceTree = SOURCE_ROOT; }; E762E9F21F73F6DE00E82B43 /* BugsnagHandledState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BugsnagHandledState.h; path = ../Source/BugsnagHandledState.h; sourceTree = SOURCE_ROOT; }; E762E9F31F73F6DE00E82B43 /* BugsnagHandledState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagHandledState.m; path = ../Source/BugsnagHandledState.m; sourceTree = SOURCE_ROOT; }; @@ -562,6 +564,7 @@ 00F9393E23FD2DDB008C7073 /* BugsnagTestsDummyClass.m */, 4B3CD2B922C5676700DBFF33 /* BugsnagThreadTest.m */, E791488D1FD82E77003EFEBF /* BugsnagUserTest.m */, + E7529FA5243CAE6A006B4932 /* BugsnagThreadSerializationTest.m */, 4B3CD2BA22C5676800DBFF33 /* RegisterErrorDataTest.m */, 00D7ACA823E98A9A00FBE4A7 /* TestConstants.m */, 8AB151201D41361700C9B218 /* report.json */, @@ -1134,6 +1137,7 @@ E77526C7242E694C0077A42F /* BugsnagOnBreadcrumbTest.m in Sources */, E790C4A12434CB5B006FFB26 /* BugsnagAppTest.m in Sources */, 8ACF0F74201812AD00173809 /* BugsnagSinkTests.m in Sources */, + E7529FA6243CAE6A006B4932 /* BugsnagThreadSerializationTest.m in Sources */, 4B3CD2BD22C5676800DBFF33 /* BugsnagThreadTest.m in Sources */, 00D7ACA723E98A5D00FBE4A7 /* BugsnagEventTests.m in Sources */, E7CE78F21FD94F1B001D07E0 /* KSMach_Tests.m in Sources */, From cbed144e30f3c388be77bc74c8ec9c147a28ecc2 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 9 Apr 2020 11:47:51 +0100 Subject: [PATCH 3/3] address review feedback --- Source/BugsnagEvent.m | 7 +++---- Source/BugsnagThread.m | 26 +++++++++++++++++++++++--- Tests/BugsnagThreadTest.m | 4 ++-- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/Source/BugsnagEvent.m b/Source/BugsnagEvent.m index 551123ca9..18c56c04c 100644 --- a/Source/BugsnagEvent.m +++ b/Source/BugsnagEvent.m @@ -75,7 +75,7 @@ + (BugsnagStackframe *)frameFromDict:(NSDictionary *)dict @interface BugsnagThread () @property BugsnagStacktrace *trace; -- (NSDictionary *)toDict; +- (NSDictionary *)toDictionary; - (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)binaryImages; @@ -85,8 +85,7 @@ - (instancetype)initWithThread:(NSDictionary *)thread depth:(NSUInteger)depth errorType:(NSString *)errorType; -+ (NSMutableArray *)serializeThreads:(NSMutableDictionary *)event - threads:(NSArray *)threads; ++ (NSMutableArray *)serializeThreads:(NSArray *)threads; @end @interface BugsnagStacktrace () @@ -578,7 +577,7 @@ - (NSDictionary *)toJson { } } - BSGDictSetSafeObject(event, [BugsnagThread serializeThreads:event threads:self.threads], BSGKeyThreads); + BSGDictSetSafeObject(event, [BugsnagThread serializeThreads:self.threads], BSGKeyThreads); // Build Event BSGDictSetSafeObject(event, BSGFormatSeverity(self.severity), BSGKeySeverity); diff --git a/Source/BugsnagThread.m b/Source/BugsnagThread.m index 75c4ba042..0879d313b 100644 --- a/Source/BugsnagThread.m +++ b/Source/BugsnagThread.m @@ -41,7 +41,7 @@ - (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)bi return self.trace.trace; } -- (NSDictionary *)toDict { +- (NSDictionary *)toDictionary { NSMutableDictionary *dict = [NSMutableDictionary new]; BSGDictInsertIfNotNil(dict, self.id, @"id"); BSGDictInsertIfNotNil(dict, self.name, @"name"); @@ -52,14 +52,20 @@ - (NSDictionary *)toDict { return dict; } -+ (NSMutableArray *)serializeThreads:(NSMutableDictionary *)event threads:(NSArray *)threads { +/** + * Converts bugsnag threads to JSON + */ ++ (NSMutableArray *)serializeThreads:(NSArray *)threads { NSMutableArray *threadArray = [NSMutableArray new]; for (BugsnagThread *thread in threads) { - [threadArray addObject:[thread toDict]]; + [threadArray addObject:[thread toDictionary]]; } return threadArray; } +/** + * Deerializes Bugsnag Threads from a KSCrash report + */ + (NSMutableArray *)threadsFromArray:(NSArray *)threads binaryImages:(NSArray *)binaryImages depth:(NSUInteger)depth @@ -74,6 +80,20 @@ + (NSMutableArray *)serializeThreads:(NSMutableDictionary *)event threads:(NSArr return bugsnagThreads; } +/** + * Enhances the thread information recorded by KSCrash. Specifically, this will trim the error reporting thread frames + * by the `depth` configured, and add information to each frame indicating whether they + * are within the program counter/link register. + * + * The error reporting thread is the thread on which the error occurred, and is given more + * prominence in the Bugsnag Dashboard - therefore we enhance it with extra info. + * + * @param thread the captured thread + * @param depth the 'depth'. This is equivalent to the number of frames which should be discarded from a report, + * and is configurable by the user. + * @param errorType the type of error as recorded by KSCrash (e.g. mach, signal) + * @return the enhanced thread information + */ + (NSDictionary *)enhanceThreadInfo:(NSDictionary *)thread depth:(NSUInteger)depth errorType:(NSString *)errorType { diff --git a/Tests/BugsnagThreadTest.m b/Tests/BugsnagThreadTest.m index a97e8dbbb..fd89e7197 100644 --- a/Tests/BugsnagThreadTest.m +++ b/Tests/BugsnagThreadTest.m @@ -12,7 +12,7 @@ #import "BugsnagThread.h" @interface BugsnagThread () -- (NSDictionary *)toDict; +- (NSDictionary *)toDictionary; - (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)binaryImages; @@ -76,7 +76,7 @@ - (void)testThreadToDict { BugsnagThread *thread = [[BugsnagThread alloc] initWithThread:self.thread binaryImages:self.binaryImages]; thread.name = @"bugsnag-thread-1"; - NSDictionary *dict = [thread toDict]; + NSDictionary *dict = [thread toDictionary]; XCTAssertEqualObjects(@"4", dict[@"id"]); XCTAssertEqualObjects(@"bugsnag-thread-1", dict[@"name"]); XCTAssertEqualObjects(@"cocoa", dict[@"type"]);