From 18595469270f9897a08d6e01c00590175f45ec4f Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 10 Dec 2020 16:58:28 +0000 Subject: [PATCH 01/33] Add unit tests for notification based breadcrumbs --- Bugsnag.xcodeproj/project.pbxproj | 14 ++ Bugsnag/Client/BugsnagClient+Private.h | 8 + Bugsnag/Client/BugsnagClient.m | 19 ++- Tests/BugsnagClientMirrorTest.m | 2 + Tests/NotificationBreadcrumbTests.m | 224 +++++++++++++++++++++++++ Tests/UIDeviceStub.h | 25 +++ Tests/UIDeviceStub.m | 17 ++ 7 files changed, 300 insertions(+), 9 deletions(-) create mode 100644 Tests/NotificationBreadcrumbTests.m create mode 100644 Tests/UIDeviceStub.h create mode 100644 Tests/UIDeviceStub.m diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index 3ba9fe84b..5011da764 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -646,6 +646,10 @@ 0126DF1D257A92860031A70C /* BugsnagSession+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0126DF1A257A92860031A70C /* BugsnagSession+Private.h */; }; 0140D29A25767C9A00FD0306 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 01447605256684500018AB94 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; + 0163BF5925823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; + 0163BF5A25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; + 0163BF5B25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; + 0163BF6525827D9F008DC28B /* UIDeviceStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF6425827D9F008DC28B /* UIDeviceStub.m */; }; 0187D464255BD7B800C503D9 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 01B14C56251CE55F00118748 /* report-react-native-promise-rejection.json in Resources */ = {isa = PBXBuildFile; fileRef = 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */; }; 01B14C57251CE55F00118748 /* report-react-native-promise-rejection.json in Resources */ = {isa = PBXBuildFile; fileRef = 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */; }; @@ -1263,6 +1267,9 @@ 0134524A256BCF7C0088C548 /* BugsnagError+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagError+Private.h"; sourceTree = ""; }; 0134524B256BD00A0088C548 /* BugsnagThread+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagThread+Private.h"; sourceTree = ""; }; 0140D24725765F8F00FD0306 /* BSGUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGUIKit.h; sourceTree = ""; }; + 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationBreadcrumbTests.m; sourceTree = ""; }; + 0163BF6325827D9F008DC28B /* UIDeviceStub.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIDeviceStub.h; sourceTree = ""; }; + 0163BF6425827D9F008DC28B /* UIDeviceStub.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIDeviceStub.m; sourceTree = ""; }; 01937CF9257A7B4C00F2DE31 /* Bugsnag+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bugsnag+Private.h"; sourceTree = ""; }; 01937D01257A7E0E00F2DE31 /* BugsnagErrorReportSink+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagErrorReportSink+Private.h"; sourceTree = ""; }; 01937D09257A7ED000F2DE31 /* BugsnagSessionTracker+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagSessionTracker+Private.h"; sourceTree = ""; }; @@ -1685,6 +1692,7 @@ E701FAAA2490EFD9008D842F /* EventApiValidationTest.m */, 00E636C324878FFC006CBF1A /* Info.plist */, 008966D32486D43700DC48C2 /* KSCrash */, + 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */, 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */, 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */, 008966B72486D43500DC48C2 /* report.json */, @@ -1693,6 +1701,8 @@ CBA2249A251E429C00B87416 /* TestSupport.m */, 01E8765C256684E700F4B70A /* URLSessionMock.h */, 01E8765D256684E700F4B70A /* URLSessionMock.m */, + 0163BF6325827D9F008DC28B /* UIDeviceStub.h */, + 0163BF6425827D9F008DC28B /* UIDeviceStub.m */, 012482A225627B51003F7243 /* UIKitTests.m */, ); path = Tests; @@ -2539,6 +2549,7 @@ 008967902486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, 008967722486D43700DC48C2 /* KSSysCtl_Tests.m in Sources */, 0089676C2486D43700DC48C2 /* BugsnagTestsDummyClass.m in Sources */, + 0163BF6525827D9F008DC28B /* UIDeviceStub.m in Sources */, 008966EB2486D43700DC48C2 /* BugsnagDeviceTest.m in Sources */, CBCF77AB250142E0004AF22A /* BSGJSONSerializerTest.m in Sources */, 008967A22486D43700DC48C2 /* KSCrashSentry_Signal_Tests.m in Sources */, @@ -2590,6 +2601,7 @@ 008967AB2486D43700DC48C2 /* KSMach_Tests.m in Sources */, 0089672A2486D43700DC48C2 /* BugsnagStacktraceTest.m in Sources */, 0089678D2486D43700DC48C2 /* KSCrashReportConverter_Tests.m in Sources */, + 0163BF5925823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */, 008967392486D43700DC48C2 /* BugsnagEventFromKSCrashReportTest.m in Sources */, 008967182486D43700DC48C2 /* BugsnagErrorTest.m in Sources */, 008967212486D43700DC48C2 /* BugsnagErrorReportSinkTests.m in Sources */, @@ -2733,6 +2745,7 @@ 008967702486D43700DC48C2 /* NSError+SimpleConstructor_Tests.m in Sources */, 0089671C2486D43700DC48C2 /* BugsnagSessionTest.m in Sources */, 008967AC2486D43700DC48C2 /* KSMach_Tests.m in Sources */, + 0163BF5A25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */, 00896A452486DBF000DC48C2 /* BugsnagConfigurationTests.m in Sources */, 008967492486D43700DC48C2 /* BugsnagUserTest.m in Sources */, 0089673A2486D43700DC48C2 /* BugsnagEventFromKSCrashReportTest.m in Sources */, @@ -2907,6 +2920,7 @@ E701FAB12490EFE8008D842F /* ConfigurationApiValidationTest.m in Sources */, 0089677D2486D43700DC48C2 /* RFC3339DateTool_Tests.m in Sources */, CBCF77AD250142E0004AF22A /* BSGJSONSerializerTest.m in Sources */, + 0163BF5B25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */, 008967562486D43700DC48C2 /* BugsnagOnCrashTest.m in Sources */, 008967A12486D43700DC48C2 /* KSCrashSentry_Tests.m in Sources */, 008967442486D43700DC48C2 /* BugsnagSessionTrackerStopTest.m in Sources */, diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index f7b835fb3..a937ca721 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -18,6 +18,10 @@ @class BugsnagSessionTracker; @class BugsnagSystemState; +#if TARGET_OS_IOS +@class UIDevice; +#endif + NS_ASSUME_NONNULL_BEGIN @interface BugsnagClient () @@ -43,7 +47,11 @@ NS_ASSUME_NONNULL_BEGIN @property NSMutableDictionary *extraRuntimeInfo; #if TARGET_OS_IOS + +@property (strong, nonatomic) UIDevice *uiDevice; // Used for unit testing + @property (strong, nonatomic) NSString *lastOrientation; + #endif @property (strong, nonatomic) BugsnagMetadata *metadata; // Used in BugsnagReactNative diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 71ff25e80..a44e3a455 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -324,7 +324,8 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { client:self]; #if BSG_PLATFORM_IOS - _lastOrientation = BSGOrientationNameFromEnum([UIDEVICE currentDevice].orientation); + _uiDevice = [UIDEVICE currentDevice]; + _lastOrientation = BSGOrientationNameFromEnum(_uiDevice.orientation); #endif if (self.user.id == nil) { // populate with an autogenerated ID if no value set @@ -471,8 +472,8 @@ - (void)start { name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - [UIDEVICE currentDevice].batteryMonitoringEnabled = YES; - [[UIDEVICE currentDevice] beginGeneratingDeviceOrientationNotifications]; + self.uiDevice.batteryMonitoringEnabled = YES; + [self.uiDevice beginGeneratingDeviceOrientationNotifications]; [self batteryChanged:nil]; [self orientationChanged:nil]; @@ -640,8 +641,8 @@ - (void)unsubscribeFromNotifications:(id)sender { [BSGConnectivity stopMonitoring]; #if BSG_PLATFORM_IOS - [UIDEVICE currentDevice].batteryMonitoringEnabled = NO; - [[UIDEVICE currentDevice] endGeneratingDeviceOrientationNotifications]; + self.uiDevice.batteryMonitoringEnabled = NO; + [self.uiDevice endGeneratingDeviceOrientationNotifications]; #endif } @@ -1072,9 +1073,9 @@ - (void)metadataChanged:(BugsnagMetadata *)metadata { */ #if BSG_PLATFORM_IOS - (void)batteryChanged:(NSNotification *)notification { - NSNumber *batteryLevel = @([UIDEVICE currentDevice].batteryLevel); - BOOL charging = [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateCharging || - [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateFull; + NSNumber *batteryLevel = @(self.uiDevice.batteryLevel); + BOOL charging = self.uiDevice.batteryState == UIDeviceBatteryStateCharging || + self.uiDevice.batteryState == UIDeviceBatteryStateFull; [self.state addMetadata:batteryLevel withKey:BSGKeyBatteryLevel @@ -1092,7 +1093,7 @@ - (void)batteryChanged:(NSNotification *)notification { * @param notification The orientation-change notification */ - (void)orientationChanged:(NSNotification *)notification { - UIDeviceOrientation currentDeviceOrientation = [UIDEVICE currentDevice].orientation; + UIDeviceOrientation currentDeviceOrientation = self.uiDevice.orientation; NSString *orientation = BSGOrientationNameFromEnum(currentDeviceOrientation); // No orientation, nothing to be done diff --git a/Tests/BugsnagClientMirrorTest.m b/Tests/BugsnagClientMirrorTest.m index 53f2b903b..8f3cbcf32 100644 --- a/Tests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagClientMirrorTest.m @@ -131,8 +131,10 @@ - (void)setUp { @"setConfigMetadataFromLastLaunch: v24@0:8@16", @"setMetadataFromLastLaunch: v24@0:8@16", @"setStateMetadataFromLastLaunch: v24@0:8@16", + @"setUiDevice: v24@0:8@16", @"stateMetadataFile @16@0:8", @"stateMetadataFromLastLaunch @16@0:8", + @"uiDevice @16@0:8", ]]; // the following methods are implemented on Bugsnag but do not need to diff --git a/Tests/NotificationBreadcrumbTests.m b/Tests/NotificationBreadcrumbTests.m new file mode 100644 index 000000000..d111efbef --- /dev/null +++ b/Tests/NotificationBreadcrumbTests.m @@ -0,0 +1,224 @@ +// +// NotificationBreadcrumbTests.m +// Bugsnag +// +// Created by Nick Dowell on 10/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import + +#import "BugsnagBreadcrumb+Private.h" +#import "BugsnagBreadcrumbs.h" +#import "BugsnagClient+Private.h" + +#if TARGET_OS_IOS +#import "UIDeviceStub.h" +#endif + + +@interface NotificationBreadcrumbTests : XCTestCase + +@property NSNotificationCenter *notificationCenter; +@property id notificationObject; +@property NSDictionary *notificationUserInfo; + +@property BugsnagClient *client; + +@property (nonatomic) BugsnagBreadcrumb *breadcrumb; + +@end + + +#pragma mark - + +@implementation NotificationBreadcrumbTests + +#pragma mark Setup + +- (void)setUp { + self.breadcrumb = nil; + BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:@"0192837465afbecd0192837465afbecd"]; + self.client = [[BugsnagClient alloc] initWithConfiguration:configuration]; + self.client.breadcrumbs = (id)self; +#if TARGET_OS_IOS + self.client.uiDevice = (id)[[UIDeviceStub alloc] init]; + ((UIDeviceStub *)self.client.uiDevice).orientation = UIDeviceOrientationPortrait; +#endif + [self.client start]; + self.notificationCenter = NSNotificationCenter.defaultCenter; + self.notificationObject = nil; + self.notificationUserInfo = nil; +} + +- (BugsnagBreadcrumb *)breadcrumbForNotificationWithName:(NSString *)name { + self.breadcrumb = nil; + [self.notificationCenter postNotification: + [NSNotification notificationWithName:name object:self.notificationObject userInfo:self.notificationUserInfo]]; + return self.breadcrumb; +} + +#define TEST(__NAME__, __TYPE__, __MESSAGE__, __METADATA__) do { \ + BugsnagBreadcrumb *breadcrumb = [self breadcrumbForNotificationWithName:__NAME__]; \ + XCTAssertNotNil(breadcrumb); \ + if (breadcrumb) { \ + XCTAssertEqual(breadcrumb.type, __TYPE__); \ + XCTAssertEqualObjects(breadcrumb.message, __MESSAGE__); \ + XCTAssertEqualObjects(breadcrumb.metadata, __METADATA__); \ + } \ +} while (0) + +#pragma mark Tests + +- (void)testStartedBreadcrumb { + XCTAssertEqualObjects(self.breadcrumb.message, @"Bugsnag loaded"); +} + +#pragma mark iOS Tests + +#if TARGET_OS_IOS + +- (void)testUIApplicationNotifications { + TEST(UIApplicationDidEnterBackgroundNotification, BSGBreadcrumbTypeState, @"App Did Enter Background", @{}); + TEST(UIApplicationDidReceiveMemoryWarningNotification, BSGBreadcrumbTypeState, @"Memory Warning", @{}); + TEST(UIApplicationUserDidTakeScreenshotNotification, BSGBreadcrumbTypeState, @"Took Screenshot", @{}); + TEST(UIApplicationWillEnterForegroundNotification, BSGBreadcrumbTypeState, @"App Will Enter Foreground", @{}); + TEST(UIApplicationWillTerminateNotification, BSGBreadcrumbTypeState, @"App Will Terminate", @{}); +} + +- (void)testUIDeviceNotifications { + ((UIDeviceStub *)self.client.uiDevice).orientation = UIDeviceOrientationLandscapeLeft; + TEST(UIDeviceOrientationDidChangeNotification, BSGBreadcrumbTypeState, @"Orientation Changed", + (@{@"from": @"portrait", @"to": @"landscapeleft"})); +} + +- (void)testUIKeyboardNotifications { + TEST(UIKeyboardDidHideNotification, BSGBreadcrumbTypeState, @"Keyboard Became Hidden", @{}); + TEST(UIKeyboardDidShowNotification, BSGBreadcrumbTypeState, @"Keyboard Became Visible", @{}); +} + +- (void)testUIMenuNotifications { + TEST(UIMenuControllerDidHideMenuNotification, BSGBreadcrumbTypeState, @"Did Hide Menu", @{}); + TEST(UIMenuControllerDidShowMenuNotification, BSGBreadcrumbTypeState, @"Did Show Menu", @{}); +} + +- (void)testUITextFieldNotifications { + TEST(UITextFieldTextDidBeginEditingNotification, BSGBreadcrumbTypeUser, @"Began Editing Text", @{}); + TEST(UITextFieldTextDidEndEditingNotification, BSGBreadcrumbTypeUser, @"Stopped Editing Text", @{}); +} + +- (void)testUITextViewNotifications { + TEST(UITextViewTextDidBeginEditingNotification, BSGBreadcrumbTypeUser, @"Began Editing Text", @{}); + TEST(UITextViewTextDidEndEditingNotification, BSGBreadcrumbTypeUser, @"Stopped Editing Text", @{}); +} + +- (void)testUIWindowNotifications { + TEST(UIWindowDidBecomeHiddenNotification, BSGBreadcrumbTypeState, @"Window Became Hidden", @{}); + TEST(UIWindowDidBecomeVisibleNotification, BSGBreadcrumbTypeState, @"Window Became Visible", @{}); +} + +#endif + +#pragma mark iOS & tvOS Tests + +#if TARGET_OS_IOS || TARGET_OS_TV + +// This should be on macOS too! +- (void)testNSUndoManagerNotifications { + TEST(NSUndoManagerDidRedoChangeNotification, BSGBreadcrumbTypeState, @"Redo Operation", @{}); + TEST(NSUndoManagerDidUndoChangeNotification, BSGBreadcrumbTypeState, @"Undo Operation", @{}); +} + +- (void)testUITableViewNotifications { + TEST(UITableViewSelectionDidChangeNotification, BSGBreadcrumbTypeNavigation, @"TableView Select Change", @{}); +} + +#endif + +#pragma mark tvOS Tests + +#if TARGET_OS_TV + +- (void)testUIScreenNotifications { + TEST(UIScreenBrightnessDidChangeNotification, BSGBreadcrumbTypeState, @"Screen Brightness Changed", @{}); +} + +- (void)testUIWindowNotifications { + TEST(UIWindowDidBecomeHiddenNotification, BSGBreadcrumbTypeState, @"Window Became Hidden", @{}); + TEST(UIWindowDidBecomeKeyNotification, BSGBreadcrumbTypeState, @"Window Became Key", @{}); + TEST(UIWindowDidBecomeVisibleNotification, BSGBreadcrumbTypeState, @"Window Became Visible", @{}); + TEST(UIWindowDidResignKeyNotification, BSGBreadcrumbTypeState, @"Window Resigned Key", @{}); +} + +#endif + +#pragma mark macOS Tests + +#if TARGET_OS_OSX + +- (void)testNSApplicationNotifications { + TEST(NSApplicationDidBecomeActiveNotification, BSGBreadcrumbTypeState, @"App Became Active", @{}); + TEST(NSApplicationDidBecomeActiveNotification, BSGBreadcrumbTypeState, @"App Became Active", @{}); + TEST(NSApplicationDidHideNotification, BSGBreadcrumbTypeState, @"App Did Hide", @{}); + TEST(NSApplicationDidResignActiveNotification, BSGBreadcrumbTypeState, @"App Resigned Active", @{}); + TEST(NSApplicationDidUnhideNotification, BSGBreadcrumbTypeState, @"App Did Unhide", @{}); + TEST(NSApplicationWillTerminateNotification, BSGBreadcrumbTypeState, @"App Will Terminate", @{}); +} + +- (void)testNSControlNotifications { + self.notificationObject = ({ + NSControl *control = [[NSControl alloc] init]; + control.accessibilityLabel = @"button1"; + control; + }); + TEST(NSControlTextDidBeginEditingNotification, BSGBreadcrumbTypeUser, @"Control Text Began Edit", @{@"label": @"button1"}); + TEST(NSControlTextDidEndEditingNotification, BSGBreadcrumbTypeUser, @"Control Text Ended Edit", @{@"label": @"button1"}); +} + +- (void)testNSMenuNotifications { + self.notificationUserInfo = @{@"MenuItem": [[NSMenuItem alloc] initWithTitle:@"menuAction:" action:nil keyEquivalent:@""]}; + TEST(NSMenuWillSendActionNotification, BSGBreadcrumbTypeState, @"Menu Will Send Action", @{@"action": @"menuAction:"}); +} + +- (void)testNSTableViewNotifications { + self.notificationObject = [[NSTableView alloc] init]; + TEST(NSTableViewSelectionDidChangeNotification, BSGBreadcrumbTypeNavigation, @"TableView Select Change", + (@{@"selectedColumn": @(-1), @"selectedRow": @(-1)})); +} + +- (void)testNSWindowNotifications { + TEST(NSWindowDidBecomeKeyNotification, BSGBreadcrumbTypeState, @"Window Became Key", @{}); + TEST(NSWindowDidEnterFullScreenNotification, BSGBreadcrumbTypeState, @"Window Entered Full Screen", @{}); + TEST(NSWindowDidExitFullScreenNotification, BSGBreadcrumbTypeState, @"Window Exited Full Screen", @{}); + TEST(NSWindowWillCloseNotification, BSGBreadcrumbTypeState, @"Window Will Close", @{}); + TEST(NSWindowWillMiniaturizeNotification, BSGBreadcrumbTypeState, @"Window Will Miniaturize", @{}); +} + +- (void)testNSWorkspaceNotifications { + self.notificationCenter = NSWorkspace.sharedWorkspace.notificationCenter; + TEST(NSWorkspaceScreensDidSleepNotification, BSGBreadcrumbTypeState, @"Workspace Screen Slept", @{}); + TEST(NSWorkspaceScreensDidWakeNotification, BSGBreadcrumbTypeState, @"Workspace Screen Awoke", @{}); +} + +#endif + +@end + + +#pragma mark - + +@implementation NotificationBreadcrumbTests (BugsnagBreadcrumbsMock) + +- (void)removeAllBreadcrumbs {} + +- (void)addBreadcrumb:(NSString *)message {} + +- (void)addBreadcrumbWithBlock:(BSGBreadcrumbConfiguration)block { + self.breadcrumb = [BugsnagBreadcrumb breadcrumbWithBlock:block]; +} + +- (NSArray *)breadcrumbs { + return @[]; +} + +@end diff --git a/Tests/UIDeviceStub.h b/Tests/UIDeviceStub.h new file mode 100644 index 000000000..c443a55b1 --- /dev/null +++ b/Tests/UIDeviceStub.h @@ -0,0 +1,25 @@ +// +// UIDeviceStub.h +// Bugsnag-iOSTests +// +// Created by Nick Dowell on 10/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UIDeviceStub : NSObject + +@property double batteryLevel; + +@property BOOL batteryMonitoringEnabled; + +@property UIDeviceBatteryState batteryState; + +@property UIDeviceOrientation orientation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/UIDeviceStub.m b/Tests/UIDeviceStub.m new file mode 100644 index 000000000..bc7b32a4e --- /dev/null +++ b/Tests/UIDeviceStub.m @@ -0,0 +1,17 @@ +// +// UIDeviceStub.m +// Bugsnag-iOSTests +// +// Created by Nick Dowell on 10/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import "UIDeviceStub.h" + +@implementation UIDeviceStub + +- (void)beginGeneratingDeviceOrientationNotifications {} + +- (void)endGeneratingDeviceOrientationNotifications {} + +@end From 865204295fff472183179d2c93555d627478fd9c Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Fri, 11 Dec 2020 15:54:45 +0000 Subject: [PATCH 02/33] Implement Configuration.discardClasses --- Bugsnag/BugsnagErrorReportSink.m | 7 ++ Bugsnag/Client/BugsnagClient.m | 6 ++ .../BugsnagConfiguration+Private.h | 2 + Bugsnag/Configuration/BugsnagConfiguration.m | 16 ++++ Bugsnag/Payload/BugsnagEvent+Private.h | 2 +- .../include/Bugsnag/BugsnagConfiguration.h | 12 +++ Tests/BugsnagConfigurationTests.m | 70 +++++++++++++++++ features/discard_classes.feature | 21 +++++ .../iOSTestApp.xcodeproj/project.pbxproj | 4 + .../macOSTestApp.xcodeproj/project.pbxproj | 8 +- .../scenarios/DiscardClassesScenarios.swift | 77 +++++++++++++++++++ features/fixtures/shared/scenarios/Scenario.h | 3 + features/fixtures/shared/scenarios/Scenario.m | 4 + 13 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 features/discard_classes.feature create mode 100644 features/fixtures/shared/scenarios/DiscardClassesScenarios.swift diff --git a/Bugsnag/BugsnagErrorReportSink.m b/Bugsnag/BugsnagErrorReportSink.m index 555d764a0..7fcb5e49b 100644 --- a/Bugsnag/BugsnagErrorReportSink.m +++ b/Bugsnag/BugsnagErrorReportSink.m @@ -95,6 +95,13 @@ - (void)sendStoredReports:(NSDictionary *)ksCrashRe NSDictionary *report = ksCrashReports[fileKey]; BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:report]; event.redactedKeys = configuration.redactedKeys; + + NSString *errorClass = event.errors.firstObject.errorClass; + if ([configuration shouldDiscardErrorClass:errorClass]) { + bsg_log_info(@"Discarding event because errorClass \"%@\" matched configuration.discardClasses", errorClass); + [self finishActiveRequest:fileKey completed:YES error:nil block:block]; + continue; + } if ([event shouldBeSent] && [self runOnSendBlocks:configuration event:event]) { storedEvents[fileKey] = event; diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 71ff25e80..0669bdfc3 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -964,6 +964,12 @@ - (void)notify:(NSException *)exception - (void)notifyInternal:(BugsnagEvent *_Nonnull)event block:(BugsnagOnErrorBlock)block { + NSString *errorClass = event.errors.firstObject.errorClass; + if ([self.configuration shouldDiscardErrorClass:errorClass]) { + bsg_log_info(@"Discarding event because errorClass \"%@\" matched configuration.discardClasses", errorClass); + return; + } + // enhance device information with additional metadata NSDictionary *deviceFields = [self.state getMetadataFromSection:BSGKeyDeviceState]; diff --git a/Bugsnag/Configuration/BugsnagConfiguration+Private.h b/Bugsnag/Configuration/BugsnagConfiguration+Private.h index 9e20d5c89..3c28de622 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration+Private.h +++ b/Bugsnag/Configuration/BugsnagConfiguration+Private.h @@ -49,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)deletePersistedUserData; +- (BOOL)shouldDiscardErrorClass:(NSString *)errorClass; + - (BOOL)shouldRecordBreadcrumbType:(BSGBreadcrumbType)breadcrumbType; /// Throws an NSInvalidArgumentException if the API key is empty or missing. diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 1d9c04368..52ab7d4cb 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -82,6 +82,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { [copy setEnabledBreadcrumbTypes:self.enabledBreadcrumbTypes]; [copy setEnabledErrorTypes:self.enabledErrorTypes]; [copy setEnabledReleaseStages:self.enabledReleaseStages]; + copy.discardClasses = self.discardClasses; [copy setRedactedKeys:self.redactedKeys]; [copy setMaxBreadcrumbs:self.maxBreadcrumbs]; copy->_metadata = [[BugsnagMetadata alloc] initWithDictionary:[[self.metadata toDictionary] mutableCopy]]; @@ -435,6 +436,21 @@ - (void)setMaxBreadcrumbs:(NSUInteger)maxBreadcrumbs { } } +- (BOOL)shouldDiscardErrorClass:(NSString *)errorClass { + for (id obj in self.discardClasses) { + if ([obj isKindOfClass:[NSString class]]) { + if ([obj isEqualToString:errorClass]) { + return YES; + } + } else if ([obj isKindOfClass:[NSRegularExpression class]]) { + if ([obj firstMatchInString:errorClass options:0 range:NSMakeRange(0, errorClass.length)]) { + return YES; + } + } + } + return NO; +} + /** * Specific types of breadcrumb should be recorded if either enabledBreadcrumbTypes * is None, or contains the type. diff --git a/Bugsnag/Payload/BugsnagEvent+Private.h b/Bugsnag/Payload/BugsnagEvent+Private.h index 745d6b1e1..4104d4814 100644 --- a/Bugsnag/Payload/BugsnagEvent+Private.h +++ b/Bugsnag/Payload/BugsnagEvent+Private.h @@ -37,7 +37,7 @@ NS_ASSUME_NONNULL_BEGIN /// Property overrides. @property (readonly, copy) NSDictionary *overrides; -@property NSSet *redactedKeys; +@property NSSet *redactedKeys; /// The release stage of the application @property (readwrite, copy, nullable) NSString *releaseStage; diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index 844f4406e..0dbb3a276 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -149,6 +149,18 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); */ @property(readwrite, retain, nullable) NSSet *redactedKeys; +/** + * A set of strings and NSRegularExpression objects that determine which errors will be + * immediately discarded, based on the value of `BugsnagError.errorClass`. + * + * OnError / OnSendError blocks will not be called for discarded errors. + * + * Some examples of errorClass are: Objective-C exception names like "NSRangeException", + * signal names like "SIGABRT", mach exception names like "EXC_BREAKPOINT", and Swift + * error names like "Fatal error". + */ +@property(readwrite, copy, nullable) NSSet *discardClasses; + /** * A general summary of what was occuring in the application */ diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index e7a11bd9f..1a94cc5a0 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -850,4 +850,74 @@ - (void)testMetadataMutability { XCTAssertTrue([metadata2 isKindOfClass:[NSMutableDictionary class]]); } +- (void)testDiscardClasses { + XCTAssertNil([[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1].discardClasses, @"discardClasses should be nil be default"); + + NSArray *errorClasses = @[@"EXC_BAD_ACCESS", + @"EXC_BAD_INSTRUCTION", + @"EXC_BREAKPOINT", + @"Exception", + @"Fatal error", + @"NSError", + @"NSGenericException", + @"NSInternalInconsistencyException", + @"NSMallocException", + @"NSRangeException", + @"SIGABRT", + @"UIViewControllerHierarchyInconsistency", + @"std::__1::system_error"]; + + __block NSArray *discarded, *kept; + + void (^ applyDiscardClasses)(NSSet *) = ^(NSSet *discardClasses){ + BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + configuration.discardClasses = discardClasses; + NSPredicate *shouldDiscard = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + return [configuration shouldDiscardErrorClass:evaluatedObject]; + }]; + discarded = [errorClasses filteredArrayUsingPredicate:shouldDiscard]; + kept = [errorClasses filteredArrayUsingPredicate:[NSCompoundPredicate notPredicateWithSubpredicate:shouldDiscard]]; + }; + + applyDiscardClasses(nil); + XCTAssertEqualObjects(discarded, @[]); + XCTAssertEqualObjects(kept, errorClasses); + + applyDiscardClasses([NSSet setWithObjects:@"nserror", nil]); + XCTAssertEqualObjects(discarded, @[]); + XCTAssertEqualObjects(kept, errorClasses); + + applyDiscardClasses([NSSet setWithObjects:@"EXC_BAD_ACCESS", @"NSError", nil]); + XCTAssertEqualObjects(discarded, (@[@"EXC_BAD_ACCESS", @"NSError"])); + XCTAssertEqualObjects(kept, (@[@"EXC_BAD_INSTRUCTION", + @"EXC_BREAKPOINT", + @"Exception", + @"Fatal error", + @"NSGenericException", + @"NSInternalInconsistencyException", + @"NSMallocException", + @"NSRangeException", + @"SIGABRT", + @"UIViewControllerHierarchyInconsistency", + @"std::__1::system_error"])); + + applyDiscardClasses([NSSet setWithObjects:@"Exception", @"NSError", + [NSRegularExpression regularExpressionWithPattern:@"std::__1::.*" options:0 error:nil], nil]); + XCTAssertEqualObjects(discarded, (@[@"Exception", @"NSError", @"std::__1::system_error"])); + XCTAssertEqualObjects(kept, (@[@"EXC_BAD_ACCESS", + @"EXC_BAD_INSTRUCTION", + @"EXC_BREAKPOINT", + @"Fatal error", + @"NSGenericException", + @"NSInternalInconsistencyException", + @"NSMallocException", + @"NSRangeException", + @"SIGABRT", + @"UIViewControllerHierarchyInconsistency"])); + + applyDiscardClasses([NSSet setWithObjects:[NSRegularExpression regularExpressionWithPattern:@".*" options:0 error:nil], nil]); + XCTAssertEqualObjects(discarded, errorClasses); + XCTAssertEqualObjects(kept, (@[])); +} + @end diff --git a/features/discard_classes.feature b/features/discard_classes.feature new file mode 100644 index 000000000..cc3a16b14 --- /dev/null +++ b/features/discard_classes.feature @@ -0,0 +1,21 @@ +Feature: Configuration discardClasses option + + Background: + Given I clear all persistent data + + Scenario: Discard handled exception via regular expression + When I run "DiscardClassesHandledExceptionRegexScenario" + And I wait to receive a request + And the exception "errorClass" equals "NotDiscarded" + + Scenario: Discard unhandled exception + When I run "DiscardClassesUnhandledExceptionScenario" and relaunch the app + And I configure Bugsnag for "DiscardClassesUnhandledExceptionScenario" + And I wait to receive a request + And the exception "errorClass" equals "NotDiscarded" + + Scenario: Discard unhandled crash + When I run "DiscardClassesUnhandledCrashScenario" and relaunch the app + And I configure Bugsnag for "DiscardClassesUnhandledCrashScenario" + And I wait to receive a request + And the exception "errorClass" equals "NotDiscarded" diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj index c74ec9cc4..bff7d2ffc 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 00432CC4240912A100826D05 /* EnabledErrorTypesCxxScenario.mm in Sources */ = {isa = PBXBuildFile; fileRef = 00432CC2240912A000826D05 /* EnabledErrorTypesCxxScenario.mm */; }; 00507A64242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00507A63242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift */; }; 00CEB60D24080C690004793D /* EnabledErrorTypesScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */; }; + 0163BFA72583B3CF008DC28B /* DiscardClassesScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */; }; 6526A0D4248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */; }; 8A14F0F62282D4AE00337B05 /* (null) in Sources */ = {isa = PBXBuildFile; }; 8A32DB8222424E3000EDD92F /* NSExceptionShiftScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */; }; @@ -159,6 +160,7 @@ 00A98315240DBB7A0016A57E /* out_of_memory.feature */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = out_of_memory.feature; path = ../../../out_of_memory.feature; sourceTree = ""; }; 00CEB60B24080C690004793D /* EnabledErrorTypesScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnabledErrorTypesScenario.h; sourceTree = ""; }; 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnabledErrorTypesScenario.m; sourceTree = ""; }; + 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardClassesScenarios.swift; sourceTree = ""; }; 4994F05E0421A0B037DD2CC5 /* Pods_iOSTestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSTestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; 8A32DB8022424E3000EDD92F /* NSExceptionShiftScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSExceptionShiftScenario.h; sourceTree = ""; }; @@ -549,6 +551,7 @@ F49695A3243EF7B600105DA9 /* OOMs */, F49695AE2445476700105DA9 /* Plugin */, 0037410E2473CF2300BE41AA /* AppAndDeviceAttributesScenario.swift */, + 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */, 8AB1081823301FE600672818 /* ReleaseStageScenarios.swift */, F4295ABA693D273D52AA9F6B /* Scenario.h */, F42954E8B66F3FB7F5333CF7 /* Scenario.m */, @@ -877,6 +880,7 @@ 8AB65FCC22DC77CB001200AB /* LoadConfigFromFileScenario.swift in Sources */, E7B79CD4247FD6760039FB88 /* ManualContextOnErrorScenario.swift in Sources */, E7767F13221C21E30006648C /* ResumedSessionScenario.swift in Sources */, + 0163BFA72583B3CF008DC28B /* DiscardClassesScenarios.swift in Sources */, E700EE69247D73F8008CFFB6 /* UnhandledMachExceptionScenario.m in Sources */, E75040B12478214F005D33BD /* MetadataRedactionRegexScenario.swift in Sources */, E700EE5B247D3224008CFFB6 /* OriginalErrorNSExceptionScenario.swift in Sources */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index d01511523..ef03089f9 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 01452354254AFD7C00D436AA /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 01452353254AFD7C00D436AA /* Bugsnag.framework */; }; 01452355254AFD7C00D436AA /* Bugsnag.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 01452353254AFD7C00D436AA /* Bugsnag.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0163BF9B2583AF2A008DC28B /* DiscardClassesScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF9A2583AF2A008DC28B /* DiscardClassesScenarios.swift */; }; 0176C0AE254AE81B0066E0F3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0176C0AD254AE81B0066E0F3 /* main.m */; }; 0176C0B1254AE81B0066E0F3 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0176C0B0254AE81B0066E0F3 /* AppDelegate.m */; }; 0176C0B3254AE81B0066E0F3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0176C0B2254AE81B0066E0F3 /* Images.xcassets */; }; @@ -146,6 +147,7 @@ /* Begin PBXFileReference section */ 01452353254AFD7C00D436AA /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 01452360254AFEA700D436AA /* macOSTestApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "macOSTestApp-Bridging-Header.h"; sourceTree = ""; }; + 0163BF9A2583AF2A008DC28B /* DiscardClassesScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscardClassesScenarios.swift; sourceTree = ""; }; 0176C0A8254AE81B0066E0F3 /* macOSTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macOSTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0176C0AC254AE81B0066E0F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; usesTabs = 1; }; 0176C0AD254AE81B0066E0F3 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -380,6 +382,7 @@ 01F47C8F254B1B2F00B184AD /* CxxExceptionScenario.mm */, 01F47C43254B1B2D00B184AD /* DisabledSessionTrackingScenario.h */, 01F47C88254B1B2F00B184AD /* DisabledSessionTrackingScenario.m */, + 0163BF9A2583AF2A008DC28B /* DiscardClassesScenarios.swift */, 01F47CA8254B1B3000B184AD /* DiscardedBreadcrumbTypeScenario.swift */, 01F47CB6254B1B3000B184AD /* DiscardSessionScenario.swift */, 01F47C3D254B1B2D00B184AD /* EnabledBreadcrumbTypesIsNilScenario.swift */, @@ -402,6 +405,8 @@ 01F47C56254B1B2E00B184AD /* ManualSessionWithUserScenario.m */, 01F47C33254B1B2D00B184AD /* ManyConcurrentNotifyScenario.h */, 01F47C6E254B1B2E00B184AD /* ManyConcurrentNotifyScenario.m */, + CBB7878D2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.h */, + CBB7878C2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.m */, 01F47CBD254B1B3000B184AD /* MetadataMergeScenario.swift */, 01F47C29254B1B2C00B184AD /* MetadataRedactionDefaultScenario.swift */, 01F47C80254B1B2F00B184AD /* MetadataRedactionNestedScenario.swift */, @@ -489,8 +494,6 @@ 01F47C90254B1B2F00B184AD /* UnhandledInternalNotifyScenario.swift */, 01F47C38254B1B2D00B184AD /* UnhandledMachExceptionScenario.h */, 01F47C4C254B1B2D00B184AD /* UnhandledMachExceptionScenario.m */, - CBB7878D2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.h */, - CBB7878C2578FB3F0071BDE4 /* MarkUnhandledHandledScenario.m */, 01F47CB7254B1B3000B184AD /* UserDefaultInfoScenario.swift */, 01F47CB3254B1B3000B184AD /* UserDisabledScenario.swift */, 01F47C75254B1B2E00B184AD /* UserEmailScenario.swift */, @@ -686,6 +689,7 @@ 017FBFB8254B09C300809042 /* MainWindowController.m in Sources */, 01F47CF0254B1B3100B184AD /* OnSendOverwriteScenario.swift in Sources */, 01F47D1B254B1B3100B184AD /* SIGSEGVScenario.m in Sources */, + 0163BF9B2583AF2A008DC28B /* DiscardClassesScenarios.swift in Sources */, 01F47D09254B1B3100B184AD /* BreadcrumbCallbackOverrideScenario.swift in Sources */, 0176C0B1254AE81B0066E0F3 /* AppDelegate.m in Sources */, 01F47CD7254B1B3100B184AD /* AutoDetectFalseHandledScenario.swift in Sources */, diff --git a/features/fixtures/shared/scenarios/DiscardClassesScenarios.swift b/features/fixtures/shared/scenarios/DiscardClassesScenarios.swift new file mode 100644 index 000000000..326048b47 --- /dev/null +++ b/features/fixtures/shared/scenarios/DiscardClassesScenarios.swift @@ -0,0 +1,77 @@ +// +// DiscardClassesScenarios.swift +// macOSTestApp +// +// Created by Nick Dowell on 11/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +extension NSExceptionName { + + /// An exception name that should not be discarded by the discardClasses values in these scenarios. + static let notDiscarded = NSExceptionName("NotDiscarded") +} + +// MARK: - + +class DiscardClassesHandledExceptionRegexScenario: Scenario { + + override func startBugsnag() { + config.autoTrackSessions = false + config.discardClasses = try! [NSRegularExpression(pattern: #"NS\w+Exception"#)] + super.startBugsnag() + } + + override func run() { + Bugsnag.notify(NSException(name: .genericException, reason: "This exception should be discarded")) { _ in + fatalError("OnError should not be called for discarded errors") + } + Bugsnag.notify(NSException(name: .notDiscarded, reason: "This exception should not be discarded")) + } +} + +// MARK: - + +class DiscardClassesUnhandledExceptionScenario: Scenario { + + override func startBugsnag() { + config.autoTrackSessions = false + config.discardClasses = [NSExceptionName.rangeException.rawValue] + config.addOnSendError { + precondition(!$0.unhandled, "OnSendError should not be called for discarded errors (NSRangeException)") + return true + } + super.startBugsnag() + + if Bugsnag.appDidCrashLastLaunch() { + Bugsnag.notify(NSException(name: .notDiscarded, reason: "This exception should not be discarded")) + } + } + + override func run() { + NSArray().object(at: 0) + } +} + +// MARK: - + +class DiscardClassesUnhandledCrashScenario: Scenario { + + override func startBugsnag() { + config.autoTrackSessions = false + config.discardClasses = ["EXC_BREAKPOINT"] + config.addOnSendError { + precondition(!$0.unhandled, "OnSendError should not be called for discarded errors (EXC_BREAKPOINT)") + return true + } + super.startBugsnag() + + if Bugsnag.appDidCrashLastLaunch() { + Bugsnag.notify(NSException(name: .notDiscarded, reason: "This exception should not be discarded")) + } + } + + override func run() { + triggerExcBreakpoint() + } +} diff --git a/features/fixtures/shared/scenarios/Scenario.h b/features/fixtures/shared/scenarios/Scenario.h index 2e1c392f2..6eff8876c 100644 --- a/features/fixtures/shared/scenarios/Scenario.h +++ b/features/fixtures/shared/scenarios/Scenario.h @@ -26,5 +26,8 @@ void markErrorHandledCallback(const BSG_KSCrashReportWriter * _Nonnull writer); - (void)didEnterBackgroundNotification; +- (void)triggerExcBreakpoint; + @property (nonatomic, strong, nullable) NSString *eventMode; + @end diff --git a/features/fixtures/shared/scenarios/Scenario.m b/features/fixtures/shared/scenarios/Scenario.m index f03c2d052..681d41604 100644 --- a/features/fixtures/shared/scenarios/Scenario.m +++ b/features/fixtures/shared/scenarios/Scenario.m @@ -55,4 +55,8 @@ - (void)startBugsnag { - (void)didEnterBackgroundNotification { } +- (void)triggerExcBreakpoint { + __builtin_trap(); +} + @end From d349439366f44b6d28d925ec8e096901e9192d37 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 14 Dec 2020 08:34:47 +0000 Subject: [PATCH 03/33] Add #938 to changelog [skip ci] --- Bugsnag/include/Bugsnag/BugsnagConfiguration.h | 4 ++-- CHANGELOG.md | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index 0dbb3a276..df020c97c 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -150,8 +150,8 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); @property(readwrite, retain, nullable) NSSet *redactedKeys; /** - * A set of strings and NSRegularExpression objects that determine which errors will be - * immediately discarded, based on the value of `BugsnagError.errorClass`. + * A set of strings and NSRegularExpression objects that determines which errors should + * be discarded based on their `errorClass`. * * OnError / OnSendError blocks will not be called for discarded errors. * diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ad8e6e5..edcf3acfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## TBD + +### Enhancements + +* Errors may now be discarded based on their `errorClass` using the new `discardClasses` configuration option. + [#938](https://github.com/bugsnag/bugsnag-cocoa/pull/938) + ## 6.4.0 (2020-12-08) ### Enhancements From 1360415cf2ec9d76d045e1ed5fb09f3df38358b7 Mon Sep 17 00:00:00 2001 From: Karl Stenerud Date: Fri, 11 Dec 2020 15:58:16 +0100 Subject: [PATCH 04/33] Added maxPersistedEvents config option so that users can control how many unsent events will be kept before deleting the oldest. --- Bugsnag/BugsnagCrashSentry.m | 4 +-- Bugsnag/Client/BugsnagClient.m | 1 - .../Configuration/BSGConfigurationBuilder.m | 17 +++++----- Bugsnag/Configuration/BugsnagConfiguration.m | 22 +++++++++++++ Bugsnag/Helpers/BugsnagKeys.h | 3 +- Bugsnag/Helpers/BugsnagKeys.m | 3 +- .../include/Bugsnag/BugsnagConfiguration.h | 8 +++++ Tests/BSGConfigurationBuilderTests.m | 4 +++ Tests/BugsnagClientTests.m | 4 +++ Tests/BugsnagConfigurationTests.m | 32 +++++++++++++++++++ Tests/ConfigurationApiValidationTest.m | 19 +++++++++++ 11 files changed, 103 insertions(+), 14 deletions(-) diff --git a/Bugsnag/BugsnagCrashSentry.m b/Bugsnag/BugsnagCrashSentry.m index 7e779faf3..7233bdb58 100644 --- a/Bugsnag/BugsnagCrashSentry.m +++ b/Bugsnag/BugsnagCrashSentry.m @@ -16,8 +16,6 @@ #import "Bugsnag.h" #import "BugsnagErrorTypes.h" -NSUInteger const BSG_MAX_STORED_REPORTS = 12; - @implementation BugsnagCrashSentry - (void)install:(BugsnagConfiguration *)config @@ -29,7 +27,7 @@ - (void)install:(BugsnagConfiguration *)config ksCrash.sink = sink; ksCrash.introspectMemory = YES; ksCrash.onCrash = onCrash; - ksCrash.maxStoredReports = BSG_MAX_STORED_REPORTS; + ksCrash.maxStoredReports = (int)config.maxPersistedEvents; // overridden elsewhere for handled errors, so we can assume that this only // applies to unhandled errors diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 71ff25e80..07bb96c19 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -434,7 +434,6 @@ - (void)initializeNotificationNameMap { - (void)start { [self.configuration validate]; - [self.crashSentry install:self.configuration apiClient:self.errorReportApiClient onCrash:&BSSerializeDataCrashHandler]; diff --git a/Bugsnag/Configuration/BSGConfigurationBuilder.m b/Bugsnag/Configuration/BSGConfigurationBuilder.m index d704d34e9..b5c1a2070 100644 --- a/Bugsnag/Configuration/BSGConfigurationBuilder.m +++ b/Bugsnag/Configuration/BSGConfigurationBuilder.m @@ -30,6 +30,7 @@ + (BugsnagConfiguration *)configurationFromOptions:(NSDictionary *)options { BSGKeyEnabledReleaseStages, BSGKeyEndpoints, BSGKeyMaxBreadcrumbs, + BSGKeyMaxPersistedEvents, BSGKeyPersistUser, BSGKeyRedactedKeys, BSGKeyReleaseStage, @@ -54,7 +55,8 @@ + (BugsnagConfiguration *)configurationFromOptions:(NSDictionary *)options { [self loadStringArray:config options:options key:BSGKeyRedactedKeys]; [self loadEndpoints:config options:options]; - [self loadMaxBreadcrumbs:config options:options]; + [self loadNumber:config options:options key:BSGKeyMaxBreadcrumbs]; + [self loadNumber:config options:options key:BSGKeyMaxPersistedEvents]; [self loadSendThreads:config options:options]; return config; } @@ -71,6 +73,12 @@ + (void)loadString:(BugsnagConfiguration *)config options:(NSDictionary *)option } } ++ (void)loadNumber:(BugsnagConfiguration *)config options:(NSDictionary *)options key:(NSString *)key { + if (options[key] && [options[key] isKindOfClass:[NSNumber class]]) { + [config setValue:options[key] forKey:key]; + } +} + + (void)loadStringArray:(BugsnagConfiguration *)config options:(NSDictionary *)options key:(NSString *)key { if (options[key] && [options[key] isKindOfClass:[NSArray class]]) { NSArray *val = options[key]; @@ -97,13 +105,6 @@ + (void)loadEndpoints:(BugsnagConfiguration *)config options:(NSDictionary *)opt } } -+ (void)loadMaxBreadcrumbs:(BugsnagConfiguration *)config options:(NSDictionary *)options { - if (options[BSGKeyMaxBreadcrumbs] && [options[BSGKeyMaxBreadcrumbs] isKindOfClass:[NSNumber class]]) { - NSNumber *num = options[BSGKeyMaxBreadcrumbs]; - config.maxBreadcrumbs = [num unsignedIntValue]; - } -} - + (void)loadSendThreads:(BugsnagConfiguration *)config options:(NSDictionary *)options { if (options[BSGKeySendThreads] && [options[BSGKeySendThreads] isKindOfClass:[NSString class]]) { NSString *sendThreads = [options[BSGKeySendThreads] lowercaseString]; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 1d9c04368..acff49840 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -83,6 +83,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { [copy setEnabledErrorTypes:self.enabledErrorTypes]; [copy setEnabledReleaseStages:self.enabledReleaseStages]; [copy setRedactedKeys:self.redactedKeys]; + [copy setMaxPersistedEvents:self.maxPersistedEvents]; [copy setMaxBreadcrumbs:self.maxBreadcrumbs]; copy->_metadata = [[BugsnagMetadata alloc] initWithDictionary:[[self.metadata toDictionary] mutableCopy]]; [copy setEndpoints:self.endpoints]; @@ -159,6 +160,7 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { _enabledReleaseStages = nil; _redactedKeys = [NSSet setWithArray:@[@"password"]]; _enabledBreadcrumbTypes = BSGEnabledBreadcrumbTypeAll; + _maxPersistedEvents = 12; _maxBreadcrumbs = 25; _autoTrackSessions = YES; _sendThreads = BSGThreadSendPolicyAlways; @@ -415,6 +417,26 @@ -(void)deletePersistedUserData { // MARK: - Properties: Getters and Setters // ----------------------------------------------------------------------------- +@synthesize maxPersistedEvents = _maxPersistedEvents; + +- (NSUInteger)maxPersistedEvents { + @synchronized (self) { + return _maxPersistedEvents; + } +} + +- (void)setMaxPersistedEvents:(NSUInteger)maxPersistedEvents { + @synchronized (self) { + if (maxPersistedEvents >= 1 && maxPersistedEvents <= 100) { + _maxPersistedEvents = maxPersistedEvents; + } else { + bsg_log_err(@"Invalid configuration value detected. Option maxPersistedEvents " + "should be an integer between 1-100. Supplied value is %lu", + (unsigned long) maxPersistedEvents); + } + } +} + @synthesize maxBreadcrumbs = _maxBreadcrumbs; - (NSUInteger)maxBreadcrumbs { diff --git a/Bugsnag/Helpers/BugsnagKeys.h b/Bugsnag/Helpers/BugsnagKeys.h index aeba03ee3..90fc4c835 100644 --- a/Bugsnag/Helpers/BugsnagKeys.h +++ b/Bugsnag/Helpers/BugsnagKeys.h @@ -42,8 +42,8 @@ extern NSString *const BSGKeyExceptionName; extern NSString *const BSGKeyExceptions; extern NSString *const BSGKeyExecutableName; extern NSString *const BSGKeyExtraRuntimeInfo; -extern NSString *const BSGKeyFrameAddrFormat; extern NSString *const BSGKeyFrameAddress; +extern NSString *const BSGKeyFrameAddrFormat; extern NSString *const BSGKeyGroupingHash; extern NSString *const BSGKeyHwMachine; extern NSString *const BSGKeyHwModel; @@ -63,6 +63,7 @@ extern NSString *const BSGKeyMachoLoadAddr; extern NSString *const BSGKeyMachoUUID; extern NSString *const BSGKeyMachoVMAddress; extern NSString *const BSGKeyMaxBreadcrumbs; +extern NSString *const BSGKeyMaxPersistedEvents; extern NSString *const BSGKeyMessage; extern NSString *const BSGKeyMetadata; extern NSString *const BSGKeyMethod; diff --git a/Bugsnag/Helpers/BugsnagKeys.m b/Bugsnag/Helpers/BugsnagKeys.m index 300a94fb3..060fd3f35 100644 --- a/Bugsnag/Helpers/BugsnagKeys.m +++ b/Bugsnag/Helpers/BugsnagKeys.m @@ -38,8 +38,8 @@ NSString *const BSGKeyExceptions = @"exceptions"; NSString *const BSGKeyExecutableName = @"CFBundleExecutable"; NSString *const BSGKeyExtraRuntimeInfo = @"extraRuntimeInfo"; -NSString *const BSGKeyFrameAddrFormat = @"0x%lx"; NSString *const BSGKeyFrameAddress = @"frameAddress"; +NSString *const BSGKeyFrameAddrFormat = @"0x%lx"; NSString *const BSGKeyGroupingHash = @"groupingHash"; NSString *const BSGKeyHwMachine = @"hw.machine"; NSString *const BSGKeyHwModel = @"hw.model"; @@ -59,6 +59,7 @@ NSString *const BSGKeyMachoUUID = @"machoUUID"; NSString *const BSGKeyMachoVMAddress = @"machoVMAddress"; NSString *const BSGKeyMaxBreadcrumbs = @"maxBreadcrumbs"; +NSString *const BSGKeyMaxPersistedEvents = @"maxPersistedEvents"; NSString *const BSGKeyMessage = @"message"; NSString *const BSGKeyMetadata = @"metaData"; NSString *const BSGKeyMethod = @"method"; diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index 844f4406e..e3989cc24 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -204,6 +204,14 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); @property(retain, nullable) NSString *appType; +/** + * Sets the maximum number of events which will be stored. Once the threshold is reached, + * the oldest events will be deleted. + * + * By default, 12 events are stored: this can be amended up to a maximum of 100. + */ +@property (nonatomic) NSUInteger maxPersistedEvents; + /** * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, * the oldest breadcrumbs will be deleted. diff --git a/Tests/BSGConfigurationBuilderTests.m b/Tests/BSGConfigurationBuilderTests.m index 2ab535f7a..3ad82f82d 100644 --- a/Tests/BSGConfigurationBuilderTests.m +++ b/Tests/BSGConfigurationBuilderTests.m @@ -55,6 +55,7 @@ - (void)testDecodeDefaultValues { XCTAssertNil(config.appVersion); XCTAssertTrue(config.autoDetectErrors); XCTAssertTrue(config.autoTrackSessions); + XCTAssertEqual(12, config.maxPersistedEvents); XCTAssertEqual(25, config.maxBreadcrumbs); XCTAssertTrue(config.persistUser); XCTAssertEqualObjects(@[@"password"], [config.redactedKeys allObjects]); @@ -92,6 +93,7 @@ - (void)testDecodeFullConfig { @"sessions": @"https://sessions.example.co" }, @"enabledReleaseStages": @[@"beta2", @"prod"], + @"maxPersistedEvents": @29, @"maxBreadcrumbs": @27, @"persistUser": @NO, @"redactedKeys": @[@"foo"], @@ -105,6 +107,7 @@ - (void)testDecodeFullConfig { XCTAssertFalse(config.autoDetectErrors); XCTAssertFalse(config.autoTrackSessions); XCTAssertEqualObjects(@"7.22", config.bundleVersion); + XCTAssertEqual(29, config.maxPersistedEvents); XCTAssertEqual(27, config.maxBreadcrumbs); XCTAssertFalse(config.persistUser); XCTAssertEqualObjects(@[@"foo"], config.redactedKeys); @@ -138,6 +141,7 @@ - (void)testInvalidConfigOptions { @"endpoints": [NSNull null], @"enabledReleaseStages": @[@"beta2", @"prod"], @"enabledErrorTypes": @[@"ooms", @"signals"], + @"maxPersistedEvents": @29, @"maxBreadcrumbs": @27, @"persistUser": @"pomelo", @"redactedKeys": @[@77], diff --git a/Tests/BugsnagClientTests.m b/Tests/BugsnagClientTests.m index 7a958109d..b61e1639a 100644 --- a/Tests/BugsnagClientTests.m +++ b/Tests/BugsnagClientTests.m @@ -187,6 +187,7 @@ - (void)assertEqualConfiguration:(BugsnagConfiguration *)expected withActual:(Bu XCTAssertEqual(expected.enabledReleaseStages, actual.enabledReleaseStages); XCTAssertEqualObjects(expected.endpoints.notify, actual.endpoints.notify); XCTAssertEqualObjects(expected.endpoints.sessions, actual.endpoints.sessions); + XCTAssertEqual(expected.maxPersistedEvents, actual.maxPersistedEvents); XCTAssertEqual(expected.maxBreadcrumbs, actual.maxBreadcrumbs); XCTAssertEqual(expected.persistUser, actual.persistUser); XCTAssertEqual([expected.redactedKeys count], [actual.redactedKeys count]); @@ -209,11 +210,13 @@ - (void)testChangesToConfigurationAreIgnoredAfterCallingStart { // Modify some arbitrary properties config.persistUser = !config.persistUser; + config.maxPersistedEvents = config.maxPersistedEvents * 2; config.maxBreadcrumbs = config.maxBreadcrumbs * 2; config.appVersion = @"99.99.99"; // Ensure the changes haven't been reflected in our copy XCTAssertNotEqual(initialConfig.persistUser, config.persistUser); + XCTAssertNotEqual(initialConfig.maxPersistedEvents, config.maxPersistedEvents); XCTAssertNotEqual(initialConfig.maxBreadcrumbs, config.maxBreadcrumbs); XCTAssertNotEqualObjects(initialConfig.appVersion, config.appVersion); @@ -231,6 +234,7 @@ - (void)testStartingBugsnagTwiceLogsAWarningAndIgnoresNewConfiguration { BugsnagConfiguration *updatedConfig = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_2]; updatedConfig.persistUser = !initialConfig.persistUser; updatedConfig.maxBreadcrumbs = initialConfig.maxBreadcrumbs * 2; + updatedConfig.maxPersistedEvents = initialConfig.maxPersistedEvents * 2; updatedConfig.appVersion = @"99.99.99"; [Bugsnag startWithConfiguration:updatedConfig]; diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index e7a11bd9f..248745ef3 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -529,6 +529,38 @@ - (void)testSettingPersistUser { XCTAssertTrue(config.persistUser); } +// ============================================================================= +// MARK: - Max Persisted Events +// ============================================================================= + +- (void)testMaxPersistedEvents { + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + XCTAssertEqual(12, config.maxPersistedEvents); + + // alter to valid value + config.maxPersistedEvents = 10; + XCTAssertEqual(10, config.maxPersistedEvents); + + // alter to max value + config.maxPersistedEvents = 100; + XCTAssertEqual(100, config.maxPersistedEvents); + + // alter to min value + config.maxPersistedEvents = 1; + XCTAssertEqual(1, config.maxPersistedEvents); + + config.maxPersistedEvents = 0; + XCTAssertEqual(1, config.maxPersistedEvents); + + // alter to negative value + config.maxPersistedEvents = -1; + XCTAssertEqual(1, config.maxPersistedEvents); + + // alter to > max value + config.maxPersistedEvents = 500; + XCTAssertEqual(1, config.maxPersistedEvents); +} + // ============================================================================= // MARK: - Max Breadcrumb // ============================================================================= diff --git a/Tests/ConfigurationApiValidationTest.m b/Tests/ConfigurationApiValidationTest.m index a1a099264..4afa12d5f 100644 --- a/Tests/ConfigurationApiValidationTest.m +++ b/Tests/ConfigurationApiValidationTest.m @@ -128,6 +128,25 @@ - (void)testValidAppType { XCTAssertEqualObjects(@"cocoa", self.config.appType); } +- (void)testValidMaxPersistedEvents { + self.config.maxPersistedEvents = 1; + XCTAssertEqual(1, self.config.maxPersistedEvents); + self.config.maxPersistedEvents = 100; + XCTAssertEqual(100, self.config.maxPersistedEvents); + self.config.maxPersistedEvents = 40; + XCTAssertEqual(40, self.config.maxPersistedEvents); +} + +- (void)testInvalidMaxPersistedEvents { + self.config.maxPersistedEvents = 1; + self.config.maxPersistedEvents = 0; + XCTAssertEqual(1, self.config.maxPersistedEvents); + self.config.maxPersistedEvents = -1; + XCTAssertEqual(1, self.config.maxPersistedEvents); + self.config.maxPersistedEvents = 590; + XCTAssertEqual(1, self.config.maxPersistedEvents); +} + - (void)testValidMaxBreadcrumbs { self.config.maxBreadcrumbs = 0; XCTAssertEqual(0, self.config.maxBreadcrumbs); From 20cf7ec21c71c4a2345278badc409e16b64351cb Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 14 Dec 2020 16:51:47 +0000 Subject: [PATCH 05/33] Improve header documentation for discardClasses [skip ci] --- Bugsnag/include/Bugsnag/BugsnagConfiguration.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index df020c97c..aef015bd2 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -150,9 +150,11 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); @property(readwrite, retain, nullable) NSSet *redactedKeys; /** - * A set of strings and NSRegularExpression objects that determines which errors should + * A set of strings and / or NSRegularExpression objects that determine which errors should * be discarded based on their `errorClass`. * + * Comparisons are case sensitive. + * * OnError / OnSendError blocks will not be called for discarded errors. * * Some examples of errorClass are: Objective-C exception names like "NSRangeException", From 0ae820c4b29cfbaa952a244a65e85182a629c65b Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 14 Dec 2020 16:11:34 +0000 Subject: [PATCH 06/33] Move notification breadcrumbs to BSGNotificationBreadcrumbs --- Bugsnag.xcodeproj/project.pbxproj | 24 +- .../Breadcrumbs/BSGNotificationBreadcrumbs.h | 53 +++ .../Breadcrumbs/BSGNotificationBreadcrumbs.m | 304 +++++++++++++ Bugsnag/Client/BugsnagClient+Private.h | 8 - Bugsnag/Client/BugsnagClient.m | 407 +----------------- Tests/BugsnagClientMirrorTest.m | 4 +- Tests/NotificationBreadcrumbTests.m | 64 +-- Tests/UIDeviceStub.h | 25 -- Tests/UIDeviceStub.m | 17 - 9 files changed, 417 insertions(+), 489 deletions(-) create mode 100644 Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h create mode 100644 Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m delete mode 100644 Tests/UIDeviceStub.h delete mode 100644 Tests/UIDeviceStub.m diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index 5011da764..ca53af7ff 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -646,10 +646,16 @@ 0126DF1D257A92860031A70C /* BugsnagSession+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0126DF1A257A92860031A70C /* BugsnagSession+Private.h */; }; 0140D29A25767C9A00FD0306 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 01447605256684500018AB94 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; + 01468F5225876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; + 01468F5325876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; + 01468F5425876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; + 01468F5525876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; + 01468F5625876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; + 01468F5725876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; + 01468F5825876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */ = {isa = PBXBuildFile; fileRef = 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */; }; 0163BF5925823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; 0163BF5A25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; 0163BF5B25823D8D008DC28B /* NotificationBreadcrumbTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */; }; - 0163BF6525827D9F008DC28B /* UIDeviceStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 0163BF6425827D9F008DC28B /* UIDeviceStub.m */; }; 0187D464255BD7B800C503D9 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 01B14C56251CE55F00118748 /* report-react-native-promise-rejection.json in Resources */ = {isa = PBXBuildFile; fileRef = 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */; }; 01B14C57251CE55F00118748 /* report-react-native-promise-rejection.json in Resources */ = {isa = PBXBuildFile; fileRef = 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */; }; @@ -1267,9 +1273,9 @@ 0134524A256BCF7C0088C548 /* BugsnagError+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagError+Private.h"; sourceTree = ""; }; 0134524B256BD00A0088C548 /* BugsnagThread+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagThread+Private.h"; sourceTree = ""; }; 0140D24725765F8F00FD0306 /* BSGUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGUIKit.h; sourceTree = ""; }; + 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGNotificationBreadcrumbs.h; sourceTree = ""; }; + 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGNotificationBreadcrumbs.m; sourceTree = ""; }; 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationBreadcrumbTests.m; sourceTree = ""; }; - 0163BF6325827D9F008DC28B /* UIDeviceStub.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UIDeviceStub.h; sourceTree = ""; }; - 0163BF6425827D9F008DC28B /* UIDeviceStub.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIDeviceStub.m; sourceTree = ""; }; 01937CF9257A7B4C00F2DE31 /* Bugsnag+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bugsnag+Private.h"; sourceTree = ""; }; 01937D01257A7E0E00F2DE31 /* BugsnagErrorReportSink+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagErrorReportSink+Private.h"; sourceTree = ""; }; 01937D09257A7ED000F2DE31 /* BugsnagSessionTracker+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagSessionTracker+Private.h"; sourceTree = ""; }; @@ -1701,8 +1707,6 @@ CBA2249A251E429C00B87416 /* TestSupport.m */, 01E8765C256684E700F4B70A /* URLSessionMock.h */, 01E8765D256684E700F4B70A /* URLSessionMock.m */, - 0163BF6325827D9F008DC28B /* UIDeviceStub.h */, - 0163BF6425827D9F008DC28B /* UIDeviceStub.m */, 012482A225627B51003F7243 /* UIKitTests.m */, ); path = Tests; @@ -1711,6 +1715,8 @@ 00AD1CF124869EBD00A27979 /* Breadcrumbs */ = { isa = PBXGroup; children = ( + 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */, + 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */, 008967B32486D9D700DC48C2 /* BugsnagBreadcrumbs.h */, 008967B22486D9D700DC48C2 /* BugsnagBreadcrumbs.m */, ); @@ -2005,6 +2011,7 @@ 008969962486DAD100DC48C2 /* BSG_KSBacktrace_Private.h in Headers */, 00896A112486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969ED2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, + 01468F5225876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, 00AD1F022486A17900A27979 /* RegisterErrorData.h in Headers */, 008968ED2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A292486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, @@ -2102,6 +2109,7 @@ 00896A122486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969EE2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, 00AD1F032486A17900A27979 /* RegisterErrorData.h in Headers */, + 01468F5325876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, 008968EE2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A2A2486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, 008969DC2486DAD100DC48C2 /* BSG_KSCrash.h in Headers */, @@ -2199,6 +2207,7 @@ 00896A132486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969EF2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, 00AD1F042486A17900A27979 /* RegisterErrorData.h in Headers */, + 01468F5425876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, 008968EF2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A2B2486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, 008969DD2486DAD100DC48C2 /* BSG_KSCrash.h in Headers */, @@ -2499,6 +2508,7 @@ 008969572486DAD000DC48C2 /* BSG_KSCrashDoctor.m in Sources */, 008968B92486DA9600DC48C2 /* BugsnagStacktrace.m in Sources */, 00896A142486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, + 01468F5525876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 008967BE2486DA1900DC48C2 /* BugsnagClient.m in Sources */, 008968952486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008969C32486DAD100DC48C2 /* BSG_KSObjC.c in Sources */, @@ -2549,7 +2559,6 @@ 008967902486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, 008967722486D43700DC48C2 /* KSSysCtl_Tests.m in Sources */, 0089676C2486D43700DC48C2 /* BugsnagTestsDummyClass.m in Sources */, - 0163BF6525827D9F008DC28B /* UIDeviceStub.m in Sources */, 008966EB2486D43700DC48C2 /* BugsnagDeviceTest.m in Sources */, CBCF77AB250142E0004AF22A /* BSGJSONSerializerTest.m in Sources */, 008967A22486D43700DC48C2 /* KSCrashSentry_Signal_Tests.m in Sources */, @@ -2663,6 +2672,7 @@ 008969582486DAD000DC48C2 /* BSG_KSCrashDoctor.m in Sources */, 008968BA2486DA9600DC48C2 /* BugsnagStacktrace.m in Sources */, 00896A152486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, + 01468F5625876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 008967BF2486DA1900DC48C2 /* BugsnagClient.m in Sources */, 008968962486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008969C42486DAD100DC48C2 /* BSG_KSObjC.c in Sources */, @@ -2824,6 +2834,7 @@ 008969592486DAD000DC48C2 /* BSG_KSCrashDoctor.m in Sources */, 008968BB2486DA9600DC48C2 /* BugsnagStacktrace.m in Sources */, 00896A162486DAD100DC48C2 /* BSG_KSCrashSentry_Signal.c in Sources */, + 01468F5725876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 008967C02486DA1900DC48C2 /* BugsnagClient.m in Sources */, 008968972486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, 008969C52486DAD100DC48C2 /* BSG_KSObjC.c in Sources */, @@ -2987,6 +2998,7 @@ 0089687F2486DA9600DC48C2 /* BugsnagBreadcrumb.m in Sources */, 008968012486DA4500DC48C2 /* BugsnagSessionTrackingApiClient.m in Sources */, 008968322486DA5600DC48C2 /* BugsnagCollections.m in Sources */, + 01468F5825876DC1002B0519 /* BSGNotificationBreadcrumbs.m in Sources */, 008968832486DA9600DC48C2 /* BugsnagAppWithState.m in Sources */, 008968AA2486DA9600DC48C2 /* BugsnagSession.m in Sources */, 008968982486DA9600DC48C2 /* BugsnagHandledState.m in Sources */, diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h new file mode 100644 index 000000000..c35fab68f --- /dev/null +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h @@ -0,0 +1,53 @@ +// +// BSGBreadcrumbsProducer.h +// Bugsnag +// +// Created by Nick Dowell on 10/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import + +@class BugsnagConfiguration; + +NS_ASSUME_NONNULL_BEGIN + +@protocol BSGBreadcrumbSink + +- (void)leaveBreadcrumbWithMessage:(NSString *)message metadata:(nullable NSDictionary *)metadata andType:(BSGBreadcrumbType)type; + +@end + + +#pragma mark - + +@interface BSGNotificationBreadcrumbs : NSObject + +#pragma mark Initializers + +- (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration + breadcrumbSink:(id)breadcrumbSink NS_DESIGNATED_INITIALIZER; + +- (instancetype)init UNAVAILABLE_ATTRIBUTE; + +#pragma mark Properties + +@property BugsnagConfiguration *configuration; + +@property (weak) id breadcrumbSink; + +@property NSNotificationCenter *notificationCenter; + +@property NSNotificationCenter *workspaceNotificationCenter; + +#pragma mark Methods + +/// Starts observing the default notifications. +- (void)start; + +/// Starts observing notifications with the given name and adds a "state" breadcrumbs when received. +- (void)startListeningForStateChangeNotification:(NSNotificationName)notificationName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m new file mode 100644 index 000000000..f08568be1 --- /dev/null +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m @@ -0,0 +1,304 @@ +// +// BSGNotificationBreadcrumbs.m +// Bugsnag +// +// Created by Nick Dowell on 10/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import "BSGNotificationBreadcrumbs.h" + +#import "BugsnagBreadcrumbs.h" +#import "BugsnagConfiguration+Private.h" +#import "BugsnagKeys.h" + +#if TARGET_OS_IOS || TARGET_OS_TV +#import "BSGUIKit.h" +#else +#import +#endif + + +@interface BSGNotificationBreadcrumbs () + +@property NSDictionary *notificationNameMap; + +@end + + +@implementation BSGNotificationBreadcrumbs + +- (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration + breadcrumbSink:(id)breadcrumbSink { + if ((self = [super init])) { + _configuration = configuration; + _notificationCenter = NSNotificationCenter.defaultCenter; +#if TARGET_OS_OSX + _workspaceNotificationCenter = NSWorkspace.sharedWorkspace.notificationCenter; +#endif + _breadcrumbSink = breadcrumbSink; + _notificationNameMap = @{ +#if TARGET_OS_TV + NSUndoManagerDidRedoChangeNotification : @"Redo Operation", + NSUndoManagerDidUndoChangeNotification : @"Undo Operation", + UIScreenBrightnessDidChangeNotification : @"Screen Brightness Changed", + UITableViewSelectionDidChangeNotification : @"TableView Select Change", + UIWindowDidBecomeHiddenNotification : @"Window Became Hidden", + UIWindowDidBecomeKeyNotification : @"Window Became Key", + UIWindowDidBecomeVisibleNotification : @"Window Became Visible", + UIWindowDidResignKeyNotification : @"Window Resigned Key", +#elif TARGET_OS_IOS + NSUndoManagerDidRedoChangeNotification : @"Redo Operation", + NSUndoManagerDidUndoChangeNotification : @"Undo Operation", + UIApplicationDidEnterBackgroundNotification : @"App Did Enter Background", + UIApplicationDidReceiveMemoryWarningNotification : @"Memory Warning", + UIApplicationUserDidTakeScreenshotNotification : @"Took Screenshot", + UIApplicationWillEnterForegroundNotification : @"App Will Enter Foreground", + UIApplicationWillTerminateNotification : @"App Will Terminate", + UIDeviceBatteryLevelDidChangeNotification : @"Battery Level Changed", + UIDeviceBatteryStateDidChangeNotification : @"Battery State Changed", + UIDeviceOrientationDidChangeNotification : @"Orientation Changed", + UIKeyboardDidHideNotification : @"Keyboard Became Hidden", + UIKeyboardDidShowNotification : @"Keyboard Became Visible", + UIMenuControllerDidHideMenuNotification : @"Did Hide Menu", + UIMenuControllerDidShowMenuNotification : @"Did Show Menu", + UITableViewSelectionDidChangeNotification : @"TableView Select Change", + UITextFieldTextDidBeginEditingNotification : @"Began Editing Text", + UITextFieldTextDidEndEditingNotification : @"Stopped Editing Text", + UITextViewTextDidBeginEditingNotification : @"Began Editing Text", + UITextViewTextDidEndEditingNotification : @"Stopped Editing Text", + UIWindowDidBecomeHiddenNotification : @"Window Became Hidden", + UIWindowDidBecomeVisibleNotification : @"Window Became Visible", +#elif TARGET_OS_OSX + NSUndoManagerDidRedoChangeNotification : @"Redo Operation", + NSUndoManagerDidUndoChangeNotification : @"Undo Operation", + NSApplicationDidBecomeActiveNotification : @"App Became Active", + NSApplicationDidHideNotification : @"App Did Hide", + NSApplicationDidResignActiveNotification : @"App Resigned Active", + NSApplicationDidUnhideNotification : @"App Did Unhide", + NSApplicationWillTerminateNotification : @"App Will Terminate", + NSControlTextDidBeginEditingNotification : @"Control Text Began Edit", + NSControlTextDidEndEditingNotification : @"Control Text Ended Edit", + NSMenuWillSendActionNotification : @"Menu Will Send Action", + NSTableViewSelectionDidChangeNotification : @"TableView Select Change", + NSWindowDidBecomeKeyNotification : @"Window Became Key", + NSWindowDidEnterFullScreenNotification : @"Window Entered Full Screen", + NSWindowDidExitFullScreenNotification : @"Window Exited Full Screen", + NSWindowWillCloseNotification : @"Window Will Close", + NSWindowWillMiniaturizeNotification : @"Window Will Miniaturize", + NSWorkspaceScreensDidSleepNotification : @"Workspace Screen Slept", + NSWorkspaceScreensDidWakeNotification : @"Workspace Screen Awoke", +#endif + }; + } + return self; +} + +#if TARGET_OS_OSX +- (NSArray *)workspaceBreadcrumbStateEvents { + return @[ + NSWorkspaceScreensDidSleepNotification, + NSWorkspaceScreensDidWakeNotification + ]; +} +#endif + +- (NSArray *)automaticBreadcrumbStateEvents { +#if TARGET_OS_TV + return @[ + NSUndoManagerDidRedoChangeNotification, + NSUndoManagerDidUndoChangeNotification, + UIScreenBrightnessDidChangeNotification, + UIWindowDidBecomeHiddenNotification, + UIWindowDidBecomeKeyNotification, + UIWindowDidBecomeVisibleNotification, + UIWindowDidResignKeyNotification, + ]; +#elif TARGET_OS_IOS + return @[ + NSUndoManagerDidRedoChangeNotification, + NSUndoManagerDidUndoChangeNotification, + UIApplicationDidEnterBackgroundNotification, + UIApplicationDidReceiveMemoryWarningNotification, + UIApplicationUserDidTakeScreenshotNotification, + UIApplicationWillEnterForegroundNotification, + UIApplicationWillTerminateNotification, + UIKeyboardDidHideNotification, + UIKeyboardDidShowNotification, + UIMenuControllerDidHideMenuNotification, + UIMenuControllerDidShowMenuNotification, + UIWindowDidBecomeHiddenNotification, + UIWindowDidBecomeVisibleNotification, + ]; +#elif TARGET_OS_OSX + return @[ + NSApplicationDidBecomeActiveNotification, + NSApplicationDidResignActiveNotification, + NSApplicationDidHideNotification, + NSApplicationDidUnhideNotification, + NSApplicationWillTerminateNotification, + + NSWindowDidBecomeKeyNotification, + NSWindowDidEnterFullScreenNotification, + NSWindowDidExitFullScreenNotification, + NSWindowWillCloseNotification, + NSWindowWillMiniaturizeNotification, + ]; +#endif + return nil; +} + +- (NSArray *)automaticBreadcrumbControlEvents { +#if TARGET_OS_IOS + return @[ + UITextFieldTextDidBeginEditingNotification, + UITextFieldTextDidEndEditingNotification, + UITextViewTextDidBeginEditingNotification, + UITextViewTextDidEndEditingNotification + ]; +#elif TARGET_OS_OSX + return @[ + NSControlTextDidBeginEditingNotification, + NSControlTextDidEndEditingNotification + ]; +#endif + return nil; +} + +- (NSArray *)automaticBreadcrumbTableItemEvents { +#if TARGET_OS_IOS || TARGET_OS_TV + return @[ UITableViewSelectionDidChangeNotification ]; +#elif TARGET_OS_OSX + return @[ NSTableViewSelectionDidChangeNotification ]; +#endif + return nil; +} + +- (NSArray *)automaticBreadcrumbMenuItemEvents { +#if TARGET_OS_OSX + return @[ NSMenuWillSendActionNotification ]; +#endif + return nil; +} + +- (void)dealloc { + [_notificationCenter removeObserver:self]; +} + +#pragma mark - + +- (NSString *)messageForNotificationName:(NSNotificationName)name { + return self.notificationNameMap[name] ?: [name stringByReplacingOccurrencesOfString:@"Notification" withString:@""]; +} + +- (void)addBreadcrumbWithType:(BSGBreadcrumbType)type forNotificationName:(NSNotificationName)notificationName { + [self addBreadcrumbWithType:type forNotificationName:notificationName metadata:nil]; +} + +- (void)addBreadcrumbWithType:(BSGBreadcrumbType)type forNotificationName:(NSNotificationName)notificationName metadata:(NSDictionary *)metadata { + [self.breadcrumbSink leaveBreadcrumbWithMessage:[self messageForNotificationName:notificationName] metadata:metadata ?: @{} andType:type]; +} + +#pragma mark - + +- (void)start { + // State events + if ([_configuration shouldRecordBreadcrumbType:BSGBreadcrumbTypeState]) { + // Generic state events + for (NSNotificationName name in [self automaticBreadcrumbStateEvents]) { + [self startListeningForStateChangeNotification:name]; + } + +#if TARGET_OS_OSX + // Workspace-specific events - MacOS only + for (NSNotificationName name in [self workspaceBreadcrumbStateEvents]) { + NSLog(@"%@", name); + [_workspaceNotificationCenter addObserver:self + selector:@selector(addBreadcrumbForNotification:) + name:name + object:nil]; + } +#endif + + // NSMenu events (Mac only) + for (NSNotificationName name in [self automaticBreadcrumbMenuItemEvents]) { + NSLog(@"%@", name); + [_notificationCenter addObserver:self + selector:@selector(addBreadcrumbForMenuItemNotification:) + name:name + object:nil]; + } + } + + // Navigation events + if ([_configuration shouldRecordBreadcrumbType:BSGBreadcrumbTypeNavigation]) { + // UI/NSTableView events + for (NSNotificationName name in [self automaticBreadcrumbTableItemEvents]) { + NSLog(@"%@", name); + [_notificationCenter addObserver:self + selector:@selector(addBreadcrumbForTableViewNotification:) + name:name + object:nil]; + } + } + + // User events + if ([_configuration shouldRecordBreadcrumbType:BSGBreadcrumbTypeUser]) { + // UITextField/NSControl events (text editing) + for (NSNotificationName name in [self automaticBreadcrumbControlEvents]) { + NSLog(@"%@", name); + [_notificationCenter addObserver:self + selector:@selector(addBreadcrumbForControlNotification:) + name:name + object:nil]; + } + } +} + +- (void)startListeningForStateChangeNotification:(NSNotificationName)notificationName { + [_notificationCenter addObserver:self selector:@selector(addBreadcrumbForNotification:) name:notificationName object:nil]; +} + +- (void)addBreadcrumbForNotification:(NSNotification *)notification { + [self addBreadcrumbWithType:BSGBreadcrumbTypeState forNotificationName:notification.name]; +} + +- (void)addBreadcrumbForTableViewNotification:(NSNotification *)notification { +#if TARGET_OS_IOS || TARGET_OS_TV + NSIndexPath *indexPath = ((UITableView *)notification.object).indexPathForSelectedRow; + [self addBreadcrumbWithType:BSGBreadcrumbTypeNavigation forNotificationName:notification.name metadata: + indexPath ? @{@"row" : @(indexPath.row), @"section" : @(indexPath.section)} : nil]; +#elif TARGET_OS_OSX + NSTableView *tableView = notification.object; + [self addBreadcrumbWithType:BSGBreadcrumbTypeNavigation forNotificationName:notification.name metadata: + tableView ? @{@"selectedRow" : @(tableView.selectedRow), @"selectedColumn" : @(tableView.selectedColumn)} : nil]; +#endif +} + +- (void)addBreadcrumbForMenuItemNotification:(NSNotification *)notification { +#if TARGET_OS_OSX + NSMenuItem *menuItem = [[notification userInfo] valueForKey:@"MenuItem"]; + [self addBreadcrumbWithType:BSGBreadcrumbTypeState forNotificationName:notification.name metadata: + [menuItem isKindOfClass:[NSMenuItem class]] ? @{BSGKeyAction : menuItem.title} : nil]; +#endif +} + +- (void)addBreadcrumbForControlNotification:(NSNotification *)notification { +#if TARGET_OS_IOS + NSString *label = ((UIControl *)notification.object).accessibilityLabel; + [self addBreadcrumbWithType:BSGBreadcrumbTypeUser forNotificationName:notification.name metadata: + label.length ? @{BSGKeyLabel : label} : nil]; +#elif TARGET_OS_OSX + NSControl *control = notification.object; + NSDictionary *metadata = nil; + if ([control respondsToSelector:@selector(accessibilityLabel)]) { + NSString *label = control.accessibilityLabel; + if (label.length > 0) { + metadata = @{BSGKeyLabel : label}; + } + } + [self addBreadcrumbWithType:BSGBreadcrumbTypeUser forNotificationName:notification.name metadata:metadata]; +#endif +} + +@end diff --git a/Bugsnag/Client/BugsnagClient+Private.h b/Bugsnag/Client/BugsnagClient+Private.h index a937ca721..f7b835fb3 100644 --- a/Bugsnag/Client/BugsnagClient+Private.h +++ b/Bugsnag/Client/BugsnagClient+Private.h @@ -18,10 +18,6 @@ @class BugsnagSessionTracker; @class BugsnagSystemState; -#if TARGET_OS_IOS -@class UIDevice; -#endif - NS_ASSUME_NONNULL_BEGIN @interface BugsnagClient () @@ -47,11 +43,7 @@ NS_ASSUME_NONNULL_BEGIN @property NSMutableDictionary *extraRuntimeInfo; #if TARGET_OS_IOS - -@property (strong, nonatomic) UIDevice *uiDevice; // Used for unit testing - @property (strong, nonatomic) NSString *lastOrientation; - #endif @property (strong, nonatomic) BugsnagMetadata *metadata; // Used in BugsnagReactNative diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index a44e3a455..1146ae06f 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -31,6 +31,7 @@ #import "BSGCachesDirectory.h" #import "BSGConnectivity.h" #import "BSGJSONSerialization.h" +#import "BSGNotificationBreadcrumbs.h" #import "BSGSerialization.h" #import "BSG_KSCrash.h" #import "BSG_KSCrashC.h" @@ -101,8 +102,6 @@ void (*onCrash)(const BSG_KSCrashReportWriter *writer); } bsg_g_bugsnag_data; -static NSDictionary *notificationNameMap; - static char *sessionId[128]; static char *sessionStartDate[128]; static char *watchdogSentinelPath = NULL; @@ -154,24 +153,6 @@ void BSSerializeDataCrashHandler(const BSG_KSCrashReportWriter *writer, int type } } -/** - * Maps an NSNotificationName to its standard (Bugsnag) name - * - * @param name The NSNotificationName (type aliased to NSString) - * - * @returns The Bugsnag-standard name, or the notification name minus the "Notification" portion. - */ -NSString *BSGBreadcrumbNameForNotificationName(NSString *name) { - NSString *readableName = notificationNameMap[name]; - - if (readableName) { - return readableName; - } else { - return [name stringByReplacingOccurrencesOfString:@"Notification" - withString:@""]; - } -} - /** * Convert a device orientation into its Bugsnag string representation * @@ -242,6 +223,12 @@ void BSGWriteSessionCrashData(BugsnagSession *session) { // MARK: - BugsnagClient // ============================================================================= +@interface BugsnagClient () + +@property BSGNotificationBreadcrumbs *notificationBreadcrumbs; + +@end + @implementation BugsnagClient /** @@ -284,10 +271,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { self.errorReportApiClient = [[BugsnagErrorReportApiClient alloc] initWithSession:configuration.session queueName:@"Error API queue"]; bsg_g_bugsnag_data.onCrash = (void (*)(const BSG_KSCrashReportWriter *))self.configuration.onCrashHandler; - static dispatch_once_t once_t; - dispatch_once(&once_t, ^{ - [self initializeNotificationNameMap]; - }); + _notificationBreadcrumbs = [[BSGNotificationBreadcrumbs alloc] initWithConfiguration:configuration breadcrumbSink:self]; self.sessionTracker = [[BugsnagSessionTracker alloc] initWithConfig:self.configuration client:self @@ -324,8 +308,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { client:self]; #if BSG_PLATFORM_IOS - _uiDevice = [UIDEVICE currentDevice]; - _lastOrientation = BSGOrientationNameFromEnum(_uiDevice.orientation); + _lastOrientation = BSGOrientationNameFromEnum([UIDEVICE currentDevice].orientation); #endif if (self.user.id == nil) { // populate with an autogenerated ID if no value set @@ -364,75 +347,6 @@ - (void)notifyObservers:(BugsnagStateEvent *)event { } } -NSString *const kWindowVisible = @"Window Became Visible"; -NSString *const kWindowHidden = @"Window Became Hidden"; -NSString *const kBeganTextEdit = @"Began Editing Text"; -NSString *const kStoppedTextEdit = @"Stopped Editing Text"; -NSString *const kUndoOperation = @"Undo Operation"; -NSString *const kRedoOperation = @"Redo Operation"; -NSString *const kTableViewSelectionChange = @"TableView Select Change"; -NSString *const kAppWillTerminate = @"App Will Terminate"; -NSString *const BSGBreadcrumbLoadedMessage = @"Bugsnag loaded"; - -/** - * A map of notification names to human-readable strings - */ -- (void)initializeNotificationNameMap { - notificationNameMap = @{ -#if BSG_PLATFORM_TVOS - NSUndoManagerDidUndoChangeNotification : kUndoOperation, - NSUndoManagerDidRedoChangeNotification : kRedoOperation, - UIWindowDidBecomeVisibleNotification : kWindowVisible, - UIWindowDidBecomeHiddenNotification : kWindowHidden, - UIWindowDidBecomeKeyNotification : @"Window Became Key", - UIWindowDidResignKeyNotification : @"Window Resigned Key", - UIScreenBrightnessDidChangeNotification : @"Screen Brightness Changed", - UITableViewSelectionDidChangeNotification : kTableViewSelectionChange, - -#elif BSG_PLATFORM_IOS - UIWindowDidBecomeVisibleNotification : kWindowVisible, - UIWindowDidBecomeHiddenNotification : kWindowHidden, - UIApplicationWillTerminateNotification : kAppWillTerminate, - UIApplicationWillEnterForegroundNotification : @"App Will Enter Foreground", - UIApplicationDidEnterBackgroundNotification : @"App Did Enter Background", - UIKeyboardDidShowNotification : @"Keyboard Became Visible", - UIKeyboardDidHideNotification : @"Keyboard Became Hidden", - UIMenuControllerDidShowMenuNotification : @"Did Show Menu", - UIMenuControllerDidHideMenuNotification : @"Did Hide Menu", - NSUndoManagerDidUndoChangeNotification : kUndoOperation, - NSUndoManagerDidRedoChangeNotification : kRedoOperation, - UIApplicationUserDidTakeScreenshotNotification : @"Took Screenshot", - UITextFieldTextDidBeginEditingNotification : kBeganTextEdit, - UITextViewTextDidBeginEditingNotification : kBeganTextEdit, - UITextFieldTextDidEndEditingNotification : kStoppedTextEdit, - UITextViewTextDidEndEditingNotification : kStoppedTextEdit, - UITableViewSelectionDidChangeNotification : kTableViewSelectionChange, - UIDeviceBatteryStateDidChangeNotification : @"Battery State Changed", - UIDeviceBatteryLevelDidChangeNotification : @"Battery Level Changed", - UIDeviceOrientationDidChangeNotification : @"Orientation Changed", - UIApplicationDidReceiveMemoryWarningNotification : @"Memory Warning", - -#elif BSG_PLATFORM_OSX - NSApplicationDidBecomeActiveNotification : @"App Became Active", - NSApplicationDidResignActiveNotification : @"App Resigned Active", - NSApplicationDidHideNotification : @"App Did Hide", - NSApplicationDidUnhideNotification : @"App Did Unhide", - NSApplicationWillTerminateNotification : kAppWillTerminate, - NSWorkspaceScreensDidSleepNotification : @"Workspace Screen Slept", - NSWorkspaceScreensDidWakeNotification : @"Workspace Screen Awoke", - NSWindowWillCloseNotification : @"Window Will Close", - NSWindowDidBecomeKeyNotification : @"Window Became Key", - NSWindowWillMiniaturizeNotification : @"Window Will Miniaturize", - NSWindowDidEnterFullScreenNotification : @"Window Entered Full Screen", - NSWindowDidExitFullScreenNotification : @"Window Exited Full Screen", - NSControlTextDidBeginEditingNotification : @"Control Text Began Edit", - NSControlTextDidEndEditingNotification : @"Control Text Ended Edit", - NSMenuWillSendActionNotification : @"Menu Will Send Action", - NSTableViewSelectionDidChangeNotification : kTableViewSelectionChange, -#endif - }; -} - - (void)start { [self.configuration validate]; @@ -443,7 +357,7 @@ - (void)start { [self computeDidCrashLastLaunch]; [self.breadcrumbs removeAllBreadcrumbs]; [self setupConnectivityListener]; - [self updateAutomaticBreadcrumbDetectionSettings]; + [self.notificationBreadcrumbs start]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [self watchLifecycleEvents:center]; @@ -472,8 +386,8 @@ - (void)start { name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - self.uiDevice.batteryMonitoringEnabled = YES; - [self.uiDevice beginGeneratingDeviceOrientationNotifications]; + [UIDEVICE currentDevice].batteryMonitoringEnabled = YES; + [[UIDEVICE currentDevice] beginGeneratingDeviceOrientationNotifications]; [self batteryChanged:nil]; [self orientationChanged:nil]; @@ -498,9 +412,7 @@ - (void)start { [self.sessionTracker startNewSessionIfAutoCaptureEnabled]; // Record a "Bugsnag Loaded" message - [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState - withMessage:BSGBreadcrumbLoadedMessage - andMetadata:nil]; + [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState withMessage:@"Bugsnag loaded" andMetadata:nil]; // notification not received in time on initial startup, so trigger manually [self willEnterForeground:self]; @@ -641,8 +553,8 @@ - (void)unsubscribeFromNotifications:(id)sender { [BSGConnectivity stopMonitoring]; #if BSG_PLATFORM_IOS - self.uiDevice.batteryMonitoringEnabled = NO; - [self.uiDevice endGeneratingDeviceOrientationNotifications]; + [UIDEVICE currentDevice].batteryMonitoringEnabled = NO; + [[UIDEVICE currentDevice] endGeneratingDeviceOrientationNotifications]; #endif } @@ -730,7 +642,7 @@ - (void)leaveBreadcrumbWithMessage:(NSString *_Nonnull)message { } - (void)leaveBreadcrumbForNotificationName:(NSString *_Nonnull)notificationName { - [self startListeningForStateChangeNotification:notificationName]; + [self.notificationBreadcrumbs startListeningForStateChangeNotification:notificationName]; } - (void)leaveBreadcrumbWithMessage:(NSString *_Nonnull)message @@ -884,7 +796,7 @@ - (void)notifyOutOfMemoryEvent { // If the termination breadcrumb is set, the app entered a normal // termination flow but expired before the watchdog sentinel could // be updated. In this case, no report should be sent. - if ([name isEqualToString:kAppWillTerminate]) { + if ([name isEqualToString:@"App Will Terminate"]) { return; } } @@ -1073,9 +985,9 @@ - (void)metadataChanged:(BugsnagMetadata *)metadata { */ #if BSG_PLATFORM_IOS - (void)batteryChanged:(NSNotification *)notification { - NSNumber *batteryLevel = @(self.uiDevice.batteryLevel); - BOOL charging = self.uiDevice.batteryState == UIDeviceBatteryStateCharging || - self.uiDevice.batteryState == UIDeviceBatteryStateFull; + NSNumber *batteryLevel = @([UIDEVICE currentDevice].batteryLevel); + BOOL charging = [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateCharging || + [UIDEVICE currentDevice].batteryState == UIDeviceBatteryStateFull; [self.state addMetadata:batteryLevel withKey:BSGKeyBatteryLevel @@ -1093,7 +1005,7 @@ - (void)batteryChanged:(NSNotification *)notification { * @param notification The orientation-change notification */ - (void)orientationChanged:(NSNotification *)notification { - UIDeviceOrientation currentDeviceOrientation = self.uiDevice.orientation; + UIDeviceOrientation currentDeviceOrientation = [UIDEVICE currentDevice].orientation; NSString *orientation = BSGOrientationNameFromEnum(currentDeviceOrientation); // No orientation, nothing to be done @@ -1117,7 +1029,7 @@ - (void)orientationChanged:(NSNotification *)notification { // Send a breadcrumb and preserve the orientation. [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState - withMessage:BSGBreadcrumbNameForNotificationName(notification.name) + withMessage:@"Orientation Changed" andMetadata:@{ @"from" : _lastOrientation, @"to" : orientation @@ -1130,11 +1042,8 @@ - (void)lowMemoryWarning:(NSNotification *)notif { [self.state addMetadata:[BSG_RFC3339DateTool stringFromDate:[NSDate date]] withKey:BSEventLowMemoryWarning toSection:BSGKeyDeviceState]; - - if ([[self configuration] shouldRecordBreadcrumbType:BSGBreadcrumbTypeState]) { - [self sendBreadcrumbForNotification:notif]; - } } + #endif /** @@ -1158,280 +1067,10 @@ - (void)addAutoBreadcrumbOfType:(BSGBreadcrumbType)breadcrumbType } } -/** - * Configure event listeners (i.e. observers) for enabled automatic breadcrumbs. - */ -- (void)updateAutomaticBreadcrumbDetectionSettings { - // State events - if ([[self configuration] shouldRecordBreadcrumbType:BSGBreadcrumbTypeState]) { - // Generic state events - for (NSString *name in [self automaticBreadcrumbStateEvents]) { - [self startListeningForStateChangeNotification:name]; - } - -#if BSG_PLATFORM_OSX - // Workspace-specific events - MacOS only - for (NSString *name in [self workspaceBreadcrumbStateEvents]) { - [self startListeningForWorkspaceStateChangeNotifications:name]; - } -#endif - - // NSMenu events (Mac only) - for (NSString *name in [self automaticBreadcrumbMenuItemEvents]) { - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(sendBreadcrumbForMenuItemNotification:) - name:name - object:nil]; - } - } - - // Navigation events - if ([[self configuration] shouldRecordBreadcrumbType:BSGBreadcrumbTypeNavigation]) { - // UI/NSTableView events - for (NSString *name in [self automaticBreadcrumbTableItemEvents]) { - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(sendBreadcrumbForTableViewNotification:) - name:name - object:nil]; - } - } - - // User events - if ([[self configuration] shouldRecordBreadcrumbType:BSGBreadcrumbTypeUser]) { - // UITextField/NSControl events (text editing) - for (NSString *name in [self automaticBreadcrumbControlEvents]) { - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(sendBreadcrumbForControlNotification:) - name:name - object:nil]; - } - } -} - -/** - * NSWorkspace-specific automatic breadcrumb events - */ -- (NSArray *)workspaceBreadcrumbStateEvents { -#if BSG_PLATFORM_OSX - return @[ - NSWorkspaceScreensDidSleepNotification, - NSWorkspaceScreensDidWakeNotification - ]; -#endif - - // Fall-through - return nil; -} - -- (NSArray *)automaticBreadcrumbStateEvents { -#if BSG_PLATFORM_TVOS - return @[ - NSUndoManagerDidUndoChangeNotification, - NSUndoManagerDidRedoChangeNotification, - UIWindowDidBecomeVisibleNotification, - UIWindowDidBecomeHiddenNotification, UIWindowDidBecomeKeyNotification, - UIWindowDidResignKeyNotification, - UIScreenBrightnessDidChangeNotification - ]; -#elif BSG_PLATFORM_IOS - return @[ - UIWindowDidBecomeHiddenNotification, - UIWindowDidBecomeVisibleNotification, - UIApplicationWillTerminateNotification, - UIApplicationWillEnterForegroundNotification, - UIApplicationDidEnterBackgroundNotification, - UIKeyboardDidShowNotification, UIKeyboardDidHideNotification, - UIMenuControllerDidShowMenuNotification, - UIMenuControllerDidHideMenuNotification, - NSUndoManagerDidUndoChangeNotification, - NSUndoManagerDidRedoChangeNotification, -#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 - UIApplicationUserDidTakeScreenshotNotification -#endif - ]; -#elif BSG_PLATFORM_OSX - return @[ - NSApplicationDidBecomeActiveNotification, - NSApplicationDidResignActiveNotification, - NSApplicationDidHideNotification, - NSApplicationDidUnhideNotification, - NSApplicationWillTerminateNotification, - - NSWindowWillCloseNotification, - NSWindowDidBecomeKeyNotification, - NSWindowWillMiniaturizeNotification, - NSWindowDidEnterFullScreenNotification, - NSWindowDidExitFullScreenNotification - ]; -#endif - - // Fall-through - return nil; -} - -- (NSArray *)automaticBreadcrumbControlEvents { -#if BSG_PLATFORM_IOS - return @[ - UITextFieldTextDidBeginEditingNotification, - UITextViewTextDidBeginEditingNotification, - UITextFieldTextDidEndEditingNotification, - UITextViewTextDidEndEditingNotification - ]; -#elif BSG_PLATFORM_OSX - return @[ - NSControlTextDidBeginEditingNotification, - NSControlTextDidEndEditingNotification - ]; -#endif - - // Fall-through - return nil; -} - -- (NSArray *)automaticBreadcrumbTableItemEvents { -#if BSG_PLATFORM_IOS || BSG_PLATFORM_TVOS - return @[ UITableViewSelectionDidChangeNotification ]; -#elif BSG_PLATFORM_OSX - return @[ NSTableViewSelectionDidChangeNotification ]; -#endif - - // Fall-through - return nil; -} - -- (NSArray *)automaticBreadcrumbMenuItemEvents { -#if BSG_PLATFORM_TVOS - return @[]; -#elif BSG_PLATFORM_IOS - return nil; -#elif BSG_PLATFORM_OSX - return @[ NSMenuWillSendActionNotification ]; -#endif - - // Fall-through - return nil; -} - -/** - * Configure a generic state change breadcrumb listener - * - * @param notificationName The name of the notification. - */ -- (void)startListeningForStateChangeNotification:(NSString *)notificationName { - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(sendBreadcrumbForNotification:) - name:notificationName - object:nil]; -} - -/** - * Configure an NSWorkspace-specific state change breadcrumb listener. MacOS only. - * - * @param notificationName The name of the notification. - */ -#if BSG_PLATFORM_OSX -- (void)startListeningForWorkspaceStateChangeNotifications:(NSString *)notificationName { - [NSWorkspace.sharedWorkspace.notificationCenter - addObserver:self - selector:@selector(sendBreadcrumbForNotification:) - name:notificationName - object:nil]; - } -#endif - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)sendBreadcrumbForNotification:(NSNotification *)note { - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeState; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(note.name); - }]; -} - -/** - * Leave a navigation breadcrumb whenever a tableView selection changes - * - * @param notification The UI/NSTableViewSelectionDidChangeNotification - */ -- (void)sendBreadcrumbForTableViewNotification:(NSNotification *)notification { -#if BSG_PLATFORM_IOS || BSG_PLATFORM_TVOS - UITableView *tableView = [notification object]; - NSIndexPath *indexPath = [tableView indexPathForSelectedRow]; - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeNavigation; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(notification.name); - if (indexPath) { - breadcrumb.metadata = - @{ @"row" : @(indexPath.row), - @"section" : @(indexPath.section) }; - } - }]; -#elif BSG_PLATFORM_OSX - NSTableView *tableView = [notification object]; - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeNavigation; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(notification.name); - if (tableView) { - breadcrumb.metadata = @{ - @"selectedRow" : @(tableView.selectedRow), - @"selectedColumn" : @(tableView.selectedColumn) - }; - } - }]; -#endif -} - -/** -* Leave a state breadcrumb whenever a tableView selection changes -* -* @param notification The UI/NSTableViewSelectionDidChangeNotification -*/ -- (void)sendBreadcrumbForMenuItemNotification:(NSNotification *)notification { -#if BSG_PLATFORM_OSX - NSMenuItem *menuItem = [[notification userInfo] valueForKey:@"MenuItem"]; - if ([menuItem isKindOfClass:[NSMenuItem class]]) { - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeState; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(notification.name); - if (menuItem.title.length > 0) - breadcrumb.metadata = @{BSGKeyAction : menuItem.title}; - }]; - } -#endif -} - -- (void)sendBreadcrumbForControlNotification:(NSNotification *)note { -#if BSG_PLATFORM_IOS - UIControl *control = note.object; - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeUser; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(note.name); - NSString *label = control.accessibilityLabel; - if (label.length > 0) { - breadcrumb.metadata = @{BSGKeyLabel : label}; - } - }]; -#elif BSG_PLATFORM_OSX - NSControl *control = note.object; - [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeUser; - breadcrumb.message = BSGBreadcrumbNameForNotificationName(note.name); - if ([control respondsToSelector:@selector(accessibilityLabel)]) { - NSString *label = control.accessibilityLabel; - if (label.length > 0) { - breadcrumb.metadata = @{BSGKeyLabel : label}; - } - } - }]; -#endif -} - // MARK: - - (void)addMetadata:(NSDictionary *_Nonnull)metadata diff --git a/Tests/BugsnagClientMirrorTest.m b/Tests/BugsnagClientMirrorTest.m index 8f3cbcf32..839f3b59e 100644 --- a/Tests/BugsnagClientMirrorTest.m +++ b/Tests/BugsnagClientMirrorTest.m @@ -128,13 +128,13 @@ - (void)setUp { @"configMetadataFromLastLaunch @16@0:8", @"metadataFile @16@0:8", @"metadataFromLastLaunch @16@0:8", + @"notificationBreadcrumbs @16@0:8", @"setConfigMetadataFromLastLaunch: v24@0:8@16", @"setMetadataFromLastLaunch: v24@0:8@16", + @"setNotificationBreadcrumbs: v24@0:8@16", @"setStateMetadataFromLastLaunch: v24@0:8@16", - @"setUiDevice: v24@0:8@16", @"stateMetadataFile @16@0:8", @"stateMetadataFromLastLaunch @16@0:8", - @"uiDevice @16@0:8", ]]; // the following methods are implemented on Bugsnag but do not need to diff --git a/Tests/NotificationBreadcrumbTests.m b/Tests/NotificationBreadcrumbTests.m index d111efbef..12cdcc6e9 100644 --- a/Tests/NotificationBreadcrumbTests.m +++ b/Tests/NotificationBreadcrumbTests.m @@ -8,23 +8,19 @@ #import -#import "BugsnagBreadcrumb+Private.h" -#import "BugsnagBreadcrumbs.h" -#import "BugsnagClient+Private.h" +#import -#if TARGET_OS_IOS -#import "UIDeviceStub.h" -#endif +#import "BSGNotificationBreadcrumbs.h" +#import "BugsnagBreadcrumb+Private.h" -@interface NotificationBreadcrumbTests : XCTestCase +@interface NotificationBreadcrumbTests : XCTestCase @property NSNotificationCenter *notificationCenter; @property id notificationObject; @property NSDictionary *notificationUserInfo; -@property BugsnagClient *client; - +@property BSGNotificationBreadcrumbs *notificationBreadcrumbs; @property (nonatomic) BugsnagBreadcrumb *breadcrumb; @end @@ -39,16 +35,11 @@ @implementation NotificationBreadcrumbTests - (void)setUp { self.breadcrumb = nil; BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:@"0192837465afbecd0192837465afbecd"]; - self.client = [[BugsnagClient alloc] initWithConfiguration:configuration]; - self.client.breadcrumbs = (id)self; -#if TARGET_OS_IOS - self.client.uiDevice = (id)[[UIDeviceStub alloc] init]; - ((UIDeviceStub *)self.client.uiDevice).orientation = UIDeviceOrientationPortrait; -#endif - [self.client start]; + self.notificationBreadcrumbs = [[BSGNotificationBreadcrumbs alloc] initWithConfiguration:configuration breadcrumbSink:self]; self.notificationCenter = NSNotificationCenter.defaultCenter; self.notificationObject = nil; self.notificationUserInfo = nil; + [self.notificationBreadcrumbs start]; } - (BugsnagBreadcrumb *)breadcrumbForNotificationWithName:(NSString *)name { @@ -58,6 +49,16 @@ - (BugsnagBreadcrumb *)breadcrumbForNotificationWithName:(NSString *)name { return self.breadcrumb; } +#pragma mark BSGBreadcrumbSink + +- (void)leaveBreadcrumbWithMessage:(NSString *)message metadata:(NSDictionary *)metadata andType:(BSGBreadcrumbType)type { + self.breadcrumb = [BugsnagBreadcrumb breadcrumbWithBlock:^(BugsnagBreadcrumb *breadcrumb) { + breadcrumb.message = message; + breadcrumb.metadata = metadata; + breadcrumb.type = type; + }]; +} + #define TEST(__NAME__, __TYPE__, __MESSAGE__, __METADATA__) do { \ BugsnagBreadcrumb *breadcrumb = [self breadcrumbForNotificationWithName:__NAME__]; \ XCTAssertNotNil(breadcrumb); \ @@ -68,12 +69,6 @@ - (BugsnagBreadcrumb *)breadcrumbForNotificationWithName:(NSString *)name { } \ } while (0) -#pragma mark Tests - -- (void)testStartedBreadcrumb { - XCTAssertEqualObjects(self.breadcrumb.message, @"Bugsnag loaded"); -} - #pragma mark iOS Tests #if TARGET_OS_IOS @@ -86,12 +81,6 @@ - (void)testUIApplicationNotifications { TEST(UIApplicationWillTerminateNotification, BSGBreadcrumbTypeState, @"App Will Terminate", @{}); } -- (void)testUIDeviceNotifications { - ((UIDeviceStub *)self.client.uiDevice).orientation = UIDeviceOrientationLandscapeLeft; - TEST(UIDeviceOrientationDidChangeNotification, BSGBreadcrumbTypeState, @"Orientation Changed", - (@{@"from": @"portrait", @"to": @"landscapeleft"})); -} - - (void)testUIKeyboardNotifications { TEST(UIKeyboardDidHideNotification, BSGBreadcrumbTypeState, @"Keyboard Became Hidden", @{}); TEST(UIKeyboardDidShowNotification, BSGBreadcrumbTypeState, @"Keyboard Became Visible", @{}); @@ -203,22 +192,3 @@ - (void)testNSWorkspaceNotifications { #endif @end - - -#pragma mark - - -@implementation NotificationBreadcrumbTests (BugsnagBreadcrumbsMock) - -- (void)removeAllBreadcrumbs {} - -- (void)addBreadcrumb:(NSString *)message {} - -- (void)addBreadcrumbWithBlock:(BSGBreadcrumbConfiguration)block { - self.breadcrumb = [BugsnagBreadcrumb breadcrumbWithBlock:block]; -} - -- (NSArray *)breadcrumbs { - return @[]; -} - -@end diff --git a/Tests/UIDeviceStub.h b/Tests/UIDeviceStub.h deleted file mode 100644 index c443a55b1..000000000 --- a/Tests/UIDeviceStub.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// UIDeviceStub.h -// Bugsnag-iOSTests -// -// Created by Nick Dowell on 10/12/2020. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface UIDeviceStub : NSObject - -@property double batteryLevel; - -@property BOOL batteryMonitoringEnabled; - -@property UIDeviceBatteryState batteryState; - -@property UIDeviceOrientation orientation; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Tests/UIDeviceStub.m b/Tests/UIDeviceStub.m deleted file mode 100644 index bc7b32a4e..000000000 --- a/Tests/UIDeviceStub.m +++ /dev/null @@ -1,17 +0,0 @@ -// -// UIDeviceStub.m -// Bugsnag-iOSTests -// -// Created by Nick Dowell on 10/12/2020. -// Copyright © 2020 Bugsnag Inc. All rights reserved. -// - -#import "UIDeviceStub.h" - -@implementation UIDeviceStub - -- (void)beginGeneratingDeviceOrientationNotifications {} - -- (void)endGeneratingDeviceOrientationNotifications {} - -@end From 02f0da10303fa82bf1949746e481e63f8d8eb007 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Tue, 15 Dec 2020 13:16:32 +0000 Subject: [PATCH 07/33] Avoid duplication of breadcrumb messages --- Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h | 4 ++++ Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m | 7 +++++-- Bugsnag/Client/BugsnagClient.m | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h index c35fab68f..074dd3e24 100644 --- a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.h @@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - +extern NSString * const BSGNotificationBreadcrumbsMessageAppWillTerminate; + @interface BSGNotificationBreadcrumbs : NSObject #pragma mark Initializers @@ -48,6 +50,8 @@ NS_ASSUME_NONNULL_BEGIN /// Starts observing notifications with the given name and adds a "state" breadcrumbs when received. - (void)startListeningForStateChangeNotification:(NSNotificationName)notificationName; +- (NSString *)messageForNotificationName:(NSNotificationName)name; + @end NS_ASSUME_NONNULL_END diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m index f08568be1..e02b5dc56 100644 --- a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m @@ -19,6 +19,9 @@ #endif +NSString * const BSGNotificationBreadcrumbsMessageAppWillTerminate = @"App Will Terminate"; + + @interface BSGNotificationBreadcrumbs () @property NSDictionary *notificationNameMap; @@ -54,7 +57,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration UIApplicationDidReceiveMemoryWarningNotification : @"Memory Warning", UIApplicationUserDidTakeScreenshotNotification : @"Took Screenshot", UIApplicationWillEnterForegroundNotification : @"App Will Enter Foreground", - UIApplicationWillTerminateNotification : @"App Will Terminate", + UIApplicationWillTerminateNotification : BSGNotificationBreadcrumbsMessageAppWillTerminate, UIDeviceBatteryLevelDidChangeNotification : @"Battery Level Changed", UIDeviceBatteryStateDidChangeNotification : @"Battery State Changed", UIDeviceOrientationDidChangeNotification : @"Orientation Changed", @@ -76,7 +79,7 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration NSApplicationDidHideNotification : @"App Did Hide", NSApplicationDidResignActiveNotification : @"App Resigned Active", NSApplicationDidUnhideNotification : @"App Did Unhide", - NSApplicationWillTerminateNotification : @"App Will Terminate", + NSApplicationWillTerminateNotification : BSGNotificationBreadcrumbsMessageAppWillTerminate, NSControlTextDidBeginEditingNotification : @"Control Text Began Edit", NSControlTextDidEndEditingNotification : @"Control Text Ended Edit", NSMenuWillSendActionNotification : @"Menu Will Send Action", diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 1146ae06f..9ce80486f 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -796,7 +796,7 @@ - (void)notifyOutOfMemoryEvent { // If the termination breadcrumb is set, the app entered a normal // termination flow but expired before the watchdog sentinel could // be updated. In this case, no report should be sent. - if ([name isEqualToString:@"App Will Terminate"]) { + if ([name isEqualToString:BSGNotificationBreadcrumbsMessageAppWillTerminate]) { return; } } @@ -1029,7 +1029,7 @@ - (void)orientationChanged:(NSNotification *)notification { // Send a breadcrumb and preserve the orientation. [self addAutoBreadcrumbOfType:BSGBreadcrumbTypeState - withMessage:@"Orientation Changed" + withMessage:[self.notificationBreadcrumbs messageForNotificationName:notification.name] andMetadata:@{ @"from" : _lastOrientation, @"to" : orientation From e1df9d2f686b2caf684d9f3849dc0300219e37ac Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Tue, 15 Dec 2020 13:17:01 +0000 Subject: [PATCH 08/33] Remove debug NSLog statements and fix typos --- Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m index e02b5dc56..194cf153f 100644 --- a/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m +++ b/Bugsnag/Breadcrumbs/BSGNotificationBreadcrumbs.m @@ -213,31 +213,28 @@ - (void)start { } #if TARGET_OS_OSX - // Workspace-specific events - MacOS only + // Workspace-specific events - macOS only for (NSNotificationName name in [self workspaceBreadcrumbStateEvents]) { - NSLog(@"%@", name); [_workspaceNotificationCenter addObserver:self selector:@selector(addBreadcrumbForNotification:) name:name object:nil]; } -#endif - // NSMenu events (Mac only) + // NSMenu events (macOS only) for (NSNotificationName name in [self automaticBreadcrumbMenuItemEvents]) { - NSLog(@"%@", name); [_notificationCenter addObserver:self selector:@selector(addBreadcrumbForMenuItemNotification:) name:name object:nil]; } +#endif } // Navigation events if ([_configuration shouldRecordBreadcrumbType:BSGBreadcrumbTypeNavigation]) { // UI/NSTableView events for (NSNotificationName name in [self automaticBreadcrumbTableItemEvents]) { - NSLog(@"%@", name); [_notificationCenter addObserver:self selector:@selector(addBreadcrumbForTableViewNotification:) name:name @@ -249,7 +246,6 @@ - (void)start { if ([_configuration shouldRecordBreadcrumbType:BSGBreadcrumbTypeUser]) { // UITextField/NSControl events (text editing) for (NSNotificationName name in [self automaticBreadcrumbControlEvents]) { - NSLog(@"%@", name); [_notificationCenter addObserver:self selector:@selector(addBreadcrumbForControlNotification:) name:name From 0c4c7926b2278dc11e7444ccf8a65a0cc5db7bab Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Fri, 11 Dec 2020 10:04:25 +0000 Subject: [PATCH 09/33] Wait for network connectivity before running E2E scenario --- features/fixtures/shared/scenarios/Scenario.h | 6 +++++ features/fixtures/shared/scenarios/Scenario.m | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/features/fixtures/shared/scenarios/Scenario.h b/features/fixtures/shared/scenarios/Scenario.h index 2e1c392f2..879619933 100644 --- a/features/fixtures/shared/scenarios/Scenario.h +++ b/features/fixtures/shared/scenarios/Scenario.h @@ -17,6 +17,11 @@ void markErrorHandledCallback(const BSG_KSCrashReportWriter * _Nonnull writer); - (instancetype _Nonnull)initWithConfig:(BugsnagConfiguration *_Nonnull)config; +/** + * Blocks the calling thread until network connectivity to the notify endpoint has been verified. + */ +- (void)waitForNetworkConnectivity; + /** * Executes the test case */ @@ -27,4 +32,5 @@ void markErrorHandledCallback(const BSG_KSCrashReportWriter * _Nonnull writer); - (void)didEnterBackgroundNotification; @property (nonatomic, strong, nullable) NSString *eventMode; + @end diff --git a/features/fixtures/shared/scenarios/Scenario.m b/features/fixtures/shared/scenarios/Scenario.m index f03c2d052..a67ffeae0 100644 --- a/features/fixtures/shared/scenarios/Scenario.m +++ b/features/fixtures/shared/scenarios/Scenario.m @@ -45,10 +45,36 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config { return self; } +- (void)waitForNetworkConnectivity { + NSDictionary *proxySettings = (__bridge_transfer NSDictionary *)CFNetworkCopySystemProxySettings(); + NSLog(@"*** Proxy settings = %@", proxySettings); + + // This check uses HTTP rather than sockets because connectivity is commonly provided via an HTTP proxy. + + NSURL *url = [NSURL URLWithString:self.config.endpoints.notify]; + NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:3]; + NSLog(@"*** Checking for connectivity to %@", url); + while (1) { + NSURLResponse *response = nil; + NSError *error = nil; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; +#pragma clang diagnostic pop + if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode / 100 == 2) { + NSLog(@"*** Received response from notify endpoint."); + break; + } + NSLog(@"*** No response from notify endpoint, retrying in 1 second..."); + [NSThread sleepForTimeInterval:1]; + } +} + - (void)run { } - (void)startBugsnag { + [self waitForNetworkConnectivity]; [Bugsnag startWithConfiguration:self.config]; } From 23a49baaa5157ad47d00795d2bcab702eba2c0f0 Mon Sep 17 00:00:00 2001 From: Karl Stenerud Date: Tue, 15 Dec 2020 10:09:15 +0100 Subject: [PATCH 10/33] Added maxPersistedSessions config option so that users can control how many unsent sessions will be kept before deleting the oldest. --- Bugsnag/BugsnagSessionTracker.m | 3 +- .../Configuration/BSGConfigurationBuilder.m | 2 ++ Bugsnag/Configuration/BugsnagConfiguration.m | 22 +++++++++++++ Bugsnag/Helpers/BugsnagKeys.h | 1 + Bugsnag/Helpers/BugsnagKeys.m | 1 + Bugsnag/Storage/BugsnagSessionFileStore.h | 3 +- Bugsnag/Storage/BugsnagSessionFileStore.m | 22 +++++++++++-- .../include/Bugsnag/BugsnagConfiguration.h | 8 +++++ Tests/BSGConfigurationBuilderTests.m | 4 +++ Tests/BugsnagClientTests.m | 4 +++ Tests/BugsnagConfigurationTests.m | 32 +++++++++++++++++++ Tests/ConfigurationApiValidationTest.m | 19 +++++++++++ 12 files changed, 117 insertions(+), 4 deletions(-) diff --git a/Bugsnag/BugsnagSessionTracker.m b/Bugsnag/BugsnagSessionTracker.m index a93aee55d..75761cb71 100644 --- a/Bugsnag/BugsnagSessionTracker.m +++ b/Bugsnag/BugsnagSessionTracker.m @@ -58,7 +58,8 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config if (!storePath) { BSG_KSLOG_ERROR(@"Failed to initialize session store."); } - _sessionStore = [BugsnagSessionFileStore storeWithPath:storePath]; + + _sessionStore = [BugsnagSessionFileStore storeWithPath:storePath maxPersistedSessions:config.maxPersistedSessions]; _extraRuntimeInfo = [NSMutableDictionary new]; } return self; diff --git a/Bugsnag/Configuration/BSGConfigurationBuilder.m b/Bugsnag/Configuration/BSGConfigurationBuilder.m index b5c1a2070..8574605ec 100644 --- a/Bugsnag/Configuration/BSGConfigurationBuilder.m +++ b/Bugsnag/Configuration/BSGConfigurationBuilder.m @@ -31,6 +31,7 @@ + (BugsnagConfiguration *)configurationFromOptions:(NSDictionary *)options { BSGKeyEndpoints, BSGKeyMaxBreadcrumbs, BSGKeyMaxPersistedEvents, + BSGKeyMaxPersistedSessions, BSGKeyPersistUser, BSGKeyRedactedKeys, BSGKeyReleaseStage, @@ -57,6 +58,7 @@ + (BugsnagConfiguration *)configurationFromOptions:(NSDictionary *)options { [self loadNumber:config options:options key:BSGKeyMaxBreadcrumbs]; [self loadNumber:config options:options key:BSGKeyMaxPersistedEvents]; + [self loadNumber:config options:options key:BSGKeyMaxPersistedSessions]; [self loadSendThreads:config options:options]; return config; } diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 98dddbf76..c89fca0df 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -85,6 +85,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { copy.discardClasses = self.discardClasses; [copy setRedactedKeys:self.redactedKeys]; [copy setMaxPersistedEvents:self.maxPersistedEvents]; + [copy setMaxPersistedSessions:self.maxPersistedSessions]; [copy setMaxBreadcrumbs:self.maxBreadcrumbs]; copy->_metadata = [[BugsnagMetadata alloc] initWithDictionary:[[self.metadata toDictionary] mutableCopy]]; [copy setEndpoints:self.endpoints]; @@ -162,6 +163,7 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { _redactedKeys = [NSSet setWithArray:@[@"password"]]; _enabledBreadcrumbTypes = BSGEnabledBreadcrumbTypeAll; _maxPersistedEvents = 12; + _maxPersistedSessions = 32; _maxBreadcrumbs = 25; _autoTrackSessions = YES; _sendThreads = BSGThreadSendPolicyAlways; @@ -438,6 +440,26 @@ - (void)setMaxPersistedEvents:(NSUInteger)maxPersistedEvents { } } +@synthesize maxPersistedSessions = _maxPersistedSessions; + +- (NSUInteger)maxPersistedSessions { + @synchronized (self) { + return _maxPersistedSessions; + } +} + +- (void)setMaxPersistedSessions:(NSUInteger)maxPersistedSessions { + @synchronized (self) { + if (maxPersistedSessions >= 1 && maxPersistedSessions <= 100) { + _maxPersistedSessions = maxPersistedSessions; + } else { + bsg_log_err(@"Invalid configuration value detected. Option maxPersistedSessions " + "should be an integer between 1-100. Supplied value is %lu", + (unsigned long) maxPersistedSessions); + } + } +} + @synthesize maxBreadcrumbs = _maxBreadcrumbs; - (NSUInteger)maxBreadcrumbs { diff --git a/Bugsnag/Helpers/BugsnagKeys.h b/Bugsnag/Helpers/BugsnagKeys.h index 90fc4c835..37c8c1cb3 100644 --- a/Bugsnag/Helpers/BugsnagKeys.h +++ b/Bugsnag/Helpers/BugsnagKeys.h @@ -64,6 +64,7 @@ extern NSString *const BSGKeyMachoUUID; extern NSString *const BSGKeyMachoVMAddress; extern NSString *const BSGKeyMaxBreadcrumbs; extern NSString *const BSGKeyMaxPersistedEvents; +extern NSString *const BSGKeyMaxPersistedSessions; extern NSString *const BSGKeyMessage; extern NSString *const BSGKeyMetadata; extern NSString *const BSGKeyMethod; diff --git a/Bugsnag/Helpers/BugsnagKeys.m b/Bugsnag/Helpers/BugsnagKeys.m index 060fd3f35..62d22abb0 100644 --- a/Bugsnag/Helpers/BugsnagKeys.m +++ b/Bugsnag/Helpers/BugsnagKeys.m @@ -60,6 +60,7 @@ NSString *const BSGKeyMachoVMAddress = @"machoVMAddress"; NSString *const BSGKeyMaxBreadcrumbs = @"maxBreadcrumbs"; NSString *const BSGKeyMaxPersistedEvents = @"maxPersistedEvents"; +NSString *const BSGKeyMaxPersistedSessions = @"maxPersistedSessions"; NSString *const BSGKeyMessage = @"message"; NSString *const BSGKeyMetadata = @"metaData"; NSString *const BSGKeyMethod = @"method"; diff --git a/Bugsnag/Storage/BugsnagSessionFileStore.h b/Bugsnag/Storage/BugsnagSessionFileStore.h index 6a5616c04..3e9b3bcf1 100644 --- a/Bugsnag/Storage/BugsnagSessionFileStore.h +++ b/Bugsnag/Storage/BugsnagSessionFileStore.h @@ -9,7 +9,8 @@ #import "BugsnagSession.h" @interface BugsnagSessionFileStore : BugsnagFileStore -+ (BugsnagSessionFileStore *)storeWithPath:(NSString *)path; ++ (BugsnagSessionFileStore *)storeWithPath:(NSString *)path + maxPersistedSessions:(NSUInteger)maxPersistedSessions; - (void)write:(BugsnagSession *)session; diff --git a/Bugsnag/Storage/BugsnagSessionFileStore.m b/Bugsnag/Storage/BugsnagSessionFileStore.m index 95951b56f..49b984426 100644 --- a/Bugsnag/Storage/BugsnagSessionFileStore.m +++ b/Bugsnag/Storage/BugsnagSessionFileStore.m @@ -11,11 +11,27 @@ static NSString *const kSessionStoreSuffix = @"-Session-"; +@interface BugsnagSessionFileStore () + +@property NSUInteger maxPersistedSessions; + +@end + @implementation BugsnagSessionFileStore -+ (BugsnagSessionFileStore *)storeWithPath:(NSString *)path { ++ (BugsnagSessionFileStore *)storeWithPath:(NSString *)path + maxPersistedSessions:(NSUInteger)maxPersistedSessions { return [[self alloc] initWithPath:path - filenameSuffix:kSessionStoreSuffix]; + maxPersistedSessions:maxPersistedSessions]; +} + +- (instancetype) initWithPath:(NSString *)path + maxPersistedSessions:(NSUInteger)maxPersistedSessions { + if ((self = [super initWithPath:path + filenameSuffix:kSessionStoreSuffix])) { + _maxPersistedSessions = maxPersistedSessions; + } + return self; } - (void)write:(BugsnagSession *)session { @@ -30,6 +46,8 @@ - (void)write:(BugsnagSession *)session { BSG_KSLOG_ERROR(@"Failed to write session %@", error); return; } + + [self pruneFilesLeaving:(int)self.maxPersistedSessions]; } diff --git a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h index d29b9de08..b6fa258e3 100644 --- a/Bugsnag/include/Bugsnag/BugsnagConfiguration.h +++ b/Bugsnag/include/Bugsnag/BugsnagConfiguration.h @@ -226,6 +226,14 @@ typedef BOOL (^BugsnagOnSessionBlock)(BugsnagSession *_Nonnull session); */ @property (nonatomic) NSUInteger maxPersistedEvents; +/** + * Sets the maximum number of sessions which will be stored. Once the threshold is reached, + * the oldest sessions will be deleted. + * + * By default, 32 sessions are stored: this can be amended up to a maximum of 100. + */ +@property (nonatomic) NSUInteger maxPersistedSessions; + /** * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, * the oldest breadcrumbs will be deleted. diff --git a/Tests/BSGConfigurationBuilderTests.m b/Tests/BSGConfigurationBuilderTests.m index 3ad82f82d..0ed21ab39 100644 --- a/Tests/BSGConfigurationBuilderTests.m +++ b/Tests/BSGConfigurationBuilderTests.m @@ -56,6 +56,7 @@ - (void)testDecodeDefaultValues { XCTAssertTrue(config.autoDetectErrors); XCTAssertTrue(config.autoTrackSessions); XCTAssertEqual(12, config.maxPersistedEvents); + XCTAssertEqual(32, config.maxPersistedSessions); XCTAssertEqual(25, config.maxBreadcrumbs); XCTAssertTrue(config.persistUser); XCTAssertEqualObjects(@[@"password"], [config.redactedKeys allObjects]); @@ -94,6 +95,7 @@ - (void)testDecodeFullConfig { }, @"enabledReleaseStages": @[@"beta2", @"prod"], @"maxPersistedEvents": @29, + @"maxPersistedSessions": @19, @"maxBreadcrumbs": @27, @"persistUser": @NO, @"redactedKeys": @[@"foo"], @@ -108,6 +110,7 @@ - (void)testDecodeFullConfig { XCTAssertFalse(config.autoTrackSessions); XCTAssertEqualObjects(@"7.22", config.bundleVersion); XCTAssertEqual(29, config.maxPersistedEvents); + XCTAssertEqual(19, config.maxPersistedSessions); XCTAssertEqual(27, config.maxBreadcrumbs); XCTAssertFalse(config.persistUser); XCTAssertEqualObjects(@[@"foo"], config.redactedKeys); @@ -142,6 +145,7 @@ - (void)testInvalidConfigOptions { @"enabledReleaseStages": @[@"beta2", @"prod"], @"enabledErrorTypes": @[@"ooms", @"signals"], @"maxPersistedEvents": @29, + @"maxPersistedSessions": @19, @"maxBreadcrumbs": @27, @"persistUser": @"pomelo", @"redactedKeys": @[@77], diff --git a/Tests/BugsnagClientTests.m b/Tests/BugsnagClientTests.m index b61e1639a..7d5ea45d0 100644 --- a/Tests/BugsnagClientTests.m +++ b/Tests/BugsnagClientTests.m @@ -188,6 +188,7 @@ - (void)assertEqualConfiguration:(BugsnagConfiguration *)expected withActual:(Bu XCTAssertEqualObjects(expected.endpoints.notify, actual.endpoints.notify); XCTAssertEqualObjects(expected.endpoints.sessions, actual.endpoints.sessions); XCTAssertEqual(expected.maxPersistedEvents, actual.maxPersistedEvents); + XCTAssertEqual(expected.maxPersistedSessions, actual.maxPersistedSessions); XCTAssertEqual(expected.maxBreadcrumbs, actual.maxBreadcrumbs); XCTAssertEqual(expected.persistUser, actual.persistUser); XCTAssertEqual([expected.redactedKeys count], [actual.redactedKeys count]); @@ -211,12 +212,14 @@ - (void)testChangesToConfigurationAreIgnoredAfterCallingStart { // Modify some arbitrary properties config.persistUser = !config.persistUser; config.maxPersistedEvents = config.maxPersistedEvents * 2; + config.maxPersistedSessions = config.maxPersistedSessions * 2; config.maxBreadcrumbs = config.maxBreadcrumbs * 2; config.appVersion = @"99.99.99"; // Ensure the changes haven't been reflected in our copy XCTAssertNotEqual(initialConfig.persistUser, config.persistUser); XCTAssertNotEqual(initialConfig.maxPersistedEvents, config.maxPersistedEvents); + XCTAssertNotEqual(initialConfig.maxPersistedSessions, config.maxPersistedSessions); XCTAssertNotEqual(initialConfig.maxBreadcrumbs, config.maxBreadcrumbs); XCTAssertNotEqualObjects(initialConfig.appVersion, config.appVersion); @@ -235,6 +238,7 @@ - (void)testStartingBugsnagTwiceLogsAWarningAndIgnoresNewConfiguration { updatedConfig.persistUser = !initialConfig.persistUser; updatedConfig.maxBreadcrumbs = initialConfig.maxBreadcrumbs * 2; updatedConfig.maxPersistedEvents = initialConfig.maxPersistedEvents * 2; + updatedConfig.maxPersistedSessions = initialConfig.maxPersistedSessions * 2; updatedConfig.appVersion = @"99.99.99"; [Bugsnag startWithConfiguration:updatedConfig]; diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index df4cd6197..99134cca2 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -561,6 +561,38 @@ - (void)testMaxPersistedEvents { XCTAssertEqual(1, config.maxPersistedEvents); } +// ============================================================================= +// MARK: - Max Persisted Sessions +// ============================================================================= + +- (void)testMaxPersistedSessions { + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + XCTAssertEqual(32, config.maxPersistedSessions); + + // alter to valid value + config.maxPersistedSessions = 10; + XCTAssertEqual(10, config.maxPersistedSessions); + + // alter to max value + config.maxPersistedSessions = 100; + XCTAssertEqual(100, config.maxPersistedSessions); + + // alter to min value + config.maxPersistedSessions = 1; + XCTAssertEqual(1, config.maxPersistedSessions); + + config.maxPersistedSessions = 0; + XCTAssertEqual(1, config.maxPersistedSessions); + + // alter to negative value + config.maxPersistedSessions = -1; + XCTAssertEqual(1, config.maxPersistedSessions); + + // alter to > max value + config.maxPersistedSessions = 500; + XCTAssertEqual(1, config.maxPersistedSessions); +} + // ============================================================================= // MARK: - Max Breadcrumb // ============================================================================= diff --git a/Tests/ConfigurationApiValidationTest.m b/Tests/ConfigurationApiValidationTest.m index 4afa12d5f..1d2640b30 100644 --- a/Tests/ConfigurationApiValidationTest.m +++ b/Tests/ConfigurationApiValidationTest.m @@ -147,6 +147,25 @@ - (void)testInvalidMaxPersistedEvents { XCTAssertEqual(1, self.config.maxPersistedEvents); } +- (void)testValidMaxPersistedSessions { + self.config.maxPersistedSessions = 1; + XCTAssertEqual(1, self.config.maxPersistedSessions); + self.config.maxPersistedSessions = 100; + XCTAssertEqual(100, self.config.maxPersistedSessions); + self.config.maxPersistedSessions = 40; + XCTAssertEqual(40, self.config.maxPersistedSessions); +} + +- (void)testInvalidMaxPersistedSessions { + self.config.maxPersistedSessions = 1; + self.config.maxPersistedSessions = 0; + XCTAssertEqual(1, self.config.maxPersistedSessions); + self.config.maxPersistedSessions = -1; + XCTAssertEqual(1, self.config.maxPersistedSessions); + self.config.maxPersistedSessions = 590; + XCTAssertEqual(1, self.config.maxPersistedSessions); +} + - (void)testValidMaxBreadcrumbs { self.config.maxBreadcrumbs = 0; XCTAssertEqual(0, self.config.maxBreadcrumbs); From 07ff7c780bb5b76766080d6a7fba92530fec1b0c Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 16 Dec 2020 15:15:48 +0000 Subject: [PATCH 11/33] Add E2E smoke tests --- features/barebone_tests.feature | 177 ++++++++++++++++++ .../iOSTestApp.xcodeproj/project.pbxproj | 4 + .../macOSTestApp.xcodeproj/project.pbxproj | 4 + .../scenarios/BareboneTestScenarios.swift | 109 +++++++++++ 4 files changed, 294 insertions(+) create mode 100644 features/barebone_tests.feature create mode 100644 features/fixtures/shared/scenarios/BareboneTestScenarios.swift diff --git a/features/barebone_tests.feature b/features/barebone_tests.feature new file mode 100644 index 000000000..4ea9b9a62 --- /dev/null +++ b/features/barebone_tests.feature @@ -0,0 +1,177 @@ +Feature: Barebone tests + + Background: + Given I clear all persistent data + + Scenario: Barebone test: handled errors + When I run "BareboneTestHandledScenario" + And I wait to receive 3 requests + + Then the request is valid for the session reporting API version "1.0" for the "iOS Bugsnag Notifier" notifier + And the payload field "sessions.0.id" is not null + And the session "user.id" equals "foobar" + And the session "user.email" equals "foobar@example.com" + And the session "user.name" equals "Foo Bar" + + And I discard the oldest request + + Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier + And the event "app.bundleVersion" equals "12301" + And the event "app.id" equals "com.bugsnag.iOSTestApp" + And the event "app.inForeground" is true + And the event "app.releaseStage" equals "development" + And the event "app.type" equals "iOS" + And the event "app.version" equals "12.3" + And the event "breadcrumbs.0.name" equals "Running BareboneTestHandledScenario" + And the event "breadcrumbs.1.name" equals "This is super " + And the event "device.id" is not null + And the event "device.jailbroken" is false + And the event "device.locale" is not null + And the event "device.manufacturer" equals "Apple" + And the event "device.modelNumber" is not null + And the event "device.osName" equals "iOS" + And the event "device.osVersion" matches "\d+\.\d+" + And the event "device.runtimeVersions.clangVersion" is not null + And the event "device.runtimeVersions.osBuild" is not null + And the event "device.time" is a timestamp + And the event "metaData.device.batteryLevel" is not null + And the event "metaData.device.charging" is not null + And the event "metaData.device.orientation" is not null + And the event "metaData.device.simulator" is false + And the event "metaData.device.timezone" is not null + And the event "metaData.device.wordSize" is not null + And the event "metaData.Exception.info" equals "Some error specific information" + And the event "metaData.Flags.Testing" is true + And the event "metaData.Other.password" equals "[REDACTED]" + And the event "severity" equals "warning" + And the event "severityReason.type" equals "handledException" + And the event "severityReason.unhandledOverridden" is true + And the event "unhandled" is true + And the event "user.email" equals "foobar@example.com" + And the event "user.id" equals "foobar" + And the event "user.name" equals "Foo Bar" + And the exception "errorClass" equals "NSRangeException" + And the exception "message" equals "-[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]" + And the exception "type" equals "cocoa" + And the payload field "events.0.app.dsymUUIDs" is a non-empty array + And the payload field "events.0.app.duration" is a number + And the payload field "events.0.app.durationInForeground" is a number + And the payload field "events.0.device.freeDisk" is an integer + And the payload field "events.0.device.freeMemory" is an integer + And the payload field "events.0.device.model" matches the test device model + And the payload field "events.0.device.totalMemory" is an integer + + And I discard the oldest request + + Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier + And the event "breadcrumbs.2.name" equals "NSRangeException" + And the event "breadcrumbs.2.type" equals "error" + And the event "breadcrumbs.3.name" equals "About to decode a payload..." + And the event "metaData.nserror.code" equals 4864 + And the event "metaData.nserror.domain" equals "NSCocoaErrorDomain" + And the event "metaData.nserror.reason" equals "The data isn’t in the correct format." + And the event "severity" equals "warning" + And the event "severityReason.type" equals "handledError" + And the event "severityReason.unhandledOverridden" is null + And the event "unhandled" is false + And the exception "errorClass" equals "__SwiftNativeNSError" + And the exception "message" equals "The data couldn’t be read because it isn’t in the correct format." + And the exception "type" equals "cocoa" + + Scenario: Smoke test: unhandled error + When I run "BareboneTestUnhandledErrorScenario" and relaunch the app + And I set the app to "report" mode + And I configure Bugsnag for "BareboneTestUnhandledErrorScenario" + And I wait to receive a request + + Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier + And the event "app.bundleVersion" equals "12301" + And the event "app.inForeground" is true + And the event "app.releaseStage" equals "development" + And the event "app.type" equals "iOS" + And the event "app.version" equals "12.3" + And the event "breadcrumbs.0.name" equals "Bugsnag loaded" + And the event "breadcrumbs.1.name" is null + And the event "device.id" is not null + And the event "device.jailbroken" is false + And the event "device.locale" is not null + And the event "device.manufacturer" equals "Apple" + And the event "device.modelNumber" is not null + And the event "device.osName" equals "iOS" + And the event "device.osVersion" matches "\d+\.\d+" + And the event "device.runtimeVersions.clangVersion" is not null + And the event "device.runtimeVersions.osBuild" is not null + And the event "device.time" is a timestamp + And the event "metaData.error.mach.code_name" equals "KERN_INVALID_ADDRESS" + And the event "metaData.error.mach.code" equals "0x1" + And the event "metaData.error.mach.exception_name" equals "EXC_BREAKPOINT" + And the event "severity" equals "error" + And the event "severityReason.type" equals "unhandledException" + And the event "severityReason.unhandledOverridden" is null + And the event "unhandled" is true + And the event "user.email" equals "barfoo@example.com" + And the event "user.id" equals "barfoo" + And the event "user.name" equals "Bar Foo" + And the exception "errorClass" equals "Fatal error" + # This can be uncommented once Swift fatal error message reporting is fixed. + # And the exception "message" equals "iOSTestApp/BareboneTestScenarios.swift | Unexpectedly found nil while implicitly unwrapping an Optional value" + And the exception "type" equals "cocoa" + And the payload field "events.0.app.dsymUUIDs" is a non-empty array + And the payload field "events.0.app.duration" is a number + And the payload field "events.0.app.durationInForeground" is a number + And the payload field "events.0.device.freeDisk" is an integer + And the payload field "events.0.device.freeMemory" is an integer + And the payload field "events.0.device.model" matches the test device model + And the payload field "events.0.device.totalMemory" is an integer + + Scenario: Barebone test: Out Of Memory + When I run "OOMLoadScenario" + And I wait to receive a request + + Then the "Bugsnag-API-Key" header equals "0192837465afbecd0192837465afbecd" + And the event "unhandled" is false + And the exception "message" equals "OOMLoadScenario" + And the event has a "manual" breadcrumb named "OOMLoadScenarioBreadcrumb" + And I discard the oldest request + + When I relaunch the app + And I configure Bugsnag for "OOMLoadScenario" + And I wait to receive a request + + Then the "Bugsnag-API-Key" header equals "0192837465afbecd0192837465afbecd" + And the error is an OOM event + And the event "app.bundleVersion" is not null + And the event "app.dsymUUIDs" is not null + And the event "app.id" equals "com.bugsnag.iOSTestApp" + And the event "app.inForeground" is true + And the event "app.type" equals "iOS" + And the event "app.version" is not null + And the event "device.id" is not null + And the event "device.jailbroken" is false + And the event "device.locale" is not null + And the event "device.manufacturer" equals "Apple" + And the event "device.modelNumber" is not null + And the event "device.osName" equals "iOS" + And the event "device.osVersion" matches "\d+\.\d+" + And the event "device.runtimeVersions.clangVersion" is not null + And the event "device.runtimeVersions.osBuild" is not null + And the event "device.time" is null + And the event "device.totalMemory" is not null + And the event "metaData.app.name" equals "iOSTestApp" + And the event "metaData.custom.bar" equals "foo" + And the event "metaData.device.batteryLevel" is not null + And the event "metaData.device.charging" is not null + And the event "metaData.device.orientation" is not null + And the event "metaData.device.simulator" is false + And the event "metaData.device.timezone" is not null + And the event "metaData.device.wordSize" is not null + And the event "user.email" equals "foobar@example.com" + And the event "user.id" equals "foobar" + And the event "user.name" equals "Foo Bar" + And the payload field "events.0.app.dsymUUIDs" is a non-empty array + And the payload field "events.0.app.duration" is null + And the payload field "events.0.app.durationInForeground" is null + And the payload field "events.0.device.freeDisk" is null + And the payload field "events.0.device.freeMemory" is null + And the payload field "events.0.device.model" matches the test device model + And the payload field "events.0.device.totalMemory" is an integer diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj index bff7d2ffc..5b651bb6f 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj @@ -17,6 +17,7 @@ 00507A64242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00507A63242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift */; }; 00CEB60D24080C690004793D /* EnabledErrorTypesScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */; }; 0163BFA72583B3CF008DC28B /* DiscardClassesScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */; }; + 01AF6A53258A112F00FFC803 /* BareboneTestScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */; }; 6526A0D4248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */; }; 8A14F0F62282D4AE00337B05 /* (null) in Sources */ = {isa = PBXBuildFile; }; 8A32DB8222424E3000EDD92F /* NSExceptionShiftScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A32DB8122424E3000EDD92F /* NSExceptionShiftScenario.m */; }; @@ -161,6 +162,7 @@ 00CEB60B24080C690004793D /* EnabledErrorTypesScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnabledErrorTypesScenario.h; sourceTree = ""; }; 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnabledErrorTypesScenario.m; sourceTree = ""; }; 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardClassesScenarios.swift; sourceTree = ""; }; + 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BareboneTestScenarios.swift; sourceTree = ""; }; 4994F05E0421A0B037DD2CC5 /* Pods_iOSTestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSTestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; 8A32DB8022424E3000EDD92F /* NSExceptionShiftScenario.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSExceptionShiftScenario.h; sourceTree = ""; }; @@ -551,6 +553,7 @@ F49695A3243EF7B600105DA9 /* OOMs */, F49695AE2445476700105DA9 /* Plugin */, 0037410E2473CF2300BE41AA /* AppAndDeviceAttributesScenario.swift */, + 01AF6A52258A112F00FFC803 /* BareboneTestScenarios.swift */, 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */, 8AB1081823301FE600672818 /* ReleaseStageScenarios.swift */, F4295ABA693D273D52AA9F6B /* Scenario.h */, @@ -884,6 +887,7 @@ E700EE69247D73F8008CFFB6 /* UnhandledMachExceptionScenario.m in Sources */, E75040B12478214F005D33BD /* MetadataRedactionRegexScenario.swift in Sources */, E700EE5B247D3224008CFFB6 /* OriginalErrorNSExceptionScenario.swift in Sources */, + 01AF6A53258A112F00FFC803 /* BareboneTestScenarios.swift in Sources */, 8AEFC73120F8D1A000A78779 /* AutoSessionWithUserScenario.m in Sources */, 8AB1081923301FE600672818 /* ReleaseStageScenarios.swift in Sources */, 8AF6FD7A225E3FA00056EF9E /* ResumeSessionOOMScenario.m in Sources */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index ef03089f9..1cfdb6e8b 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -16,6 +16,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 */; }; + 01AF6A50258A00DE00FFC803 /* BareboneTestScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A4F258A00DE00FFC803 /* BareboneTestScenarios.swift */; }; 01F47CC4254B1B3100B184AD /* OriginalErrorNSExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C21254B1B2C00B184AD /* OriginalErrorNSExceptionScenario.swift */; }; 01F47CC5254B1B3100B184AD /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C23254B1B2C00B184AD /* LoadConfigFromFileAutoScenario.swift */; }; 01F47CC6254B1B3100B184AD /* HandledExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C28254B1B2C00B184AD /* HandledExceptionScenario.swift */; }; @@ -158,6 +159,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 = ""; }; + 01AF6A4F258A00DE00FFC803 /* BareboneTestScenarios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BareboneTestScenarios.swift; sourceTree = ""; }; 01F47C21254B1B2C00B184AD /* OriginalErrorNSExceptionScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OriginalErrorNSExceptionScenario.swift; sourceTree = ""; }; 01F47C22254B1B2C00B184AD /* ThreadScenarios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadScenarios.h; sourceTree = ""; }; 01F47C23254B1B2C00B184AD /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; @@ -367,6 +369,7 @@ 01F47CB8254B1B3000B184AD /* AutoSessionUnhandledScenario.m */, 01F47C2E254B1B2D00B184AD /* AutoSessionWithUserScenario.h */, 01F47C98254B1B2F00B184AD /* AutoSessionWithUserScenario.m */, + 01AF6A4F258A00DE00FFC803 /* BareboneTestScenarios.swift */, 01F47C2F254B1B2D00B184AD /* BreadcrumbCallbackCrashScenario.swift */, 01F47C79254B1B2F00B184AD /* BreadcrumbCallbackDiscardScenario.swift */, 01F47C7A254B1B2F00B184AD /* BreadcrumbCallbackOrderScenario.swift */, @@ -643,6 +646,7 @@ 01F47D15254B1B3100B184AD /* UndefinedInstructionScenario.m in Sources */, 01F47D1E254B1B3100B184AD /* SIGPIPEScenario.m in Sources */, 01F47CCF254B1B3100B184AD /* ObjCExceptionScenario.m in Sources */, + 01AF6A50258A00DE00FFC803 /* BareboneTestScenarios.swift in Sources */, 01F47CED254B1B3100B184AD /* ResumedSessionScenario.swift in Sources */, 01F47CE8254B1B3100B184AD /* AppAndDeviceAttributesScenario.swift in Sources */, 01F47D1A254B1B3100B184AD /* ThreadScenarios.m in Sources */, diff --git a/features/fixtures/shared/scenarios/BareboneTestScenarios.swift b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift new file mode 100644 index 000000000..533ddfb99 --- /dev/null +++ b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift @@ -0,0 +1,109 @@ +// +// BareboneTestScenarios.swift +// macOSTestApp +// +// Created by Nick Dowell on 16/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +class BareboneTestHandledScenario: Scenario { + + var onBreadcrumbCount = 0 + var onSendErrorCount = 0 + var onSessionCount = 0 + + override func startBugsnag() { + config.addOnBreadcrumb { + NSLog("OnBreadcrumb: \"\($0.message)\"") + self.onBreadcrumbCount += 1 + if $0.message.contains("secret") { + $0.message = $0.message.replacingOccurrences(of: "secret", with: "") + } + return true + } + config.addOnSendError { + NSLog("OnSendError: \"\($0.errors[0].errorClass ?? "")\" \"\($0.errors[0].errorMessage ?? "")\"") + self.onSendErrorCount += 1 + return true + } + config.addOnSession { + NSLog("OnSession: \($0.id) started at \($0.startedAt)") + self.onSessionCount += 1 + return true + } + config.enabledBreadcrumbTypes = [.error] + config.addMetadata(["Testing": true], section: "Flags") + config.addMetadata(["password": "123456"], section: "Other") + config.sendThreads = .unhandledOnly + config.setUser("foobar", withEmail: "foobar@example.com", andName: "Foo Bar") + config.appVersion = "12.3" + config.bundleVersion = "12301" + super.startBugsnag() + } + + override func run() { + precondition(onSessionCount == 1) + + Bugsnag.leaveBreadcrumb(withMessage: "Running BareboneTestHandledScenario") + + precondition(onBreadcrumbCount == 1) + + Bugsnag.notify(NSException(name: .genericException, reason: nil)) { _ in + return false + } + + Bugsnag.leaveBreadcrumb(withMessage: "This is super secret") + + Bugsnag.notify(NSException(name: .rangeException, reason: "-[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]")) { + $0.addMetadata(["info": "Some error specific information"], section: "Exception") + $0.unhandled = true + return true + } + + // There is a delay between notify() and an error being sent. + RunLoop.current.run(until: .init(timeIntervalSinceNow: 2)) + precondition(onSendErrorCount == 1) + + Bugsnag.leaveBreadcrumb(withMessage: "About to decode a payload...") + + do { + _ = try JSONDecoder().decode(Payload.self, from: Data()) + } catch { + Bugsnag.notifyError(error) + } + } +} + +// MARK: - + +class BareboneTestUnhandledErrorScenario: Scenario { + + private var payload: Payload! + + override func startBugsnag() { + config.autoTrackSessions = false + if eventMode == "report" { + // The version of the app at report time. + config.appVersion = "23.4" + config.bundleVersion = "23401" + } else { + // The version of the app at crash time. + config.appVersion = "12.3" + config.bundleVersion = "12301" + } + super.startBugsnag() + } + + override func run() { + Bugsnag.setUser("barfoo", withEmail: "barfoo@example.com", andName: "Bar Foo") + + // Triggers "Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: ..." + print(payload.name) + } +} + +// MARK: - + +private struct Payload: Decodable { + let name: String +} From 1f23633b90c1b188c006926c6f8a6cdfedc25647 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 08:28:57 +0000 Subject: [PATCH 12/33] Add crash_info_message to KSCrashReport --- Bugsnag.xcodeproj/project.pbxproj | 20 ++-- .../KSCrash/Recording/BSG_KSCrashReport.c | 25 +++++ .../Recording/BSG_KSCrashReportFields.h | 1 + ...SG_KSMachHeaders.m => BSG_KSMachHeaders.c} | 95 +++++++++++++++++-- .../Recording/Tools/BSG_KSMachHeaders.h | 7 ++ 5 files changed, 132 insertions(+), 16 deletions(-) rename Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/{BSG_KSMachHeaders.m => BSG_KSMachHeaders.c} (72%) diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index ca53af7ff..e0166ef4f 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -423,9 +423,9 @@ 008969812486DAD100DC48C2 /* BSG_KSObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = 0089690F2486DAD000DC48C2 /* BSG_KSObjC.h */; }; 008969822486DAD100DC48C2 /* BSG_KSObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = 0089690F2486DAD000DC48C2 /* BSG_KSObjC.h */; }; 008969832486DAD100DC48C2 /* BSG_KSObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = 0089690F2486DAD000DC48C2 /* BSG_KSObjC.h */; }; - 008969842486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */; }; - 008969852486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */; }; - 008969862486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */; }; + 008969842486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */; }; + 008969852486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */; }; + 008969862486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */; }; 008969872486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */ = {isa = PBXBuildFile; fileRef = 008969112486DAD000DC48C2 /* BSG_KSMachApple.h */; }; 008969882486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */ = {isa = PBXBuildFile; fileRef = 008969112486DAD000DC48C2 /* BSG_KSMachApple.h */; }; 008969892486DAD100DC48C2 /* BSG_KSMachApple.h in Headers */ = {isa = PBXBuildFile; fileRef = 008969112486DAD000DC48C2 /* BSG_KSMachApple.h */; }; @@ -804,7 +804,7 @@ E701FAB12490EFE8008D842F /* ConfigurationApiValidationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E701FAAE2490EFE8008D842F /* ConfigurationApiValidationTest.m */; }; E74628FC248907C100F92D67 /* BSG_KSCrashDoctor.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969002486DAD000DC48C2 /* BSG_KSCrashDoctor.m */; }; E74628FD248907C100F92D67 /* BSG_KSJSONCodecObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969082486DAD000DC48C2 /* BSG_KSJSONCodecObjC.m */; }; - E74628FF248907C100F92D67 /* BSG_KSMachHeaders.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */; }; + E74628FF248907C100F92D67 /* BSG_KSMachHeaders.c in Sources */ = {isa = PBXBuildFile; fileRef = 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */; }; E7462900248907C100F92D67 /* NSError+BSG_SimpleConstructor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0089691E2486DAD000DC48C2 /* NSError+BSG_SimpleConstructor.m */; }; E7462901248907C100F92D67 /* BSG_KSLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969262486DAD000DC48C2 /* BSG_KSLogger.m */; }; E7462902248907C100F92D67 /* BSG_KSCrashState.m in Sources */ = {isa = PBXBuildFile; fileRef = 008969292486DAD000DC48C2 /* BSG_KSCrashState.m */; }; @@ -1172,7 +1172,7 @@ 0089690C2486DAD000DC48C2 /* NSError+BSG_SimpleConstructor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+BSG_SimpleConstructor.h"; sourceTree = ""; }; 0089690E2486DAD000DC48C2 /* BSG_KSArchSpecific.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSG_KSArchSpecific.h; sourceTree = ""; }; 0089690F2486DAD000DC48C2 /* BSG_KSObjC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSG_KSObjC.h; sourceTree = ""; }; - 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSG_KSMachHeaders.m; sourceTree = ""; }; + 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = BSG_KSMachHeaders.c; sourceTree = ""; }; 008969112486DAD000DC48C2 /* BSG_KSMachApple.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSG_KSMachApple.h; sourceTree = ""; }; 008969122486DAD000DC48C2 /* BSG_KSString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSG_KSString.h; sourceTree = ""; }; 008969132486DAD000DC48C2 /* BSG_KSMach.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = BSG_KSMach.c; sourceTree = ""; }; @@ -1553,7 +1553,7 @@ 008969282486DAD000DC48C2 /* BSG_KSMach.h */, 008969112486DAD000DC48C2 /* BSG_KSMachApple.h */, 008969232486DAD000DC48C2 /* BSG_KSMachHeaders.h */, - 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.m */, + 008969102486DAD000DC48C2 /* BSG_KSMachHeaders.c */, 008969252486DAD000DC48C2 /* BSG_KSObjC.c */, 0089690F2486DAD000DC48C2 /* BSG_KSObjC.h */, 008969202486DAD000DC48C2 /* BSG_KSObjCApple.h */, @@ -2475,7 +2475,7 @@ 008969992486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967E82486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 008968722486DA9500DC48C2 /* BugsnagDevice.m in Sources */, - 008969842486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */, + 008969842486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */, 00896A322486DAD100DC48C2 /* BSG_KSCrashC.c in Sources */, 008969902486DAD100DC48C2 /* BSG_RFC3339DateTool.m in Sources */, 008968AB2486DA9600DC48C2 /* BugsnagStateEvent.m in Sources */, @@ -2638,7 +2638,7 @@ 0089699A2486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967E92486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 008968732486DA9500DC48C2 /* BugsnagDevice.m in Sources */, - 008969852486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */, + 008969852486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */, 00896A332486DAD100DC48C2 /* BSG_KSCrashC.c in Sources */, 008969912486DAD100DC48C2 /* BSG_RFC3339DateTool.m in Sources */, 008968AC2486DA9600DC48C2 /* BugsnagStateEvent.m in Sources */, @@ -2800,7 +2800,7 @@ 0089699B2486DAD100DC48C2 /* BSG_KSMach_Arm64.c in Sources */, 008967EA2486DA2D00DC48C2 /* BugsnagErrorTypes.m in Sources */, 008968742486DA9500DC48C2 /* BugsnagDevice.m in Sources */, - 008969862486DAD100DC48C2 /* BSG_KSMachHeaders.m in Sources */, + 008969862486DAD100DC48C2 /* BSG_KSMachHeaders.c in Sources */, 00896A342486DAD100DC48C2 /* BSG_KSCrashC.c in Sources */, 008969922486DAD100DC48C2 /* BSG_RFC3339DateTool.m in Sources */, 008968AD2486DA9600DC48C2 /* BugsnagStateEvent.m in Sources */, @@ -2981,7 +2981,7 @@ E746291C248907E500F92D67 /* BSG_KSCrashC.c in Sources */, E74628FC248907C100F92D67 /* BSG_KSCrashDoctor.m in Sources */, E74628FD248907C100F92D67 /* BSG_KSJSONCodecObjC.m in Sources */, - E74628FF248907C100F92D67 /* BSG_KSMachHeaders.m in Sources */, + E74628FF248907C100F92D67 /* BSG_KSMachHeaders.c in Sources */, E7462900248907C100F92D67 /* NSError+BSG_SimpleConstructor.m in Sources */, E7462901248907C100F92D67 /* BSG_KSLogger.m in Sources */, E7462902248907C100F92D67 /* BSG_KSCrashState.m in Sources */, diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReport.c b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReport.c index 20207a14a..59d728d19 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReport.c +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReport.c @@ -965,6 +965,27 @@ void bsg_kscrw_i_writeNotableAddresses( writer->endContainer(writer); } +/** Write the message from the `__crash_info` Mach section into the report. + * + * @param writer The writer. + * + * @param key The object key. + * + * @param address The address of the first frame in the backtrace. + */ +void bsg_kscrw_i_writeCrashInfoMessage(const BSG_KSCrashReportWriter *const writer, + const char *key, uintptr_t address) { + BSG_Mach_Header_Info *image = bsg_mach_headers_image_at_address(address); + if (!image) { + BSG_KSLOG_ERROR("Could not locate mach header info"); + return; + } + const char *message = bsg_mach_headers_get_crash_info_message(image); + if (message) { + writer->addStringElement(writer, key, message); + } +} + /** Write information about a thread to the report. * * @param writer The writer. @@ -1021,6 +1042,10 @@ void bsg_kscrw_i_writeThread(const BSG_KSCrashReportWriter *const writer, writer, BSG_KSCrashField_NotableAddresses, machineContext); } } + if (isCrashedThread && backtrace && backtraceLength) { + bsg_kscrw_i_writeCrashInfoMessage(writer, BSG_KSCrashField_CrashInfoMessage, + backtrace[0]); + } } writer->endContainer(writer); } diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReportFields.h b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReportFields.h index 475c7e005..44aa19d5f 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReportFields.h +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrashReportFields.h @@ -95,6 +95,7 @@ #define BSG_KSCrashField_Backtrace "backtrace" #define BSG_KSCrashField_Basic "basic" #define BSG_KSCrashField_Crashed "crashed" +#define BSG_KSCrashField_CrashInfoMessage "crash_info_message" #define BSG_KSCrashField_CurrentThread "current_thread" #define BSG_KSCrashField_DispatchQueue "dispatch_queue" #define BSG_KSCrashField_NotableAddresses "notable_addresses" diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.m b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.c similarity index 72% rename from Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.m rename to Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.c index 277ff55dd..4b3049609 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.m +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.c @@ -1,16 +1,36 @@ // -// BSG_KSMachHeaders.m +// BSG_KSMachHeaders.c // Bugsnag // // Created by Robin Macharg on 04/05/2020. // Copyright © 2020 Bugsnag. All rights reserved. // -#import -#import -#import -#import "BSG_KSDynamicLinker.h" -#import "BSG_KSMachHeaders.h" +#include "BSG_KSMachHeaders.h" + +#include "BSG_KSDynamicLinker.h" +#include "BSG_KSMach.h" + +#include +#include +#include +#include + +// Copied from https://github.com/apple/swift/blob/swift-5.0-RELEASE/include/swift/Runtime/Debug.h#L28-L40 + +#define CRASHREPORTER_ANNOTATIONS_VERSION 5 +#define CRASHREPORTER_ANNOTATIONS_SECTION "__crash_info" + +struct crashreporter_annotations_t { + uint64_t version; // unsigned long + uint64_t message; // char * + uint64_t signature_string; // char * + uint64_t backtrace; // char * + uint64_t message2; // char * + uint64_t thread; // uint64_t + uint64_t dialog_mode; // unsigned int + uint64_t abort_cause; // unsigned int +}; // MARK: - Mach Header Linked List @@ -255,3 +275,66 @@ uintptr_t bsg_mach_headers_image_at_base_of_image_index(const struct mach_header return 0; } +static uintptr_t bsg_mach_header_info_get_section_addr_named(const BSG_Mach_Header_Info *header, const char *name) { + uintptr_t cmdPtr = bsg_mach_headers_first_cmd_after_header(header->header); + if (!cmdPtr) { + return 0; + } + for (uint32_t i = 0; i < header->header->ncmds; i++) { + const struct load_command *loadCmd = (struct load_command *)cmdPtr; + if (loadCmd->cmd == LC_SEGMENT) { + const struct segment_command *segment = (void *)cmdPtr; + char *sectionPtr = (void *)(cmdPtr + sizeof(*segment)); + for (uint32_t i = 0; i < segment->nsects; i++) { + struct section *section = (void *)sectionPtr; + if (strcmp(name, section->sectname) == 0) { + return section->addr + header->slide; + } + sectionPtr += sizeof(*section); + } + } else if (loadCmd->cmd == LC_SEGMENT_64) { + const struct segment_command_64 *segment = (void *)cmdPtr; + char *sectionPtr = (void *)(cmdPtr + sizeof(*segment)); + for (uint32_t i = 0; i < segment->nsects; i++) { + struct section_64 *section = (void *)sectionPtr; + if (strcmp(name, section->sectname) == 0) { + return (uintptr_t)section->addr + header->slide; + } + sectionPtr += sizeof(*section); + } + } + cmdPtr += loadCmd->cmdsize; + } + return 0; +} + +const char *bsg_mach_headers_get_crash_info_message(const BSG_Mach_Header_Info *header) { + struct crashreporter_annotations_t info; + uintptr_t sectionAddress = bsg_mach_header_info_get_section_addr_named(header, CRASHREPORTER_ANNOTATIONS_SECTION); + if (!sectionAddress) { + return NULL; + } + if (bsg_ksmachcopyMem((void *)sectionAddress, &info, sizeof(info)) != KERN_SUCCESS) { + return NULL; + } + // Version 4 was in use until iOS 9 / Swift 2.0 when the version was bumped to 5. + if (info.version > CRASHREPORTER_ANNOTATIONS_VERSION) { + return NULL; + } + if (!info.message) { + return NULL; + } + // Probe the string to ensure it's safe to read. + for (uintptr_t i = 0; i < 500; i++) { + char c; + if (bsg_ksmachcopyMem((void *)(info.message + i), &c, sizeof(c)) != KERN_SUCCESS) { + // String is not readable. + return NULL; + } + if (c == '\0') { + // Found end of string. + return (const char *)info.message; + } + } + return NULL; +} diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.h b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.h index 38f22f985..376aad0e0 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.h +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSMachHeaders.h @@ -99,4 +99,11 @@ uintptr_t bsg_mach_headers_first_cmd_after_header(const struct mach_header *head */ uintptr_t bsg_mach_headers_image_at_base_of_image_index(const struct mach_header *header); +/** Get the __crash_info message of the specified image. + * + * @param header The header to get commands for. + * @return The __crash_info message, or NULL if no readable message could be found. + */ +const char *bsg_mach_headers_get_crash_info_message(const BSG_Mach_Header_Info *header); + #endif /* BSG_KSMachHeaders_h */ From d37e7e3d4047395d0788866ad82a889b2b321b1e Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 13:28:02 +0000 Subject: [PATCH 13/33] Parse crash info message => errorClass & message --- Bugsnag/Payload/BugsnagError+Private.h | 3 + Bugsnag/Payload/BugsnagError.m | 43 +++-- Bugsnag/Payload/BugsnagEvent.m | 10 +- Bugsnag/Payload/BugsnagThread+Private.h | 2 + Bugsnag/Payload/BugsnagThread.m | 15 +- Tests/BugsnagErrorTest.m | 132 +++++++++----- Tests/BugsnagEventTests.m | 227 ------------------------ 7 files changed, 137 insertions(+), 295 deletions(-) diff --git a/Bugsnag/Payload/BugsnagError+Private.h b/Bugsnag/Payload/BugsnagError+Private.h index 395fc45e7..99308add9 100644 --- a/Bugsnag/Payload/BugsnagError+Private.h +++ b/Bugsnag/Payload/BugsnagError+Private.h @@ -23,6 +23,9 @@ NS_ASSUME_NONNULL_BEGIN + (BugsnagError *)errorFromJson:(NSDictionary *)json; +/// Parses the `__crash_info` message and updates the `errorClass` and `errorMessage` as appropriate. +- (void)updateWithCrashInfoMessage:(NSString *)crashInfoMessage; + - (NSDictionary *)toDictionary; @end diff --git a/Bugsnag/Payload/BugsnagError.m b/Bugsnag/Payload/BugsnagError.m index 960113a27..2577e9a55 100644 --- a/Bugsnag/Payload/BugsnagError.m +++ b/Bugsnag/Payload/BugsnagError.m @@ -6,14 +6,16 @@ // Copyright © 2020 Bugsnag. All rights reserved. // -#import "BugsnagError.h" +#import "BugsnagError+Private.h" +#import "BSG_KSCrashReportFields.h" +#import "BugsnagCollections.h" #import "BugsnagKeys.h" +#import "BugsnagLogger.h" #import "BugsnagStackframe+Private.h" #import "BugsnagStacktrace.h" -#import "BugsnagCollections.h" -#import "RegisterErrorData.h" -#import "BugsnagThread.h" +#import "BugsnagThread+Private.h" + NSString *_Nonnull BSGSerializeErrorType(BSGErrorType errorType) { switch (errorType) { @@ -87,12 +89,6 @@ - (instancetype)initWithEvent:(NSDictionary *)event errorReportingThread:(Bugsna _type = BSGErrorTypeCocoa; if (![[event valueForKeyPath:@"user.state.didOOM"] boolValue]) { - NSArray *threadDict = [event valueForKeyPath:@"crash.threads"]; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:threadDict]; - if (data) { - _errorClass = data.errorClass; - _errorMessage = data.errorMessage; - } _stacktrace = thread.stacktrace; } } @@ -132,6 +128,33 @@ + (BugsnagError *)errorFromJson:(NSDictionary *)json { return error; } +- (void)updateWithCrashInfoMessage:(NSString *)crashInfoMessage { + @try { + // Messages that match this pattern should override the errorClass (and errorMessage if there is enough information.) + NSString *pattern = @"^(Assertion failed|Fatal error|Precondition failed): ((.+): )?file .+, line \\d+\n$"; + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:nil]; + NSArray *matches = [regex matchesInString:crashInfoMessage options:0 range:NSMakeRange(0, crashInfoMessage.length)]; + if (matches.count != 1 || matches[0].numberOfRanges != 4) { + if (!self.errorMessage.length) { + // It's better to fall back to the raw string than have an empty errorMessage. + self.errorMessage = crashInfoMessage; + } + return; + } + NSRange errorClassRange = [matches[0] rangeAtIndex:1]; + if (errorClassRange.location != NSNotFound) { + self.errorClass = [crashInfoMessage substringWithRange:errorClassRange]; + } + NSRange errorMessageRange = [matches[0] rangeAtIndex:3]; + if (errorMessageRange.location != NSNotFound) { + self.errorMessage = [crashInfoMessage substringWithRange:errorMessageRange]; + } + } @catch (NSException *exception) { + bsg_log_err(@"Exception thrown while parsing crash info message: %@", exception); + return; + } +} + - (NSDictionary *)findErrorReportingThread:(NSDictionary *)event { NSArray *threads = [event valueForKeyPath:@"crash.threads"]; diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 7c5d5984e..7237a8b4e 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -8,6 +8,8 @@ #import "BugsnagPlatformConditional.h" +#import "BugsnagEvent+Private.h" + #if BSG_PLATFORM_IOS #import "BSGUIKit.h" #include @@ -27,7 +29,6 @@ #import "BugsnagConfiguration+Private.h" #import "BugsnagDeviceWithState+Private.h" #import "BugsnagError+Private.h" -#import "BugsnagEvent+Private.h" #import "BugsnagHandledState.h" #import "BugsnagKeys.h" #import "BugsnagMetadata+Private.h" @@ -267,7 +268,7 @@ - (instancetype)initWithOOMData:(NSDictionary *)event { * @return a BugsnagEvent containing the parsed information */ - (instancetype)initWithKSCrashData:(NSDictionary *)event { - NSDictionary *error = [event valueForKeyPath:@"crash.error"]; + NSMutableDictionary *error = [[event valueForKeyPath:@"crash.error"] mutableCopy]; NSString *errorType = error[BSGKeyType]; // Always assume that a report coming from KSCrash is by default an unhandled error. @@ -333,6 +334,11 @@ - (instancetype)initWithKSCrashData:(NSDictionary *)event { NSArray *errors = @[[[BugsnagError alloc] initWithEvent:event errorReportingThread:errorReportingThread]]; + if (errorReportingThread.crashInfoMessage) { + [errors[0] updateWithCrashInfoMessage:errorReportingThread.crashInfoMessage]; + error[@"crashInfo"] = errorReportingThread.crashInfoMessage; + } + BugsnagHandledState *handledState; if (recordedState) { handledState = [[BugsnagHandledState alloc] initWithDictionary:recordedState]; diff --git a/Bugsnag/Payload/BugsnagThread+Private.h b/Bugsnag/Payload/BugsnagThread+Private.h index 1caf2459d..3616e22c0 100644 --- a/Bugsnag/Payload/BugsnagThread+Private.h +++ b/Bugsnag/Payload/BugsnagThread+Private.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)threadFromJson:(NSDictionary *)json; +@property (readonly) NSString *crashInfoMessage; + + (NSDictionary *)enhanceThreadInfo:(NSDictionary *)thread depth:(NSUInteger)depth errorType:(nullable NSString *)errorType; diff --git a/Bugsnag/Payload/BugsnagThread.m b/Bugsnag/Payload/BugsnagThread.m index cc2f73c08..fb6864399 100644 --- a/Bugsnag/Payload/BugsnagThread.m +++ b/Bugsnag/Payload/BugsnagThread.m @@ -6,8 +6,9 @@ // Copyright © 2020 Bugsnag. All rights reserved. // -#import "BugsnagThread.h" +#import "BugsnagThread+Private.h" +#import "BSG_KSCrashReportFields.h" #import "BugsnagCollections.h" #import "BugsnagStackframe+Private.h" #import "BugsnagStacktrace+Private.h" @@ -56,13 +57,13 @@ - (instancetype)initWithId:(NSString *)id - (instancetype)initWithThread:(NSDictionary *)thread binaryImages:(NSArray *)binaryImages { if (self = [super init]) { - _errorReportingThread = [thread[@"crashed"] boolValue]; - self.id = [thread[@"index"] stringValue]; - self.type = BSGThreadTypeCocoa; - - NSArray *backtrace = thread[@"backtrace"][@"contents"]; + _errorReportingThread = [thread[@(BSG_KSCrashField_Crashed)] boolValue]; + _id = [thread[@(BSG_KSCrashField_Index)] stringValue]; + _type = BSGThreadTypeCocoa; + _crashInfoMessage = [thread[@(BSG_KSCrashField_CrashInfoMessage)] copy]; + NSArray *backtrace = thread[@(BSG_KSCrashField_Backtrace)][@(BSG_KSCrashField_Contents)]; BugsnagStacktrace *frames = [[BugsnagStacktrace alloc] initWithTrace:backtrace binaryImages:binaryImages]; - self.stacktrace = frames.trace; + _stacktrace = [frames.trace copy]; } return self; } diff --git a/Tests/BugsnagErrorTest.m b/Tests/BugsnagErrorTest.m index 2edcf0602..939933ecf 100644 --- a/Tests/BugsnagErrorTest.m +++ b/Tests/BugsnagErrorTest.m @@ -24,10 +24,6 @@ @interface BugsnagErrorTest : XCTestCase @implementation BugsnagErrorTest - (void)setUp { - self.event = [self generateEvent:@{}]; -} - -- (NSDictionary *)generateEvent:(NSDictionary *)notableAddresses { NSDictionary *thread = @{ @"current_thread": @YES, @"crashed": @YES, @@ -43,8 +39,7 @@ - (NSDictionary *)generateEvent:(NSDictionary *)notableAddresses { @"object_addr": @4490747904 } ] - }, - @"notable_addresses": notableAddresses + } }; NSDictionary *binaryImage = @{ @"uuid": @"D0A41830-4FD2-3B02-A23B-0741AD4C7F52", @@ -53,7 +48,7 @@ - (NSDictionary *)generateEvent:(NSDictionary *)notableAddresses { @"image_size": @483328, @"name": @"/Users/joesmith/foo", }; - return @{ + self.event = @{ @"crash": @{ @"error": @{ @"type": @"user", @@ -112,48 +107,6 @@ - (BugsnagThread *)findErrorReportingThread:(NSDictionary *)event { return nil; } -/** - * If notable addresses are in the event, the error message/class should be enhanced - * with these values - */ -- (void)testMessageEnhancement { - self.event = [self generateEvent:@{ - @"x9": @{ - @"address": @4511086448, - @"type": @"string", - @"value": @"Something went wrong" - }, - @"r16": @{ - @"address": @4511089532, - @"type": @"string", - @"value": @"Fatal error" - } - }]; - BugsnagError *error = [[BugsnagError alloc] initWithEvent:self.event errorReportingThread:nil]; - NSDictionary *dict = [error toDictionary]; - XCTAssertEqualObjects(@"Fatal error", dict[@"errorClass"]); - XCTAssertEqualObjects(@"Something went wrong", dict[@"message"]); -} - -- (void) testEmptyErrorDataFromThreads { - self.event = [self generateEvent:@{ - @"x9": @{ - @"address": @4511086448, - @"type": @"string", - @"value": @"Something went wrong" - }, - @"r16": @{ - @"address": @4511089532, - @"type": @"string", - @"value": [NSNull null] - } - }]; - BugsnagError *error = [[BugsnagError alloc] initWithEvent:self.event errorReportingThread:nil]; - NSDictionary *dict = [error toDictionary]; - XCTAssertEqualObjects(@"Foo Exception", dict[@"errorClass"]); - XCTAssertEqualObjects(@"Foo overload", dict[@"message"]); -} - - (void)testErrorClassParse { XCTAssertEqualObjects(@"foo", BSGParseErrorClass(@{@"cpp_exception": @{@"name": @"foo"}}, @"cpp_exception")); XCTAssertEqualObjects(@"bar", BSGParseErrorClass(@{@"mach": @{@"exception_name": @"bar"}}, @"mach")); @@ -195,4 +148,85 @@ - (void)testStacktraceOverride { XCTAssertEqual(0, error.stacktrace.count); } +- (void)testUpdateWithCrashInfoMessage { + BugsnagError *error = [[BugsnagError alloc] initWithErrorClass:@"" errorMessage:@"" errorType:BSGErrorTypeCocoa stacktrace:nil]; + + // Swift fatal errors with a message. + // The errorClass and errorMessage should be overwritten with values extracted from the crash info message. + + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"Assertion failed: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Assertion failed"); + XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"Fatal error: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Fatal error"); + XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"Precondition failed: : strange formatting 😱::: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Precondition failed"); + XCTAssertEqualObjects(error.errorMessage, @" : strange formatting 😱::"); + + // Swift fatal errors without a message. + // The errorClass should be overwritten but the errorMessage left as-is. + + error.errorClass = nil; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@"Assertion failed: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Assertion failed"); + XCTAssertEqualObjects(error.errorMessage, @"Expected message"); + + error.errorClass = nil; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@"Fatal error: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Fatal error"); + XCTAssertEqualObjects(error.errorMessage, @"Expected message"); + + error.errorClass = nil; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@"Precondition failed: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Precondition failed"); + XCTAssertEqualObjects(error.errorMessage, @"Expected message"); + + // Non-matching crash info messages. + // The errorClass should not be overwritten, the errorMessage should be overwritten if it was previously empty / nil. + + error.errorClass = nil; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@"Assertion failed: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\njunk"]; + XCTAssertEqualObjects(error.errorClass, nil,); + XCTAssertEqualObjects(error.errorMessage, @"Expected message"); + + error.errorClass = @"Expected error class"; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@"BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread"]; + XCTAssertEqualObjects(error.errorClass, @"Expected error class"); + + error.errorClass = @"Expected error class"; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread"]; + XCTAssertEqualObjects(error.errorClass, @"Expected error class",); + XCTAssertEqualObjects(error.errorMessage, @"BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread"); + + error.errorClass = @"Expected error class"; + error.errorMessage = @"Expected message"; + [error updateWithCrashInfoMessage:@""]; + XCTAssertEqualObjects(error.errorClass, @"Expected error class"); + XCTAssertEqualObjects(error.errorMessage, @"Expected message",); + + error.errorClass = @"Expected error class"; + error.errorMessage = @"Expected message"; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [error updateWithCrashInfoMessage:nil]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(error.errorClass, @"Expected error class",); + XCTAssertEqualObjects(error.errorMessage, @"Expected message",); +} + @end diff --git a/Tests/BugsnagEventTests.m b/Tests/BugsnagEventTests.m index ec9d216e3..fe487e95d 100644 --- a/Tests/BugsnagEventTests.m +++ b/Tests/BugsnagEventTests.m @@ -97,233 +97,6 @@ - (void)testDefaultErrorMessageNilForEmptyThreads { payload[@"exceptions"][0][@"message"]); } -- (void)testEnhancedErrorMessageNilForEmptyNotableAddresses { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"threads" : @[ @{@"crashed" : @YES, @"notable_addresses" : @{}} ] - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"Exception", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageForFatalErrorWithoutAdditionalMessage { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"r14" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : @"fatal error" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"fatal error", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageForAssertionWithoutAdditionalMessage { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"r14" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : @"assertion failed" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"assertion failed", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageForAssertionError { - for (NSString *assertionName in @[ - @"assertion failed", @"Assertion failed", @"fatal error", - @"Fatal error" - ]) { - BugsnagEvent *event = - [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"x9" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"Something went wrong" - }, - @"r16" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : assertionName - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(assertionName, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"Something went wrong", - payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); - } -} - -- (void)testEnhancedErrorMessageIgnoresFilePaths { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"x9" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"/usr/include/lib/something.swift" - }, - @"r16" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : @"fatal error" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"fatal error", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageIgnoresNonStrings { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"x9" : @{ - @"address" : @4511086448, - @"type" : @"long", - @"value" : @"A message from beyond" - }, - @"r16" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : @"fatal error" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"fatal error", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageConcatenatesMultipleMessages { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"x9" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"A message from beyond" - }, - @"r14" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"Wo0o0o" - }, - @"r16" : @{ - @"address" : @4511089532, - @"type" : @"string", - @"value" : @"Fatal error" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"Fatal error", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"A message from beyond | Wo0o0o", - payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - -- (void)testEnhancedErrorMessageIgnoresUnknownAssertionTypes { - BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{ - @"crash" : @{ - @"threads" : @[ @{ - @"crashed" : @YES, - @"notable_addresses" : @{ - @"x9" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"A message from beyond" - }, - @"r14" : @{ - @"address" : @4511086448, - @"type" : @"string", - @"value" : @"Wo0o0o" - } - } - } ] - } - }]; - NSDictionary *payload = [event toJson]; - XCTAssertEqualObjects(@"Exception", - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(@"", payload[@"exceptions"][0][@"message"]); - XCTAssertEqualObjects(event.errors[0].errorClass, - payload[@"exceptions"][0][@"errorClass"]); - XCTAssertEqualObjects(event.errors[0].errorMessage, - payload[@"exceptions"][0][@"message"]); -} - - (void)testEmptyReport { BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:@{}]; XCTAssertNil(event); From 5c9a89ef00ee114e2567ebc303629f2aab4a68c9 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 16:19:16 +0000 Subject: [PATCH 14/33] Update e2e tests for Swift fatal error messages --- features/crashprobe.feature | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/features/crashprobe.feature b/features/crashprobe.feature index e371f3ffc..a0aec85ab 100644 --- a/features/crashprobe.feature +++ b/features/crashprobe.feature @@ -101,26 +101,23 @@ Feature: Reporting crash events And the "method" of stack frame 0 equals "objc_msgSend" And the "method" of stack frame 1 equals "__29-[ReleasedObjectScenario run]_block_invoke" -# N.B. this scenario is "imprecise" on CrashProbe due to line number info, -# which is not tested here as this would require symbolication Scenario: Crash within Swift code When I run "SwiftCrash" and relaunch the app And I configure Bugsnag for "SwiftCrash" And I wait to receive a request Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier - # And the exception "message" equals "Unexpectedly found nil while unwrapping an Optional value" And the exception "errorClass" equals "Fatal error" + And the exception "message" equals "Unexpectedly found nil while unwrapping an Optional value" + And the event "metaData.error.crashInfo" matches "Fatal error: Unexpectedly found nil while unwrapping an Optional value: file .+\.swift, line \d+\n" Scenario: Assertion failure in Swift code When I run "SwiftAssertion" and relaunch the app And I configure Bugsnag for "SwiftAssertion" And I wait to receive a request Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier - # Temporary workaround until potential issue is investigated thoroughly [PLAT-4875] - And the exception "errorClass" equals one of: - | Fatal error | - | EXC_BREAKPOINT | - # And the exception "message" equals "several unfortunate things just happened" + And the exception "errorClass" equals "Fatal error" + And the exception "message" equals "several unfortunate things just happened" + And the event "metaData.error.crashInfo" matches "Fatal error: several unfortunate things just happened: file .+\.swift, line \d+\n" Scenario: Dereference a null pointer When I run "NullPointerScenario" and relaunch the app From 46b87167fa22c0a6a9c048f1a896e74d14596215 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 16:19:47 +0000 Subject: [PATCH 15/33] Add libdispatch crash e2e scenario --- features/crashprobe.feature | 9 ++++++++ .../iOSTestApp.xcodeproj/project.pbxproj | 4 ++++ .../macOSTestApp.xcodeproj/project.pbxproj | 4 ++++ .../scenarios/DispatchCrashScenario.swift | 22 +++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 features/fixtures/shared/scenarios/DispatchCrashScenario.swift diff --git a/features/crashprobe.feature b/features/crashprobe.feature index a0aec85ab..2dd19f4a7 100644 --- a/features/crashprobe.feature +++ b/features/crashprobe.feature @@ -157,3 +157,12 @@ Feature: Reporting crash events And the exception "message" equals "Attempted to dereference garbage pointer 0x10." And the exception "errorClass" equals "EXC_BAD_ACCESS" And the "method" of stack frame 0 equals "objc_msgSend" + + Scenario: Misuse of libdispatch + When I run "DispatchCrashScenario" and relaunch the app + And I configure Bugsnag for "DispatchCrashScenario" + And I wait to receive a request + Then the request is valid for the error reporting API version "4.0" for the "iOS Bugsnag Notifier" notifier + And the exception "errorClass" equals "EXC_BREAKPOINT" + And the exception "message" starts with "BUG IN CLIENT OF LIBDISPATCH: dispatch_" + And the event "metaData.error.crashInfo" starts with "BUG IN CLIENT OF LIBDISPATCH: dispatch_" diff --git a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj index bff7d2ffc..318ce4776 100644 --- a/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/ios-swift-cocoapods/iOSTestApp.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 00432CC4240912A100826D05 /* EnabledErrorTypesCxxScenario.mm in Sources */ = {isa = PBXBuildFile; fileRef = 00432CC2240912A000826D05 /* EnabledErrorTypesCxxScenario.mm */; }; 00507A64242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00507A63242BFE5600EF1B87 /* EnabledBreadcrumbTypesIsNilScenario.swift */; }; 00CEB60D24080C690004793D /* EnabledErrorTypesScenario.m in Sources */ = {isa = PBXBuildFile; fileRef = 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */; }; + 0104085F258CA0A100933C60 /* DispatchCrashScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0104085E258CA0A100933C60 /* DispatchCrashScenario.swift */; }; 0163BFA72583B3CF008DC28B /* DiscardClassesScenarios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0163BFA62583B3CF008DC28B /* DiscardClassesScenarios.swift */; }; 6526A0D4248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */; }; 8A14F0F62282D4AE00337B05 /* (null) in Sources */ = {isa = PBXBuildFile; }; @@ -160,6 +161,7 @@ 00A98315240DBB7A0016A57E /* out_of_memory.feature */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = out_of_memory.feature; path = ../../../out_of_memory.feature; sourceTree = ""; }; 00CEB60B24080C690004793D /* EnabledErrorTypesScenario.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EnabledErrorTypesScenario.h; sourceTree = ""; }; 00CEB60C24080C690004793D /* EnabledErrorTypesScenario.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EnabledErrorTypesScenario.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 = ""; }; 4994F05E0421A0B037DD2CC5 /* Pods_iOSTestApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSTestApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6526A0D3248A83350002E2C9 /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; @@ -599,6 +601,7 @@ F42956D34274D4ED16B4D491 /* BuiltinTrapScenario.m */, F42951B01B327C380EC2D8A6 /* CxxExceptionScenario.h */, F429550B682F8F9677305881 /* CxxExceptionScenario.mm */, + 0104085E258CA0A100933C60 /* DispatchCrashScenario.swift */, F42950D49A5F24FF7155EEE1 /* NonExistentMethodScenario.h */, F4295F13EBCAC9CB0ABC4008 /* NonExistentMethodScenario.m */, F4295C1C7C54101194B61E93 /* NullPointerScenario.h */, @@ -933,6 +936,7 @@ 8A840FBA21AF5C450041DBFA /* SwiftAssertion.swift in Sources */, E753F24824927412001FB671 /* OnSendErrorCallbackCrashScenario.swift in Sources */, 001E5502243B8FDA0009E31D /* AutoCaptureRunScenario.m in Sources */, + 0104085F258CA0A100933C60 /* DispatchCrashScenario.swift in Sources */, E700EE55247D3204008CFFB6 /* OnSendOverwriteScenario.swift in Sources */, F429538D8941382EC2C857CE /* AsyncSafeThreadScenario.m in Sources */, F42955869D33EE0E510B9651 /* ReadGarbagePointerScenario.m in Sources */, diff --git a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj index ef03089f9..4886d277b 100644 --- a/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj +++ b/features/fixtures/macos/macOSTestApp.xcodeproj/project.pbxproj @@ -16,6 +16,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 */; }; + 01AF6A84258BB38A00FFC803 /* DispatchCrashScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AF6A83258BB38A00FFC803 /* DispatchCrashScenario.swift */; }; 01F47CC4254B1B3100B184AD /* OriginalErrorNSExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C21254B1B2C00B184AD /* OriginalErrorNSExceptionScenario.swift */; }; 01F47CC5254B1B3100B184AD /* LoadConfigFromFileAutoScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C23254B1B2C00B184AD /* LoadConfigFromFileAutoScenario.swift */; }; 01F47CC6254B1B3100B184AD /* HandledExceptionScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F47C28254B1B2C00B184AD /* HandledExceptionScenario.swift */; }; @@ -158,6 +159,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 = ""; }; + 01AF6A83258BB38A00FFC803 /* DispatchCrashScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatchCrashScenario.swift; sourceTree = ""; }; 01F47C21254B1B2C00B184AD /* OriginalErrorNSExceptionScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OriginalErrorNSExceptionScenario.swift; sourceTree = ""; }; 01F47C22254B1B2C00B184AD /* ThreadScenarios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadScenarios.h; sourceTree = ""; }; 01F47C23254B1B2C00B184AD /* LoadConfigFromFileAutoScenario.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadConfigFromFileAutoScenario.swift; sourceTree = ""; }; @@ -385,6 +387,7 @@ 0163BF9A2583AF2A008DC28B /* DiscardClassesScenarios.swift */, 01F47CA8254B1B3000B184AD /* DiscardedBreadcrumbTypeScenario.swift */, 01F47CB6254B1B3000B184AD /* DiscardSessionScenario.swift */, + 01AF6A83258BB38A00FFC803 /* DispatchCrashScenario.swift */, 01F47C3D254B1B2D00B184AD /* EnabledBreadcrumbTypesIsNilScenario.swift */, 01F47C5C254B1B2E00B184AD /* EnabledErrorTypesCxxScenario.h */, 01F47C81254B1B2F00B184AD /* EnabledErrorTypesCxxScenario.mm */, @@ -698,6 +701,7 @@ 01F47D30254B1B3100B184AD /* AutoContextNSExceptionScenario.swift in Sources */, 01F47CE1254B1B3100B184AD /* ManualSessionWithUserScenario.m in Sources */, 01F47D01254B1B3100B184AD /* SessionCallbackOrderScenario.swift in Sources */, + 01AF6A84258BB38A00FFC803 /* DispatchCrashScenario.swift in Sources */, 01F47D0A254B1B3100B184AD /* CxxExceptionScenario.mm in Sources */, 01F47D03254B1B3100B184AD /* OriginalErrorNSErrorScenario.swift in Sources */, 01F47D28254B1B3100B184AD /* ReadGarbagePointerScenario.m in Sources */, diff --git a/features/fixtures/shared/scenarios/DispatchCrashScenario.swift b/features/fixtures/shared/scenarios/DispatchCrashScenario.swift new file mode 100644 index 000000000..d3b6168e4 --- /dev/null +++ b/features/fixtures/shared/scenarios/DispatchCrashScenario.swift @@ -0,0 +1,22 @@ +// +// DispatchCrashScenario.swift +// iOSTestApp +// +// Created by Nick Dowell on 17/12/2020. +// Copyright © 2020 Bugsnag. All rights reserved. +// + +class DispatchCrashScenario: Scenario { + + override func startBugsnag() { + config.autoTrackSessions = false + super.startBugsnag() + } + + override func run() { + precondition(Thread.isMainThread) + DispatchQueue.main.sync { + print("This code will never run because DispatchQueue.main.sync was called on the main thread") + } + } +} From 9e0759cb678ecebc65fc03b5db129750e9ad96b3 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 16:26:06 +0000 Subject: [PATCH 16/33] Remove RegisterErrorData class Swift error messages are now extracted from crash_info_message --- Bugsnag.xcodeproj/project.pbxproj | 28 ---- Bugsnag/Payload/BugsnagEvent.m | 2 +- Bugsnag/RegisterErrorData.h | 23 --- Bugsnag/RegisterErrorData.m | 82 ---------- Tests/RegisterErrorDataTest.m | 262 ------------------------------ 5 files changed, 1 insertion(+), 396 deletions(-) delete mode 100644 Bugsnag/RegisterErrorData.h delete mode 100644 Bugsnag/RegisterErrorData.m delete mode 100644 Tests/RegisterErrorDataTest.m diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index e0166ef4f..38db8a29d 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -34,9 +34,6 @@ 008966F42486D43700DC48C2 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A72486D43400DC48C2 /* BugsnagThreadTest.m */; }; 008966F52486D43700DC48C2 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A72486D43400DC48C2 /* BugsnagThreadTest.m */; }; 008966F62486D43700DC48C2 /* BugsnagThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A72486D43400DC48C2 /* BugsnagThreadTest.m */; }; - 008966F72486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */; }; - 008966F82486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */; }; - 008966F92486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */; }; 008966FD2486D43700DC48C2 /* BugsnagOnBreadcrumbTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966AB2486D43500DC48C2 /* BugsnagOnBreadcrumbTest.m */; }; 008966FE2486D43700DC48C2 /* BugsnagOnBreadcrumbTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966AB2486D43500DC48C2 /* BugsnagOnBreadcrumbTest.m */; }; 008966FF2486D43700DC48C2 /* BugsnagOnBreadcrumbTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 008966AB2486D43500DC48C2 /* BugsnagOnBreadcrumbTest.m */; }; @@ -609,13 +606,6 @@ 00AD1C7C24869B0E00A27979 /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00AD1C7224869B0E00A27979 /* Bugsnag.framework */; }; 00AD1CB624869C1200A27979 /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00AD1CAD24869C1200A27979 /* Bugsnag.framework */; }; 00AD1CD224869C2400A27979 /* Bugsnag.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00AD1CC924869C2400A27979 /* Bugsnag.framework */; }; - 00AD1F022486A17900A27979 /* RegisterErrorData.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF42486A17600A27979 /* RegisterErrorData.h */; }; - 00AD1F032486A17900A27979 /* RegisterErrorData.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF42486A17600A27979 /* RegisterErrorData.h */; }; - 00AD1F042486A17900A27979 /* RegisterErrorData.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF42486A17600A27979 /* RegisterErrorData.h */; }; - 00AD1F052486A17900A27979 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = 00AD1EF52486A17600A27979 /* RegisterErrorData.m */; }; - 00AD1F062486A17900A27979 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = 00AD1EF52486A17600A27979 /* RegisterErrorData.m */; }; - 00AD1F072486A17900A27979 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = 00AD1EF52486A17600A27979 /* RegisterErrorData.m */; }; - 00AD1F082486A17900A27979 /* RegisterErrorData.m in Sources */ = {isa = PBXBuildFile; fileRef = 00AD1EF52486A17600A27979 /* RegisterErrorData.m */; }; 00AD1F102486A17900A27979 /* BugsnagSessionTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF82486A17700A27979 /* BugsnagSessionTracker.h */; }; 00AD1F112486A17900A27979 /* BugsnagSessionTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF82486A17700A27979 /* BugsnagSessionTracker.h */; }; 00AD1F122486A17900A27979 /* BugsnagSessionTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EF82486A17700A27979 /* BugsnagSessionTracker.h */; }; @@ -911,7 +901,6 @@ E746299524890D3200F92D67 /* BugsnagCrashSentry.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 00AD1F002486A17900A27979 /* BugsnagCrashSentry.h */; }; E746299624890D3200F92D67 /* BugsnagSessionTracker.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 00AD1EF82486A17700A27979 /* BugsnagSessionTracker.h */; }; E746299724890D3200F92D67 /* BugsnagErrorReportSink.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 00AD1EFD2486A17800A27979 /* BugsnagErrorReportSink.h */; }; - E746299824890D3200F92D67 /* RegisterErrorData.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 00AD1EF42486A17600A27979 /* RegisterErrorData.h */; }; E75A5CDB248A5D97005D2C74 /* BugsnagErrorReportSink.h in Headers */ = {isa = PBXBuildFile; fileRef = 00AD1EFD2486A17800A27979 /* BugsnagErrorReportSink.h */; }; E75A5CDC248A5DA2005D2C74 /* BugsnagErrorReportSink.m in Sources */ = {isa = PBXBuildFile; fileRef = 00AD1EF92486A17700A27979 /* BugsnagErrorReportSink.m */; }; /* End PBXBuildFile section */ @@ -1030,7 +1019,6 @@ E746299524890D3200F92D67 /* BugsnagCrashSentry.h in CopyFiles */, E746299624890D3200F92D67 /* BugsnagSessionTracker.h in CopyFiles */, E746299724890D3200F92D67 /* BugsnagErrorReportSink.h in CopyFiles */, - E746299824890D3200F92D67 /* RegisterErrorData.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1044,7 +1032,6 @@ 008966A62486D43400DC48C2 /* BugsnagMetadataRedactionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagMetadataRedactionTest.m; sourceTree = ""; }; 008966A72486D43400DC48C2 /* BugsnagThreadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagThreadTest.m; sourceTree = ""; }; 008966A82486D43400DC48C2 /* BugsnagTestConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagTestConstants.h; sourceTree = ""; }; - 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegisterErrorDataTest.m; sourceTree = ""; }; 008966AB2486D43500DC48C2 /* BugsnagOnBreadcrumbTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagOnBreadcrumbTest.m; sourceTree = ""; }; 008966AC2486D43500DC48C2 /* BugsnagEventPersistLoadTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagEventPersistLoadTest.m; sourceTree = ""; }; 008966AD2486D43500DC48C2 /* BugsnagThreadSerializationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagThreadSerializationTest.m; sourceTree = ""; }; @@ -1242,8 +1229,6 @@ 00AD1CC924869C2400A27979 /* Bugsnag.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Bugsnag.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 00AD1CD124869C2400A27979 /* Bugsnag-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Bugsnag-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 00AD1CE424869C6C00A27979 /* libBugsnagStatic.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libBugsnagStatic.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 00AD1EF42486A17600A27979 /* RegisterErrorData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegisterErrorData.h; sourceTree = ""; }; - 00AD1EF52486A17600A27979 /* RegisterErrorData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegisterErrorData.m; sourceTree = ""; }; 00AD1EF82486A17700A27979 /* BugsnagSessionTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagSessionTracker.h; sourceTree = ""; }; 00AD1EF92486A17700A27979 /* BugsnagErrorReportSink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagErrorReportSink.m; sourceTree = ""; }; 00AD1EFD2486A17800A27979 /* BugsnagErrorReportSink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagErrorReportSink.h; sourceTree = ""; }; @@ -1631,8 +1616,6 @@ 01937D09257A7ED000F2DE31 /* BugsnagSessionTracker+Private.h */, CBB0928B2519F891007698BC /* BugsnagSystemState.h */, CBB0928A2519F891007698BC /* BugsnagSystemState.m */, - 00AD1EF42486A17600A27979 /* RegisterErrorData.h */, - 00AD1EF52486A17600A27979 /* RegisterErrorData.m */, 00AD1CF124869EBD00A27979 /* Breadcrumbs */, 00AD1CF224869ECF00A27979 /* Client */, 00AD1CF324869ED700A27979 /* Configuration */, @@ -1699,7 +1682,6 @@ 00E636C324878FFC006CBF1A /* Info.plist */, 008966D32486D43700DC48C2 /* KSCrash */, 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */, - 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */, 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */, 008966B72486D43500DC48C2 /* report.json */, 008966AE2486D43500DC48C2 /* Swift Tests */, @@ -2012,7 +1994,6 @@ 00896A112486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969ED2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, 01468F5225876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, - 00AD1F022486A17900A27979 /* RegisterErrorData.h in Headers */, 008968ED2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A292486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, 008969DB2486DAD100DC48C2 /* BSG_KSCrash.h in Headers */, @@ -2108,7 +2089,6 @@ 008969972486DAD100DC48C2 /* BSG_KSBacktrace_Private.h in Headers */, 00896A122486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969EE2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, - 00AD1F032486A17900A27979 /* RegisterErrorData.h in Headers */, 01468F5325876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, 008968EE2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A2A2486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, @@ -2206,7 +2186,6 @@ 008969982486DAD100DC48C2 /* BSG_KSBacktrace_Private.h in Headers */, 00896A132486DAD100DC48C2 /* BSG_KSCrashSentry_CPPException.h in Headers */, 008969EF2486DAD100DC48C2 /* BSG_KSCrashDoctor.h in Headers */, - 00AD1F042486A17900A27979 /* RegisterErrorData.h in Headers */, 01468F5425876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */, 008968EF2486DAB800DC48C2 /* BugsnagFileStore.h in Headers */, 00896A2B2486DAD100DC48C2 /* BSG_KSCrashType.h in Headers */, @@ -2527,7 +2506,6 @@ 008968C32486DA9600DC48C2 /* BugsnagUser.m in Sources */, CBAB4DD82510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 008968A72486DA9600DC48C2 /* BugsnagSession.m in Sources */, - 00AD1F052486A17900A27979 /* RegisterErrorData.m in Sources */, 0089683A2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, 008969F62486DAD100DC48C2 /* BSG_KSCrash.m in Sources */, 0089695D2486DAD000DC48C2 /* BSG_KSMach_x86_32.c in Sources */, @@ -2604,7 +2582,6 @@ 008967332486D43700DC48C2 /* BugsnagClientTests.m in Sources */, 004E353F2487B3BD007FBAE4 /* BugsnagSwiftConfigurationTests.swift in Sources */, 008967542486D43700DC48C2 /* BugsnagOnCrashTest.m in Sources */, - 008966F72486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */, 008967152486D43700DC48C2 /* BugsnagCollectionsBSGDictMergeTest.m in Sources */, 01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */, 008967AB2486D43700DC48C2 /* KSMach_Tests.m in Sources */, @@ -2691,7 +2668,6 @@ 008968C42486DA9600DC48C2 /* BugsnagUser.m in Sources */, CBAB4DD92510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 008968A82486DA9600DC48C2 /* BugsnagSession.m in Sources */, - 00AD1F062486A17900A27979 /* RegisterErrorData.m in Sources */, 0089683B2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, 008969F72486DAD100DC48C2 /* BSG_KSCrash.m in Sources */, 0089695E2486DAD000DC48C2 /* BSG_KSMach_x86_32.c in Sources */, @@ -2729,7 +2705,6 @@ 0089679A2486D43700DC48C2 /* FileBasedTestCase.m in Sources */, 008967912486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, 008967732486D43700DC48C2 /* KSSysCtl_Tests.m in Sources */, - 008966F82486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */, E701FAAC2490EFD9008D842F /* EventApiValidationTest.m in Sources */, 0089674F2486D43700DC48C2 /* BugsnagPluginTest.m in Sources */, 008967132486D43700DC48C2 /* BugsnagEventTests.m in Sources */, @@ -2853,7 +2828,6 @@ 008968C52486DA9600DC48C2 /* BugsnagUser.m in Sources */, CBAB4DDA2510D2460092CBAA /* BugsnagKVStoreObjC.m in Sources */, 008968A92486DA9600DC48C2 /* BugsnagSession.m in Sources */, - 00AD1F072486A17900A27979 /* RegisterErrorData.m in Sources */, 0089683C2486DA6C00DC48C2 /* BugsnagMetadata.m in Sources */, 008969F82486DAD100DC48C2 /* BSG_KSCrash.m in Sources */, 0089695F2486DAD000DC48C2 /* BSG_KSMach_x86_32.c in Sources */, @@ -2888,7 +2862,6 @@ CB10E541250BA8E000AF5824 /* BugsnagKVStoreTest.m in Sources */, 008967922486D43700DC48C2 /* KSJSONCodec_Tests.m in Sources */, 008967742486D43700DC48C2 /* KSSysCtl_Tests.m in Sources */, - 008966F92486D43700DC48C2 /* RegisterErrorDataTest.m in Sources */, 008967502486D43700DC48C2 /* BugsnagPluginTest.m in Sources */, 008967142486D43700DC48C2 /* BugsnagEventTests.m in Sources */, 0089675C2486D43700DC48C2 /* BugsnagEnabledBreadcrumbTest.m in Sources */, @@ -3030,7 +3003,6 @@ 008968942486DA9600DC48C2 /* BugsnagError.m in Sources */, 008967E12486DA2D00DC48C2 /* BSGConfigurationBuilder.m in Sources */, 008967FD2486DA4500DC48C2 /* BugsnagApiClient.m in Sources */, - 00AD1F082486A17900A27979 /* RegisterErrorData.m in Sources */, 008968EC2486DAB800DC48C2 /* BugsnagSessionFileStore.m in Sources */, 008968F32486DAB800DC48C2 /* BugsnagFileStore.m in Sources */, ); diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 7237a8b4e..8619ad27e 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -37,7 +37,7 @@ #import "BugsnagStacktrace+Private.h" #import "BugsnagThread+Private.h" #import "BugsnagUser+Private.h" -#import "RegisterErrorData.h" + static NSString *const DEFAULT_EXCEPTION_TYPE = @"cocoa"; diff --git a/Bugsnag/RegisterErrorData.h b/Bugsnag/RegisterErrorData.h deleted file mode 100644 index 6bf108c91..000000000 --- a/Bugsnag/RegisterErrorData.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// RegisterErrorData.h -// Bugsnag -// -// Created by Jamie Lynch on 07/04/2020. -// Copyright © 2020 Bugsnag. All rights reserved. -// - -#import - -/** - * Inspects data from the register captured by the KSCrash report for - * useful information that can be added to the error class/message. For - * example, this can enhance the error message set for Swift's fatalError(). - */ -@interface RegisterErrorData : NSObject -@property (nonatomic, strong) NSString *_Nullable errorClass; -@property (nonatomic, strong) NSString *_Nullable errorMessage; -+ (instancetype _Nullable )errorDataFromThreads:(NSArray *_Nullable)threads; -- (instancetype _Nonnull )initWithClass:(NSString *_Nonnull)errorClass - message:(NSString *_Nonnull)errorMessage - NS_DESIGNATED_INITIALIZER; -@end diff --git a/Bugsnag/RegisterErrorData.m b/Bugsnag/RegisterErrorData.m deleted file mode 100644 index bd96c9b74..000000000 --- a/Bugsnag/RegisterErrorData.m +++ /dev/null @@ -1,82 +0,0 @@ -// -// RegisterErrorData.m -// Bugsnag -// -// Created by Jamie Lynch on 07/04/2020. -// Copyright © 2020 Bugsnag. All rights reserved. -// - -#import "RegisterErrorData.h" -#import "BugsnagKeys.h" - -@implementation RegisterErrorData -+ (instancetype)errorDataFromThreads:(NSArray *)threads { - for (NSDictionary *thread in threads) { - if (![thread[@"crashed"] boolValue]) { - continue; - } - NSDictionary *notableAddresses = thread[@"notable_addresses"]; - NSMutableArray *interestingValues = [NSMutableArray new]; - NSString *reservedWord = nil; - - for (NSString *key in notableAddresses) { - NSDictionary *data = notableAddresses[key]; - if (![@"string" isEqualToString:data[BSGKeyType]]) { - continue; - } - NSString *contentValue = data[@"value"]; - - if (contentValue == nil || ![contentValue isKindOfClass:[NSString class]]) { - continue; - } - - if ([self isReservedWord:contentValue]) { - reservedWord = contentValue; - } else if ([[contentValue componentsSeparatedByString:@"/"] count] <= 2) { - // must be a string that isn't a reserved word and isn't a filepath - [interestingValues addObject:contentValue]; - } - } - - [interestingValues sortUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; - - NSString *message = [interestingValues componentsJoinedByString:@" | "]; - // reservedWord *shouldn't* equal nil, but since RegisterErrorData expects a non-nil - // argument guard against it anyway, and fall through. - if (reservedWord != nil) { - return [[RegisterErrorData alloc] initWithClass:reservedWord - message:message]; - } - } - return nil; -} - -/** - * Determines whether a string is a "reserved word" that identifies it as a known value. - * - * For fatalError, preconditionFailure, and assertionFailure, "fatal error" will be in one of the registers. - * - * For assert, "assertion failed" will be in one of the registers. - */ -+ (BOOL)isReservedWord:(NSString *)contentValue { - return [@"assertion failed" caseInsensitiveCompare:contentValue] == NSOrderedSame - || [@"fatal error" caseInsensitiveCompare:contentValue] == NSOrderedSame - || [@"precondition failed" caseInsensitiveCompare:contentValue] == NSOrderedSame; -} - -- (instancetype)init { - return [self initWithClass:@"Unknown" message:@""]; -} - -- (instancetype)initWithClass:(NSString *)errorClass message:(NSString *)errorMessage { - if (errorClass.length == 0) { - return nil; - } - if (self = [super init]) { - _errorClass = errorClass; - _errorMessage = errorMessage; - } - return self; -} - -@end diff --git a/Tests/RegisterErrorDataTest.m b/Tests/RegisterErrorDataTest.m deleted file mode 100644 index c302835a9..000000000 --- a/Tests/RegisterErrorDataTest.m +++ /dev/null @@ -1,262 +0,0 @@ -// -// Created by Jamie Lynch on 11/06/2018. -// Copyright (c) 2018 Bugsnag. All rights reserved. -// - - -#import -#import - -@interface RegisterErrorData -+ (instancetype)errorDataFromThreads:(NSArray *)threads; -@property (nonatomic, strong) NSString *errorClass; -@property (nonatomic, strong) NSString *errorMessage; -@end - -@interface RegisterErrorDataTest : XCTestCase -@end - -@implementation RegisterErrorDataTest - - -- (void)testNilAddresses { - XCTAssertNil([RegisterErrorData errorDataFromThreads:nil]); -} - -- (void)testEmptyAddresses { - XCTAssertNil([RegisterErrorData errorDataFromThreads:@[]]); -} - -- (void)testEmptyCrashedThreadDict { - NSDictionary *thread = @{ - @"crashed": @YES - }; - XCTAssertNil([RegisterErrorData errorDataFromThreads:@[thread]]); -} - -- (void)testEmptyNotableAddresses { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{} - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNil(data); -} - -- (void)testEmptyContentValue { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{} - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNil(data); -} - -- (void)testNilValueImplicit { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNil(data); -} - -- (void)testNilValueExplicit { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": [NSNull null] - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNil(data); -} - -- (void)testHasTypeAndValue{ - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"Hello, World!" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNil(data); -} - -- (void)testFatalError { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNotNil(data); - XCTAssertEqualObjects(@"fatal error", data.errorClass); - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testAssertionFailed { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"assertion failed" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNotNil(data); - XCTAssertEqualObjects(@"assertion failed", data.errorClass); - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testPreconditionFailed { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"precondition failed" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertNotNil(data); - XCTAssertEqualObjects(@"precondition failed", data.errorClass); - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testSingleMessageValue { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"string", - @"value": @"Single Message" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"Single Message", data.errorMessage); -} - -- (void)testMultiMessageValue { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"string", - @"value": @"A is for aardvark" - }, - @"message2": @{ - @"type": @"string", - @"value": @"Z is for zebra" - }, - @"message3": @{ - @"type": @"string", - @"value": @"C is for crayfish" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"A is for aardvark | C is for crayfish | Z is for zebra", data.errorMessage); -} - -- (void)testStackExcluded { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"stack", - @"value": @"0xf0924501" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testOtherTypesExcluded { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"someOtherType", - @"value": @"do not serialise" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testFilepathExcluded { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"string", - @"value": @"/usr/share/locale" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"", data.errorMessage); -} - -- (void)testForwardSlashIncluded { - NSDictionary *thread = @{ - @"crashed": @YES, - @"notable_addresses": @{ - @"hello_world": @{ - @"type": @"string", - @"value": @"fatal error" - }, - @"message": @{ - @"type": @"string", - @"value": @"usr/share" - } - } - }; - RegisterErrorData *data = [RegisterErrorData errorDataFromThreads:@[thread]]; - XCTAssertEqualObjects(@"usr/share", data.errorMessage); -} - -@end From 0d93d873fe3e73ad0ad4dee3f73a2d7da6c81dc9 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Thu, 17 Dec 2020 16:31:32 +0000 Subject: [PATCH 17/33] Disable introspection of registers at crash time We were using this to capture Swift fatal error messages but now have a more reliable mechanism. --- Bugsnag/BugsnagCrashSentry.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bugsnag/BugsnagCrashSentry.m b/Bugsnag/BugsnagCrashSentry.m index 7233bdb58..0e9c7928b 100644 --- a/Bugsnag/BugsnagCrashSentry.m +++ b/Bugsnag/BugsnagCrashSentry.m @@ -25,7 +25,7 @@ - (void)install:(BugsnagConfiguration *)config BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient]; BSG_KSCrash *ksCrash = [BSG_KSCrash sharedInstance]; ksCrash.sink = sink; - ksCrash.introspectMemory = YES; + ksCrash.introspectMemory = NO; ksCrash.onCrash = onCrash; ksCrash.maxStoredReports = (int)config.maxPersistedEvents; From 2118f32c885d1d083103a1e29d9e5587a6d2b07a Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Fri, 18 Dec 2020 17:27:51 +0000 Subject: [PATCH 18/33] Cater for lowercased error messages from Swift < 4.1 See https://github.com/apple/swift/commit/d03a575279cf5c523779ef68f8d7903f09ba901e --- Bugsnag/Payload/BugsnagError.m | 2 +- Tests/BugsnagErrorTest.m | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Bugsnag/Payload/BugsnagError.m b/Bugsnag/Payload/BugsnagError.m index 2577e9a55..d44cd2f4d 100644 --- a/Bugsnag/Payload/BugsnagError.m +++ b/Bugsnag/Payload/BugsnagError.m @@ -132,7 +132,7 @@ - (void)updateWithCrashInfoMessage:(NSString *)crashInfoMessage { @try { // Messages that match this pattern should override the errorClass (and errorMessage if there is enough information.) NSString *pattern = @"^(Assertion failed|Fatal error|Precondition failed): ((.+): )?file .+, line \\d+\n$"; - NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:pattern options:0 error:nil]; + NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil]; NSArray *matches = [regex matchesInString:crashInfoMessage options:0 range:NSMakeRange(0, crashInfoMessage.length)]; if (matches.count != 1 || matches[0].numberOfRanges != 4) { if (!self.errorMessage.length) { diff --git a/Tests/BugsnagErrorTest.m b/Tests/BugsnagErrorTest.m index 939933ecf..4a2ce5e7d 100644 --- a/Tests/BugsnagErrorTest.m +++ b/Tests/BugsnagErrorTest.m @@ -160,18 +160,36 @@ - (void)testUpdateWithCrashInfoMessage { XCTAssertEqualObjects(error.errorClass, @"Assertion failed"); XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"assertion failed: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"assertion failed"); + XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + error.errorClass = nil; error.errorMessage = nil; [error updateWithCrashInfoMessage:@"Fatal error: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\n"]; XCTAssertEqualObjects(error.errorClass, @"Fatal error"); XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"fatal error: This should NEVER happen: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"fatal error"); + XCTAssertEqualObjects(error.errorMessage, @"This should NEVER happen"); + error.errorClass = nil; error.errorMessage = nil; [error updateWithCrashInfoMessage:@"Precondition failed: : strange formatting 😱::: file bugsnag_example/AnotherClass.swift, line 24\n"]; XCTAssertEqualObjects(error.errorClass, @"Precondition failed"); XCTAssertEqualObjects(error.errorMessage, @" : strange formatting 😱::"); + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"precondition failed: : strange formatting 😱::: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"precondition failed"); + XCTAssertEqualObjects(error.errorMessage, @" : strange formatting 😱::"); + // Swift fatal errors without a message. // The errorClass should be overwritten but the errorMessage left as-is. From df99c08a1c9ba82996c8adccbaf4f127e5de6a50 Mon Sep 17 00:00:00 2001 From: Alex Moinet Date: Mon, 21 Dec 2020 12:22:50 +0000 Subject: [PATCH 19/33] [PLAT-5434, PLAT-5493] Move unit tests from Travis to buildkite (#949) --- .buildkite/pipeline.yml | 253 +++++++++++++++++- .travis.yml | 129 +-------- Bugsnag.xcodeproj/project.pbxproj | 10 + .../BugsnagConfiguration+Private.h | 3 + Bugsnag/Configuration/BugsnagConfiguration.m | 18 +- Makefile | 18 +- Tests/BugsnagConfigurationTests.m | 2 +- Tests/NSUserDefaultsStub.h | 17 ++ Tests/NSUserDefaultsStub.m | 41 +++ 9 files changed, 356 insertions(+), 135 deletions(-) create mode 100644 Tests/NSUserDefaultsStub.h create mode 100644 Tests/NSUserDefaultsStub.m diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 864905884..c36c3e3f5 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,5 +1,6 @@ steps: - label: Build cocoa IPA + key: cocoa_fixture timeout_in_minutes: 20 agents: queue: opensource-mac-cocoa @@ -10,9 +11,242 @@ steps: - ./features/scripts/export_ios_app.sh - ./features/scripts/export_mac_app.sh - - wait + - label: Static framework and Swift Package Manager builds + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + concurrency: 3 + concurrency_group: cocoa-unit-tests + env: + LANG: "en_GB.UTF-8" + commands: + - mkdir -p features/fixtures/carthage-proj + - make build_swift + - make build_ios_static + + - label: macOS 11 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-11 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "macOS" + commands: + - make bootstrap + - make analyze test + + - label: macOS 10.15 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + concurrency: 3 + concurrency_group: cocoa-unit-tests + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "macOS" + commands: + - make bootstrap + - make analyze test + + - label: macOS 10.14 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.14 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "macOS" + commands: + - make bootstrap + - make analyze test + + - label: macOS 10.13 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.13 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "macOS" + commands: + - make bootstrap + - make analyze test + + - label: iOS 14 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-11 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "iOS" + OS: "14.2" + commands: + - make bootstrap + - make test + + - label: iOS 13 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "iOS" + OS: "13.7" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: iOS 12 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "iOS" + OS: "12.4" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: iOS 11 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "iOS" + OS: "11.4" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: iOS 10 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.14 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "iOS" + OS: "10.3.1" + DEVICE: "iPhone 5s" + commands: + - make bootstrap + - make test + + - label: iOS 9 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.13 + env: + LANG: "en_GB.UTF-8" + PLATFORM: "iOS" + OS: "9.3" + DEVICE: "iPhone 5s" + commands: + - make bootstrap + - TEST_CONFIGURATION=Debug make test + - TEST_CONFIGURATION=Release make test + + - label: tvOS 14 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-11 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "14.0" + commands: + - make bootstrap + - make test + + - label: tvOS 13 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "13.3" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: tvOS 12 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "12.4" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: tvOS 11 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "11.4" + concurrency: 3 + concurrency_group: cocoa-unit-tests + commands: + - make bootstrap + - make test + + - label: tvOS 10 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.14 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "10.2" + commands: + - make bootstrap + - make test + + - label: tvOS 9 unit tests + timeout_in_minutes: 10 + agents: + queue: opensource-mac-cocoa-10.13 + env: + LANG: "en_GB.UTF-8" + TEST_CONFIGURATION: "Debug" + PLATFORM: "tvOS" + OS: "9.2" + commands: + - make bootstrap + - make test - label: ':ios: iOS 14 end-to-end tests' + depends_on: + - cocoa_fixture timeout_in_minutes: 60 agents: queue: opensource @@ -36,6 +270,8 @@ steps: limit: 2 - label: ':ios: iOS 13 end-to-end tests' + depends_on: + - cocoa_fixture timeout_in_minutes: 60 agents: queue: opensource @@ -59,6 +295,8 @@ steps: limit: 2 - label: ':ios: iOS 12 end-to-end tests' + depends_on: + - cocoa_fixture timeout_in_minutes: 60 agents: queue: opensource @@ -82,6 +320,8 @@ steps: limit: 2 - label: ':ios: iOS 11 end-to-end tests' + depends_on: + - cocoa_fixture # More time than other steps as the BrowserStack iOS 11 devices seem particularly unstable and # sessions need resetting frequently, taking a minute or more each time. timeout_in_minutes: 90 @@ -107,6 +347,8 @@ steps: limit: 2 - label: ':ios: iOS 10 end-to-end tests' + depends_on: + - cocoa_fixture timeout_in_minutes: 60 agents: queue: opensource @@ -128,3 +370,12 @@ steps: automatic: - exit_status: -1 # Agent was lost limit: 2 + + - label: 'Update documentation page' + if: build.tag =~ /^v[2-9]\.[0-9]+\.[0-9]+\$/ && build.branch == "master" + agents: + queue: opensource-mac-cocoa + concurrency: 3 + concurrency_group: cocoa-unit-tests + command: + - make update-docs \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 174123790..c0086794a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,142 +1,15 @@ language: objective-c cache: - bundler -- cocoapods - -install: make bootstrap - -env: -- TEST_CONFIGURATION=Debug - -script: make analyze test - -stages: -- build -- macOS unit tests -- unit tests -- integration -- name: deploy - if: branch = master AND tag IS present jobs: include: - - # ---------------------------------------------------------------------------- - # Unit Tests - Mac - # ---------------------------------------------------------------------------- - - - osx_image: xcode12.2 # macos 10.15.7 - name: macOS 10.15.7 unit tests - stage: macOS unit tests - env: PLATFORM=macOS - - - osx_image: xcode11.6 # macos 10.15.5 - name: macOS 10.15.5 unit tests - stage: macOS unit tests - env: PLATFORM=macOS - - - osx_image: xcode10.2 # macos 10.14 - name: macOS 10.14 unit tests - stage: macOS unit tests - env: PLATFORM=macOS - - - osx_image: xcode9.4 # macos 10.13 - name: macOS 10.13 unit tests - stage: macOS unit tests - env: PLATFORM=macOS - - # ---------------------------------------------------------------------------- - # Unit Tests - iOS - # ---------------------------------------------------------------------------- - - - osx_image: xcode12.2 - name: iOS 11-14 unit tests - stage: unit tests - env: - - PLATFORM=iOS - - DEVICE="iPhone 8" - script: - - make test OS=14.1 - - make test OS=13.5 - - make test OS=12.4 - - make test OS=11.4 - - - osx_image: xcode11 - name: iOS 10 unit tests - stage: unit tests - env: - - PLATFORM=iOS - - DEVICE="iPhone 7" - script: - - make test OS=10.3.1 - - - osx_image: xcode10.2 - name: iOS 9 unit tests - stage: unit tests - env: PLATFORM=iOS - before_script: - # https://stackoverflow.com/questions/55389080/xcode-10-2-failed-to-run-app-on-simulator-with-ios-10 - - sudo mkdir -p '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 9.3.simruntime/Contents/Resources/RuntimeRoot/usr/lib/swift' - script: - - make test OS=9.3 - - make test OS=9.3 TEST_CONFIGURATION=Release - - # ---------------------------------------------------------------------------- - # Unit Tests - tvOS - # ---------------------------------------------------------------------------- - - - osx_image: xcode12.2 - name: tvOS 11-14 unit tests - stage: unit tests - env: PLATFORM=tvOS - script: - - make test OS=14.0 - - make test OS=13.3 - - make test OS=12.4 - - make test OS=11.4 - - - osx_image: xcode11 - name: tvOS 10 unit tests - stage: unit tests - env: PLATFORM=tvOS - script: - - make test OS=10.2 - - - osx_image: xcode10.2 - name: tvOS 9 unit tests - stage: unit tests - env: PLATFORM=tvOS - script: - - make test OS=9.2 - - # ---------------------------------------------------------------------------- - # Static framework, Carthage and Swift Package Manager builds - # ---------------------------------------------------------------------------- - - osx_image: xcode11 - name: Static framework, Carthage and Swift Package Manager builds + name: Carthage build stage: integration before_script: # Xcode 11+ no longer ships with all device combinations premade - xcrun simctl create "13-xs" "iPhone XS" com.apple.CoreSimulator.SimRuntime.iOS-13-0 - mkdir -p features/fixtures/carthage-proj script: - - make build_swift # Build with Swift Package Manager - make build_carthage # Build example carthage project - - make build_ios_static # Build static framework target - - # ---------------------------------------------------------------------------- - # Doc Generation - # ---------------------------------------------------------------------------- - - - osx_image: xcode11 - stage: deploy - before_deploy: make doc - script: skip - deploy: - provider: pages - local_dir: docs # only include the contents of the generated docs dir - skip_cleanup: true - github_token: $GITHUB_TOKEN # set in travis-ci dashboard - on: - tags: true # only deploy when tag is applied to commit diff --git a/Bugsnag.xcodeproj/project.pbxproj b/Bugsnag.xcodeproj/project.pbxproj index ca53af7ff..a881fbfcd 100644 --- a/Bugsnag.xcodeproj/project.pbxproj +++ b/Bugsnag.xcodeproj/project.pbxproj @@ -646,6 +646,9 @@ 0126DF1D257A92860031A70C /* BugsnagSession+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0126DF1A257A92860031A70C /* BugsnagSession+Private.h */; }; 0140D29A25767C9A00FD0306 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; 01447605256684500018AB94 /* BugsnagApiClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = CB9103632502320A00E9D1E2 /* BugsnagApiClientTest.m */; }; + 016875C6258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 016875C5258D003200DFFF69 /* NSUserDefaultsStub.m */; }; + 016875C7258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 016875C5258D003200DFFF69 /* NSUserDefaultsStub.m */; }; + 016875C8258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 016875C5258D003200DFFF69 /* NSUserDefaultsStub.m */; }; 01468F5225876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; 01468F5325876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; 01468F5425876DC1002B0519 /* BSGNotificationBreadcrumbs.h in Headers */ = {isa = PBXBuildFile; fileRef = 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */; }; @@ -1273,6 +1276,8 @@ 0134524A256BCF7C0088C548 /* BugsnagError+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagError+Private.h"; sourceTree = ""; }; 0134524B256BD00A0088C548 /* BugsnagThread+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BugsnagThread+Private.h"; sourceTree = ""; }; 0140D24725765F8F00FD0306 /* BSGUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BSGUIKit.h; sourceTree = ""; }; + 016875C4258D003200DFFF69 /* NSUserDefaultsStub.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSUserDefaultsStub.h; sourceTree = ""; }; + 016875C5258D003200DFFF69 /* NSUserDefaultsStub.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSUserDefaultsStub.m; sourceTree = ""; }; 01468F5025876DC1002B0519 /* BSGNotificationBreadcrumbs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BSGNotificationBreadcrumbs.h; sourceTree = ""; }; 01468F5125876DC1002B0519 /* BSGNotificationBreadcrumbs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGNotificationBreadcrumbs.m; sourceTree = ""; }; 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationBreadcrumbTests.m; sourceTree = ""; }; @@ -1698,6 +1703,8 @@ E701FAAA2490EFD9008D842F /* EventApiValidationTest.m */, 00E636C324878FFC006CBF1A /* Info.plist */, 008966D32486D43700DC48C2 /* KSCrash */, + 016875C4258D003200DFFF69 /* NSUserDefaultsStub.h */, + 016875C5258D003200DFFF69 /* NSUserDefaultsStub.m */, 0163BF5825823D8D008DC28B /* NotificationBreadcrumbTests.m */, 008966A92486D43400DC48C2 /* RegisterErrorDataTest.m */, 01B14C55251CE55F00118748 /* report-react-native-promise-rejection.json */, @@ -2583,6 +2590,7 @@ 0089676F2486D43700DC48C2 /* NSError+SimpleConstructor_Tests.m in Sources */, 008966F42486D43700DC48C2 /* BugsnagThreadTest.m in Sources */, 008967692486D43700DC48C2 /* BugsnagSessionTrackerTest.m in Sources */, + 016875C6258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */, 008967842486D43700DC48C2 /* KSDynamicLinker_Tests.m in Sources */, 008967032486D43700DC48C2 /* BugsnagThreadSerializationTest.m in Sources */, 0089674B2486D43700DC48C2 /* BSGConnectivityTest.m in Sources */, @@ -2739,6 +2747,7 @@ 008967A62486D43700DC48C2 /* KSString_Tests.m in Sources */, 004E353D2487B3B8007FBAE4 /* BugsnagSwiftTests.swift in Sources */, 008967192486D43700DC48C2 /* BugsnagErrorTest.m in Sources */, + 016875C7258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */, 008967162486D43700DC48C2 /* BugsnagCollectionsBSGDictMergeTest.m in Sources */, 008967582486D43700DC48C2 /* BugsnagClientMirrorTest.m in Sources */, 0089676A2486D43700DC48C2 /* BugsnagSessionTrackerTest.m in Sources */, @@ -2940,6 +2949,7 @@ 008967322486D43700DC48C2 /* BugsnagStateEventTest.m in Sources */, CBA2249D251E429C00B87416 /* TestSupport.m in Sources */, 004E35372487AFF2007FBAE4 /* BugsnagHandledStateTest.m in Sources */, + 016875C8258D003200DFFF69 /* NSUserDefaultsStub.m in Sources */, 0089678C2486D43700DC48C2 /* KSCrashReportStore_Tests.m in Sources */, 01C17AE92542ED7F00C102C9 /* KSCrashReportWriterTests.m in Sources */, 00896A422486DBDD00DC48C2 /* BSGConfigurationBuilderTests.m in Sources */, diff --git a/Bugsnag/Configuration/BugsnagConfiguration+Private.h b/Bugsnag/Configuration/BugsnagConfiguration+Private.h index 3c28de622..d40fed2ce 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration+Private.h +++ b/Bugsnag/Configuration/BugsnagConfiguration+Private.h @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Properties +/// The user defaults database to use for persistence of user information. +@property (class, nonatomic) NSUserDefaults *userDefaults; + /// Meta-information about the state of Bugsnag @property (retain, nullable) BugsnagMetadata *config; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index c89fca0df..9d1056b07 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -51,6 +51,12 @@ @implementation BugsnagConfiguration +static NSUserDefaults *userDefaults; + ++ (void)initialize { + userDefaults = NSUserDefaults.standardUserDefaults; +} + + (instancetype _Nonnull)loadConfig { NSDictionary *options = [[NSBundle mainBundle] infoDictionary][@"bugsnag"]; return [BSGConfigurationBuilder configurationFromOptions:options]; @@ -127,6 +133,14 @@ + (BOOL)isValidApiKey:(NSString *)apiKey { return isHex && [apiKey length] == BSGApiKeyLength; } ++ (void)setUserDefaults:(NSUserDefaults *)newValue { + userDefaults = newValue; +} + ++ (NSUserDefaults *)userDefaults { + return userDefaults; +} + // ----------------------------------------------------------------------------- // MARK: - Initializers // ----------------------------------------------------------------------------- @@ -355,7 +369,6 @@ - (void)setPersistUser:(BOOL)persistUser { */ - (BugsnagUser *)getPersistedUserData { @synchronized(self) { - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; NSString *email = [userDefaults objectForKey:kBugsnagUserEmailAddress]; NSString *name = [userDefaults objectForKey:kBugsnagUserName]; NSString *userId = [userDefaults objectForKey:kBugsnagUserUserId]; @@ -375,8 +388,6 @@ - (BugsnagUser *)getPersistedUserData { - (void)persistUserData { @synchronized(self) { if (_user) { - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; - // Email if (_user.email) { [userDefaults setObject:_user.email forKey:kBugsnagUserEmailAddress]; @@ -409,7 +420,6 @@ - (void)persistUserData { */ -(void)deletePersistedUserData { @synchronized(self) { - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults removeObjectForKey:kBugsnagUserEmailAddress]; [userDefaults removeObjectForKey:kBugsnagUserName]; [userDefaults removeObjectForKey:kBugsnagUserUserId]; diff --git a/Makefile b/Makefile index d0297ce51..f0ca49410 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ else DESTINATION?=platform=tvOS Simulator,name=Apple TV,OS=$(OS) else SDK?=iphonesimulator - DEVICE?=iPhone 5s + DEVICE?=iPhone 8 DESTINATION?=platform=iOS Simulator,name=$(DEVICE),OS=$(OS) RELEASE_DIR=Release-iphoneos endif @@ -92,6 +92,8 @@ analyze: ## Run static analysis on the build and fail if issues found && [[ -z `find $(DATA_PATH)/analyzer -name "*.html"` ]] test: ## Run unit tests + @sw_vers + @$(XCODEBUILD) -version @$(XCODEBUILD) $(BUILD_FLAGS) $(BUILD_ONLY_FLAGS) test $(FORMATTER) test-fixtures: ## Build the end-to-end test fixture @@ -160,5 +162,19 @@ doc: ## Generate html documentation @gatherheaderdoc docs @mv docs/masterTOC.html docs/index.html +update-docs: ## Update and upload docs to Github +ifeq ($(BUILDKITE),) + @$(error Docs deployment is handled by CI, and shouldn't be run locally) +endif +ifeq ($(BUILDKITE_TAG),) + @$(error Docs deployments should only occur on a tagged release) +endif + @git clone --single-branch --branch=gh-pages git@github.com:bugsnag/bugsnag-cocoa.git docs + @make doc + @cd docs + @git add . + @git commit -m "Docs update for $BUILDKITE_TAG release" + @git push --force-with-lease + help: ## Show help text @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index 99134cca2..53b67390f 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -351,7 +351,7 @@ - (void)testSetMalformedNotifyEndpoint { // Helper - (void)getName:(NSString **)name email:(NSString **)email id:(NSString ** )id { - NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + NSUserDefaults *userDefaults = BugsnagConfiguration.userDefaults; *email = [userDefaults objectForKey:kBugsnagUserEmailAddress]; *id = [userDefaults objectForKey:kBugsnagUserUserId]; *name = [userDefaults objectForKey:kBugsnagUserName]; diff --git a/Tests/NSUserDefaultsStub.h b/Tests/NSUserDefaultsStub.h new file mode 100644 index 000000000..544ed703d --- /dev/null +++ b/Tests/NSUserDefaultsStub.h @@ -0,0 +1,17 @@ +// +// NSUserDefaultsStub.h +// Bugsnag +// +// Created by Nick Dowell on 18/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSUserDefaultsStub : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/NSUserDefaultsStub.m b/Tests/NSUserDefaultsStub.m new file mode 100644 index 000000000..757deffbb --- /dev/null +++ b/Tests/NSUserDefaultsStub.m @@ -0,0 +1,41 @@ +// +// NSUserDefaultsStub.m +// Bugsnag +// +// Created by Nick Dowell on 18/12/2020. +// Copyright © 2020 Bugsnag Inc. All rights reserved. +// + +#import "NSUserDefaultsStub.h" + +#import "BugsnagConfiguration+Private.h" + + +@implementation NSUserDefaultsStub { + NSMutableDictionary *_storage; +} + ++ (void)load { + BugsnagConfiguration.userDefaults = (id)[[self alloc] init]; +} + +- (instancetype)init { + if ((self = [super init])) { + _storage = [NSMutableDictionary dictionary]; + } + return self; +} + +- (id)objectForKey:(NSString *)defaultName { + return [_storage objectForKey:defaultName]; +} + +- (void)removeObjectForKey:(NSString *)defaultName { + [_storage removeObjectForKey:defaultName]; +} + +- (void)setObject:(id)value forKey:(NSString *)defaultName { + [_storage setObject:value forKey:defaultName]; +} + +@end From 41cce0c60129a4f1d90b73c9534f36165e422fd4 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Fri, 18 Dec 2020 16:19:57 +0000 Subject: [PATCH 20/33] Remove uses of [Bugsnag configuration] --- Bugsnag/BugsnagCrashSentry.m | 2 +- Bugsnag/BugsnagErrorReportSink+Private.h | 2 + Bugsnag/BugsnagErrorReportSink.h | 13 +++++-- Bugsnag/BugsnagErrorReportSink.m | 38 ++++++++++--------- Bugsnag/Configuration/BugsnagConfiguration.m | 18 +++++++++ .../Delivery/BugsnagErrorReportApiClient.m | 5 ++- .../BugsnagSessionTrackingApiClient.m | 2 +- Bugsnag/Payload/BugsnagEvent.m | 6 +-- 8 files changed, 56 insertions(+), 30 deletions(-) diff --git a/Bugsnag/BugsnagCrashSentry.m b/Bugsnag/BugsnagCrashSentry.m index 7233bdb58..0f6d3d6a8 100644 --- a/Bugsnag/BugsnagCrashSentry.m +++ b/Bugsnag/BugsnagCrashSentry.m @@ -22,7 +22,7 @@ - (void)install:(BugsnagConfiguration *)config apiClient:(BugsnagErrorReportApiClient *)apiClient onCrash:(BSGReportCallback)onCrash { - BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient]; + BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient configuration:config]; BSG_KSCrash *ksCrash = [BSG_KSCrash sharedInstance]; ksCrash.sink = sink; ksCrash.introspectMemory = YES; diff --git a/Bugsnag/BugsnagErrorReportSink+Private.h b/Bugsnag/BugsnagErrorReportSink+Private.h index 306c06ac5..1a4994be8 100644 --- a/Bugsnag/BugsnagErrorReportSink+Private.h +++ b/Bugsnag/BugsnagErrorReportSink+Private.h @@ -8,6 +8,8 @@ #import "BugsnagErrorReportSink.h" +@class BugsnagEvent; + NS_ASSUME_NONNULL_BEGIN @interface BugsnagErrorReportSink () diff --git a/Bugsnag/BugsnagErrorReportSink.h b/Bugsnag/BugsnagErrorReportSink.h index 966ff928a..ecf6a4712 100644 --- a/Bugsnag/BugsnagErrorReportSink.h +++ b/Bugsnag/BugsnagErrorReportSink.h @@ -25,16 +25,21 @@ // #import -#import "BSG_KSCrash.h" -#import "BugsnagErrorReportApiClient.h" + +#import "BSGOnErrorSentBlock.h" + +@class BugsnagConfiguration; +@class BugsnagErrorReportApiClient; NS_ASSUME_NONNULL_BEGIN @interface BugsnagErrorReportSink : NSObject -@property(nonatomic, strong) BugsnagErrorReportApiClient *apiClient; +- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient configuration:(BugsnagConfiguration *)configuration; + +@property (strong, nonatomic) BugsnagErrorReportApiClient *apiClient; -- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient; +@property (strong, nonatomic) BugsnagConfiguration *configuration; /** * Invoked when reports stored by KSCrash need to be delivered. diff --git a/Bugsnag/BugsnagErrorReportSink.m b/Bugsnag/BugsnagErrorReportSink.m index 7fcb5e49b..302bdab0d 100644 --- a/Bugsnag/BugsnagErrorReportSink.m +++ b/Bugsnag/BugsnagErrorReportSink.m @@ -31,6 +31,7 @@ #import "BugsnagClient+Private.h" #import "BugsnagCollections.h" #import "BugsnagConfiguration+Private.h" +#import "BugsnagErrorReportApiClient.h" #import "BugsnagEvent+Private.h" #import "BugsnagKeys.h" #import "BugsnagLogger.h" @@ -42,10 +43,11 @@ @interface BugsnagErrorReportSink () @implementation BugsnagErrorReportSink -- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient { - if (self = [super init]) { - self.apiClient = apiClient; - self.activeRequests = [NSMutableSet new]; +- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient configuration:(BugsnagConfiguration *)configuration { + if ((self = [super init])) { + _apiClient = apiClient; + _activeRequests = [NSMutableSet new]; + _configuration = configuration; } return self; } @@ -86,7 +88,6 @@ - (void)sendStoredReports:(NSDictionary *)ksCrashRe // 4. When a request has completed and deleted the file, remove the files from the dictionary NSArray *keys = [self prepareNewRequests:[ksCrashReports allKeys]]; NSMutableDictionary* storedEvents = [NSMutableDictionary new]; - BugsnagConfiguration *configuration = [Bugsnag configuration]; // run user callbacks on events before enqueueing any requests, as // this way events can be discarded quickly. This frees up disk @@ -94,35 +95,33 @@ - (void)sendStoredReports:(NSDictionary *)ksCrashRe for (NSString *fileKey in keys) { NSDictionary *report = ksCrashReports[fileKey]; BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:report]; - event.redactedKeys = configuration.redactedKeys; + event.redactedKeys = self.configuration.redactedKeys; NSString *errorClass = event.errors.firstObject.errorClass; - if ([configuration shouldDiscardErrorClass:errorClass]) { + if ([self.configuration shouldDiscardErrorClass:errorClass]) { bsg_log_info(@"Discarding event because errorClass \"%@\" matched configuration.discardClasses", errorClass); [self finishActiveRequest:fileKey completed:YES error:nil block:block]; continue; } - - if ([event shouldBeSent] && [self runOnSendBlocks:configuration event:event]) { + + if (self.configuration.shouldSendReports && [event shouldBeSent] && [self runOnSendBlocksForEvent:event]) { storedEvents[fileKey] = event; } else { // delete the report as the user has discarded it [self finishActiveRequest:fileKey completed:YES error:nil block:block]; } } - [self deliverStoredEvents:storedEvents configuration:configuration block:block]; + [self deliverStoredEvents:storedEvents block:block]; } -- (void)deliverStoredEvents:(NSMutableDictionary *)storedEvents - configuration:(BugsnagConfiguration *)configuration - block:(BSGOnErrorSentBlock)block { +- (void)deliverStoredEvents:(NSMutableDictionary *)storedEvents block:(BSGOnErrorSentBlock)block { for (NSString *filename in storedEvents) { BugsnagEvent *event = storedEvents[filename]; NSDictionary *requestPayload = [self prepareEventPayload:event]; - NSMutableDictionary *apiHeaders = [[configuration errorApiHeaders] mutableCopy]; + NSMutableDictionary *apiHeaders = [self.configuration.errorApiHeaders mutableCopy]; apiHeaders[BugsnagHTTPHeaderNameApiKey] = event.apiKey; apiHeaders[BugsnagHTTPHeaderNameStacktraceTypes] = [event.stacktraceTypes componentsJoinedByString:@","]; - [self.apiClient sendJSONPayload:requestPayload headers:apiHeaders toURL:configuration.notifyURL + [self.apiClient sendJSONPayload:requestPayload headers:apiHeaders toURL:self.configuration.notifyURL completionHandler:^(BugsnagApiClientDeliveryStatus status, NSError *error) { BOOL completed = status == BugsnagApiClientDeliveryStatusDelivered || status == BugsnagApiClientDeliveryStatusUndeliverable; [self finishActiveRequest:filename completed:completed error:error block:block]; @@ -130,9 +129,8 @@ - (void)deliverStoredEvents:(NSMutableDictionary *)s } } -- (BOOL)runOnSendBlocks:(BugsnagConfiguration *)configuration - event:(BugsnagEvent *)event { - for (BugsnagOnSendErrorBlock onSendErrorBlock in configuration.onSendBlocks) { +- (BOOL)runOnSendBlocksForEvent:(BugsnagEvent *)event { + for (BugsnagOnSendErrorBlock onSendErrorBlock in self.configuration.onSendBlocks) { @try { if (!onSendErrorBlock(event)) { return false; @@ -150,6 +148,10 @@ - (BOOL)runOnSendBlocks:(BugsnagConfiguration *)configuration * @return an Error Reporting API payload represented as a serializable dictionary */ - (NSDictionary *)prepareEventPayload:(BugsnagEvent *)event { + if (!event.app.type) { + // Use current value for crashes from older notifier versions that didn't persist config.appType + event.app.type = self.configuration.appType; + } NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; data[BSGKeyNotifier] = [[Bugsnag client].notifier toDict]; data[BSGKeyApiKey] = event.apiKey; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index 9d1056b07..cfa7b99ce 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -228,6 +228,7 @@ - (instancetype)initWithMetadata:(NSDictionary *)metadata { if (!(self = [super init])) { return nil; } + _appType = metadata[BSGKeyAppType]; _appVersion = metadata[BSGKeyAppVersion]; _context = metadata[BSGKeyContext]; _bundleVersion = metadata[BSGKeyBundleVersion]; @@ -619,6 +620,23 @@ - (void)setContext:(NSString *)newContext { // MARK: - +@synthesize appType = _appType; + +- (NSString *)appType { + @synchronized (self) { + return _appType; + } +} + +- (void)setAppType:(NSString *)appType { + @synchronized (self) { + _appType = [appType copy]; + [self.config addMetadata:appType withKey:BSGKeyAppType toSection:BSGKeyConfig]; + } +} + +// MARK: - + @synthesize appVersion = _appVersion; - (NSString *)appVersion { diff --git a/Bugsnag/Delivery/BugsnagErrorReportApiClient.m b/Bugsnag/Delivery/BugsnagErrorReportApiClient.m index b9bb89a90..859e6b00a 100644 --- a/Bugsnag/Delivery/BugsnagErrorReportApiClient.m +++ b/Bugsnag/Delivery/BugsnagErrorReportApiClient.m @@ -7,11 +7,14 @@ // #import "BugsnagErrorReportApiClient.h" + +#import "BSG_KSCrash.h" #import "Bugsnag.h" -#import "BugsnagLogger.h" #import "BugsnagClient.h" #import "BugsnagErrorReportSink.h" #import "BugsnagKeys.h" +#import "BugsnagLogger.h" + @interface BSGDeliveryOperation : NSOperation @end diff --git a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m index d8f446762..83a89e0b2 100644 --- a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m +++ b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m @@ -61,7 +61,7 @@ - (void)deliverSessionsInStore:(BugsnagSessionFileStore *)store { [self.sendQueue addOperationWithBlock:^{ BugsnagSessionTrackingPayload *payload = [[BugsnagSessionTrackingPayload alloc] initWithSessions:@[session] - config:[Bugsnag configuration] + config:self.config codeBundleId:self.codeBundleId]; NSMutableDictionary *data = [payload toJson]; NSDictionary *HTTPHeaders = @{ diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 7c5d5984e..d64ad4a26 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -358,9 +358,6 @@ - (instancetype)initWithKSCrashData:(NSDictionary *)event { BugsnagUser *user = [self parseUser:event deviceAppHash:deviceAppHash deviceId:device.id]; BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithMetadata:[event valueForKeyPath:@"user.config"]]; BugsnagAppWithState *app = [BugsnagAppWithState appWithDictionary:event config:config codeBundleId:self.codeBundleId]; - if (!app.type) { // Configuration.type does not get stored in the crash report at the time of writing. - app.type = [Bugsnag configuration].appType; - } BugsnagEvent *obj = [self initWithApp:app device:device handledState:handledState @@ -496,8 +493,7 @@ - (void)setApiKey:(NSString *)apiKey { - (BOOL)shouldBeSent { return [self.enabledReleaseStages containsObject:self.releaseStage] || - (self.enabledReleaseStages.count == 0 && - [[Bugsnag configuration] shouldSendReports]); + (self.enabledReleaseStages.count == 0); } - (NSArray *)serializeBreadcrumbs { From a7ed0378d76ecffcb2a05a45a34b95e1bdfbb936 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 11:15:28 +0000 Subject: [PATCH 21/33] Remove uses of [Bugsnag client] --- Bugsnag/BugsnagCrashSentry.h | 3 +++ Bugsnag/BugsnagCrashSentry.m | 3 ++- Bugsnag/BugsnagErrorReportSink.h | 7 ++++++- Bugsnag/BugsnagErrorReportSink.m | 7 +++++-- Bugsnag/BugsnagSessionTracker.m | 4 ++-- Bugsnag/Client/BugsnagClient.m | 4 +--- .../Delivery/BugsnagSessionTrackingApiClient.h | 5 ++++- .../Delivery/BugsnagSessionTrackingApiClient.m | 7 ++++--- Bugsnag/Payload/BugsnagSessionTrackingPayload.h | 4 +++- Bugsnag/Payload/BugsnagSessionTrackingPayload.m | 5 ++++- Tests/BugsnagErrorReportSinkTests.m | 7 ++++++- Tests/BugsnagSessionTrackingPayloadTest.m | 15 ++++++++++----- 12 files changed, 50 insertions(+), 21 deletions(-) diff --git a/Bugsnag/BugsnagCrashSentry.h b/Bugsnag/BugsnagCrashSentry.h index 129f5dd8a..863f02958 100644 --- a/Bugsnag/BugsnagCrashSentry.h +++ b/Bugsnag/BugsnagCrashSentry.h @@ -12,10 +12,13 @@ #import "BugsnagConfiguration.h" #import "BugsnagErrorReportApiClient.h" +@class BugsnagNotifier; + @interface BugsnagCrashSentry : NSObject - (void)install:(BugsnagConfiguration *)config apiClient:(BugsnagErrorReportApiClient *)apiClient + notifier:(BugsnagNotifier *)notifier onCrash:(BSGReportCallback)onCrash; - (void)reportUserException:(NSString *)reportName diff --git a/Bugsnag/BugsnagCrashSentry.m b/Bugsnag/BugsnagCrashSentry.m index 0f6d3d6a8..4b9935505 100644 --- a/Bugsnag/BugsnagCrashSentry.m +++ b/Bugsnag/BugsnagCrashSentry.m @@ -20,9 +20,10 @@ @implementation BugsnagCrashSentry - (void)install:(BugsnagConfiguration *)config apiClient:(BugsnagErrorReportApiClient *)apiClient + notifier:(BugsnagNotifier *)notifier onCrash:(BSGReportCallback)onCrash { - BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient configuration:config]; + BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient configuration:config notifier:notifier]; BSG_KSCrash *ksCrash = [BSG_KSCrash sharedInstance]; ksCrash.sink = sink; ksCrash.introspectMemory = YES; diff --git a/Bugsnag/BugsnagErrorReportSink.h b/Bugsnag/BugsnagErrorReportSink.h index ecf6a4712..2339e1bb8 100644 --- a/Bugsnag/BugsnagErrorReportSink.h +++ b/Bugsnag/BugsnagErrorReportSink.h @@ -30,17 +30,22 @@ @class BugsnagConfiguration; @class BugsnagErrorReportApiClient; +@class BugsnagNotifier; NS_ASSUME_NONNULL_BEGIN @interface BugsnagErrorReportSink : NSObject -- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient configuration:(BugsnagConfiguration *)configuration; +- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient + configuration:(BugsnagConfiguration *)configuration + notifier:(BugsnagNotifier *)notifier; @property (strong, nonatomic) BugsnagErrorReportApiClient *apiClient; @property (strong, nonatomic) BugsnagConfiguration *configuration; +@property (strong, nonatomic) BugsnagNotifier *notifier; + /** * Invoked when reports stored by KSCrash need to be delivered. * diff --git a/Bugsnag/BugsnagErrorReportSink.m b/Bugsnag/BugsnagErrorReportSink.m index 302bdab0d..cae40457d 100644 --- a/Bugsnag/BugsnagErrorReportSink.m +++ b/Bugsnag/BugsnagErrorReportSink.m @@ -43,11 +43,14 @@ @interface BugsnagErrorReportSink () @implementation BugsnagErrorReportSink -- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient configuration:(BugsnagConfiguration *)configuration { +- (instancetype)initWithApiClient:(BugsnagErrorReportApiClient *)apiClient + configuration:(BugsnagConfiguration *)configuration + notifier:(BugsnagNotifier *)notifier { if ((self = [super init])) { _apiClient = apiClient; _activeRequests = [NSMutableSet new]; _configuration = configuration; + _notifier = notifier; } return self; } @@ -153,7 +156,7 @@ - (NSDictionary *)prepareEventPayload:(BugsnagEvent *)event { event.app.type = self.configuration.appType; } NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; - data[BSGKeyNotifier] = [[Bugsnag client].notifier toDict]; + data[BSGKeyNotifier] = [self.notifier toDict]; data[BSGKeyApiKey] = event.apiKey; data[BSGKeyPayloadVersion] = @"4.0"; data[BSGKeyEvents] = @[[event toJson]]; diff --git a/Bugsnag/BugsnagSessionTracker.m b/Bugsnag/BugsnagSessionTracker.m index 75761cb71..06db4fc8f 100644 --- a/Bugsnag/BugsnagSessionTracker.m +++ b/Bugsnag/BugsnagSessionTracker.m @@ -9,7 +9,7 @@ #import "BugsnagSessionTracker+Private.h" #import "BugsnagApp+Private.h" -#import "BugsnagClient.h" +#import "BugsnagClient+Private.h" #import "BugsnagConfiguration+Private.h" #import "BugsnagDevice+Private.h" #import "BugsnagSessionFileStore.h" @@ -51,7 +51,7 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config if (self = [super init]) { _config = config; _client = client; - _apiClient = [[BugsnagSessionTrackingApiClient alloc] initWithConfig:config queueName:@"Session API queue"]; + _apiClient = [[BugsnagSessionTrackingApiClient alloc] initWithConfig:config queueName:@"Session API queue" notifier:client.notifier]; _callback = callback; NSString *storePath = [BugsnagFileStore findReportStorePath:@"Sessions"]; diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 55cae2cbf..401590523 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -349,9 +349,7 @@ - (void)notifyObservers:(BugsnagStateEvent *)event { - (void)start { [self.configuration validate]; - [self.crashSentry install:self.configuration - apiClient:self.errorReportApiClient - onCrash:&BSSerializeDataCrashHandler]; + [self.crashSentry install:self.configuration apiClient:self.errorReportApiClient notifier:self.notifier onCrash:&BSSerializeDataCrashHandler]; [self.systemState recordAppUUID]; // Needs to be called after crashSentry installed but before -computeDidCrashLastLaunch [self computeDidCrashLastLaunch]; [self.breadcrumbs removeAllBreadcrumbs]; diff --git a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.h b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.h index 08dc7dde5..abe1b71ba 100644 --- a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.h +++ b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.h @@ -7,11 +7,12 @@ #import "BugsnagApiClient.h" @class BugsnagConfiguration; +@class BugsnagNotifier; @class BugsnagSessionFileStore; @interface BugsnagSessionTrackingApiClient : BugsnagApiClient -- (instancetype)initWithConfig:(BugsnagConfiguration *)configuration queueName:(NSString *)queueName; +- (instancetype)initWithConfig:(BugsnagConfiguration *)configuration queueName:(NSString *)queueName notifier:(BugsnagNotifier *)notifier; /** * Asynchronously delivers sessions written to the store @@ -22,4 +23,6 @@ @property (copy) NSString *codeBundleId; +@property BugsnagNotifier *notifier; + @end diff --git a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m index 83a89e0b2..f30e3394f 100644 --- a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m +++ b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m @@ -5,7 +5,6 @@ #import "BugsnagSessionTrackingApiClient.h" -#import "Bugsnag+Private.h" #import "BugsnagConfiguration+Private.h" #import "BugsnagSessionTrackingPayload.h" #import "BugsnagSessionFileStore.h" @@ -22,10 +21,11 @@ @interface BugsnagSessionTrackingApiClient () @implementation BugsnagSessionTrackingApiClient -- (instancetype)initWithConfig:(BugsnagConfiguration *)configuration queueName:(NSString *)queueName { +- (instancetype)initWithConfig:(BugsnagConfiguration *)configuration queueName:(NSString *)queueName notifier:(BugsnagNotifier *)notifier { if ((self = [super initWithSession:configuration.session queueName:queueName])) { _activeIds = [NSMutableSet new]; _config = configuration; + _notifier = notifier; } return self; } @@ -62,7 +62,8 @@ - (void)deliverSessionsInStore:(BugsnagSessionFileStore *)store { BugsnagSessionTrackingPayload *payload = [[BugsnagSessionTrackingPayload alloc] initWithSessions:@[session] config:self.config - codeBundleId:self.codeBundleId]; + codeBundleId:self.codeBundleId + notifier:self.notifier]; NSMutableDictionary *data = [payload toJson]; NSDictionary *HTTPHeaders = @{ BugsnagHTTPHeaderNameApiKey: apiKey ?: @"", diff --git a/Bugsnag/Payload/BugsnagSessionTrackingPayload.h b/Bugsnag/Payload/BugsnagSessionTrackingPayload.h index 21034135c..17420d96a 100644 --- a/Bugsnag/Payload/BugsnagSessionTrackingPayload.h +++ b/Bugsnag/Payload/BugsnagSessionTrackingPayload.h @@ -10,6 +10,7 @@ #import "BugsnagSession.h" @class BugsnagConfiguration; +@class BugsnagNotifier; @interface BugsnagSessionTrackingPayload : NSObject @@ -17,7 +18,8 @@ - (instancetype)initWithSessions:(NSArray *)sessions config:(BugsnagConfiguration *)config - codeBundleId:(NSString *)codeBundleId; + codeBundleId:(NSString *)codeBundleId + notifier:(BugsnagNotifier *)notifier; - (NSMutableDictionary *)toJson; diff --git a/Bugsnag/Payload/BugsnagSessionTrackingPayload.m b/Bugsnag/Payload/BugsnagSessionTrackingPayload.m index 28721909b..5e6c43011 100644 --- a/Bugsnag/Payload/BugsnagSessionTrackingPayload.m +++ b/Bugsnag/Payload/BugsnagSessionTrackingPayload.m @@ -23,6 +23,7 @@ @interface BugsnagSessionTrackingPayload () @property (nonatomic) BugsnagConfiguration *config; @property(nonatomic, copy) NSString *codeBundleId; +@property (nonatomic) BugsnagNotifier *notifier; @end @implementation BugsnagSessionTrackingPayload @@ -30,11 +31,13 @@ @implementation BugsnagSessionTrackingPayload - (instancetype)initWithSessions:(NSArray *)sessions config:(BugsnagConfiguration *)config codeBundleId:(NSString *)codeBundleId + notifier:(BugsnagNotifier *)notifier { if (self = [super init]) { _sessions = sessions; _config = config; _codeBundleId = codeBundleId; + _notifier = notifier; } return self; } @@ -48,7 +51,7 @@ - (NSMutableDictionary *)toJson [sessionData addObject:[session toDictionary]]; } dict[@"sessions"] = sessionData; - dict[BSGKeyNotifier] = [[Bugsnag client].notifier toDict]; + dict[BSGKeyNotifier] = [self.notifier toDict]; // app/device data collection relies on KSCrash reports, // need to mimic the JSON structure here diff --git a/Tests/BugsnagErrorReportSinkTests.m b/Tests/BugsnagErrorReportSinkTests.m index c487c0d7a..298b8ec51 100644 --- a/Tests/BugsnagErrorReportSinkTests.m +++ b/Tests/BugsnagErrorReportSinkTests.m @@ -49,7 +49,12 @@ - (void)setUp { [client start]; BugsnagEvent *report = [[BugsnagEvent alloc] initWithKSReport:self.rawReportData]; - self.processedData = [[BugsnagErrorReportSink new] prepareEventPayload:report]; + + BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:client.errorReportApiClient + configuration:client.configuration + notifier:client.notifier]; + + self.processedData = [sink prepareEventPayload:report]; } - (void)tearDown { diff --git a/Tests/BugsnagSessionTrackingPayloadTest.m b/Tests/BugsnagSessionTrackingPayloadTest.m index 3bfe3aa4e..e4dd91b7b 100644 --- a/Tests/BugsnagSessionTrackingPayloadTest.m +++ b/Tests/BugsnagSessionTrackingPayloadTest.m @@ -9,11 +9,12 @@ #import #import "BugsnagApp+Private.h" +#import "BugsnagConfiguration+Private.h" #import "BugsnagDevice+Private.h" +#import "BugsnagNotifier.h" +#import "BugsnagSession+Private.h" #import "BugsnagSessionTrackingPayload.h" -#import "BugsnagConfiguration+Private.h" #import "BugsnagTestConstants.h" -#import "BugsnagSession+Private.h" @interface BugsnagSessionTrackingPayloadTest : XCTestCase @property NSDictionary *payload; @@ -29,15 +30,19 @@ - (void)setUp { BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; config.releaseStage = @"beta"; - BugsnagSessionTrackingPayload *data = [[BugsnagSessionTrackingPayload alloc] initWithSessions:@[] config:config codeBundleId:nil]; + + BugsnagSessionTrackingPayload *payload = [[BugsnagSessionTrackingPayload alloc] initWithSessions:@[] + config:config + codeBundleId:nil + notifier:[[BugsnagNotifier alloc] init]]; BugsnagSession *session = [[BugsnagSession alloc] initWithId:@"test" startDate:[NSDate date] user:nil autoCaptured:NO app:self.app device:self.device]; - data.sessions = @[session]; - self.payload = [data toJson]; + payload.sessions = @[session]; + self.payload = [payload toJson]; } - (BugsnagApp *)generateApp { From 7a901502faa9d1990801425e273740ff3aa4c7af Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 15:02:34 +0000 Subject: [PATCH 22/33] Allow BugsnagErrorReportSinkTests to be run in isolation --- Bugsnag/BugsnagErrorReportSink.m | 3 +++ Bugsnag/Configuration/BugsnagConfiguration.m | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Bugsnag/BugsnagErrorReportSink.m b/Bugsnag/BugsnagErrorReportSink.m index cae40457d..60a21423c 100644 --- a/Bugsnag/BugsnagErrorReportSink.m +++ b/Bugsnag/BugsnagErrorReportSink.m @@ -155,6 +155,9 @@ - (NSDictionary *)prepareEventPayload:(BugsnagEvent *)event { // Use current value for crashes from older notifier versions that didn't persist config.appType event.app.type = self.configuration.appType; } + if (!event.apiKey) { + event.apiKey = self.configuration.apiKey; + } NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; data[BSGKeyNotifier] = [self.notifier toDict]; data[BSGKeyApiKey] = event.apiKey; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index cfa7b99ce..b0eea1366 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -99,7 +99,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { [copy setPersistUser:self.persistUser]; [copy setPlugins:[self.plugins copy]]; [copy setReleaseStage:self.releaseStage]; - [copy setSession:[self.session copy]]; + copy.session = self.session; // NSURLSession does not declare conformance to NSCopying [copy setSendThreads:self.sendThreads]; [copy setUser:self.user.id withEmail:self.user.email From 4cb27eface05c4a99af406b34e54aac187d2d957 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 15:03:04 +0000 Subject: [PATCH 23/33] Fix unit test analyzer warnings --- Tests/BSGConnectivityTest.m | 4 ---- Tests/BugsnagApiClientTest.m | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/BSGConnectivityTest.m b/Tests/BSGConnectivityTest.m index 770f70c24..165d6cf15 100644 --- a/Tests/BSGConnectivityTest.m +++ b/Tests/BSGConnectivityTest.m @@ -43,8 +43,4 @@ - (void)mockMonitorURLWithCallback:(BSGConnectivityChangeBlock)block { usingCallback:block]; } -- (void)simulateConnectivityChangeTo:(SCNetworkReachabilityFlags) flags { - BSGConnectivityCallback(nil, flags, NULL); -} - @end diff --git a/Tests/BugsnagApiClientTest.m b/Tests/BugsnagApiClientTest.m index 80c1c1ee4..c5e8a7ab2 100644 --- a/Tests/BugsnagApiClientTest.m +++ b/Tests/BugsnagApiClientTest.m @@ -82,7 +82,10 @@ - (void)testNotConnectedToInternetError { - (void)testSHA1HashStringWithData { BugsnagApiClient *client = [[BugsnagApiClient alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" XCTAssertNil([client SHA1HashStringWithData:nil]); +#pragma clang diagnostic pop XCTAssertEqualObjects([client SHA1HashStringWithData:[@"{\"foo\":\"bar\"}" dataUsingEncoding:NSUTF8StringEncoding]], @"a5e744d0164540d33b1d7ea616c28f2fa97e754a"); } From 68ec1331c2200aa9b176a8206a6ffeb168277e5f Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 16:52:53 +0000 Subject: [PATCH 24/33] Fix flake in BareboneTestHandledScenario --- .../shared/scenarios/BareboneTestScenarios.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/features/fixtures/shared/scenarios/BareboneTestScenarios.swift b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift index 533ddfb99..9da15efb6 100644 --- a/features/fixtures/shared/scenarios/BareboneTestScenarios.swift +++ b/features/fixtures/shared/scenarios/BareboneTestScenarios.swift @@ -12,6 +12,8 @@ class BareboneTestHandledScenario: Scenario { var onSendErrorCount = 0 var onSessionCount = 0 + var afterSendErrorBlock: (() -> Void)? + override func startBugsnag() { config.addOnBreadcrumb { NSLog("OnBreadcrumb: \"\($0.message)\"") @@ -24,6 +26,10 @@ class BareboneTestHandledScenario: Scenario { config.addOnSendError { NSLog("OnSendError: \"\($0.errors[0].errorClass ?? "")\" \"\($0.errors[0].errorMessage ?? "")\"") self.onSendErrorCount += 1 + if let block = self.afterSendErrorBlock { + DispatchQueue.main.async(execute: block) + self.afterSendErrorBlock = nil + } return true } config.addOnSession { @@ -60,8 +66,10 @@ class BareboneTestHandledScenario: Scenario { return true } - // There is a delay between notify() and an error being sent. - RunLoop.current.run(until: .init(timeIntervalSinceNow: 2)) + self.afterSendErrorBlock = self.afterSendError + } + + func afterSendError() { precondition(onSendErrorCount == 1) Bugsnag.leaveBreadcrumb(withMessage: "About to decode a payload...") From aec5b57ba5749609346fc8f2dfbb119b5a5fd411 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 16:53:59 +0000 Subject: [PATCH 25/33] Fix scenario name (Barebone, not Smoke!) --- features/barebone_tests.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/barebone_tests.feature b/features/barebone_tests.feature index 4ea9b9a62..5bb654f09 100644 --- a/features/barebone_tests.feature +++ b/features/barebone_tests.feature @@ -78,7 +78,7 @@ Feature: Barebone tests And the exception "message" equals "The data couldn’t be read because it isn’t in the correct format." And the exception "type" equals "cocoa" - Scenario: Smoke test: unhandled error + Scenario: Barebone test: unhandled error When I run "BareboneTestUnhandledErrorScenario" and relaunch the app And I set the app to "report" mode And I configure Bugsnag for "BareboneTestUnhandledErrorScenario" From cd71a0e814c0e8dce7d57147fd1a89c71e24de5a Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 21 Dec 2020 16:03:22 +0000 Subject: [PATCH 26/33] Replace BugsnagConfiguration -config with -dictionaryRepresentation --- Bugsnag/Client/BugsnagClient.m | 8 +- .../BugsnagConfiguration+Private.h | 6 +- Bugsnag/Configuration/BugsnagConfiguration.m | 144 +++--------------- Bugsnag/Payload/BugsnagEvent.m | 2 +- Tests/BugsnagAppTest.m | 2 +- Tests/BugsnagConfigurationTests.m | 23 +++ Tests/BugsnagSessionTest.m | 2 +- Tests/BugsnagSessionTrackingPayloadTest.m | 2 +- 8 files changed, 50 insertions(+), 139 deletions(-) diff --git a/Bugsnag/Client/BugsnagClient.m b/Bugsnag/Client/BugsnagClient.m index 401590523..9f1fe013f 100644 --- a/Bugsnag/Client/BugsnagClient.m +++ b/Bugsnag/Client/BugsnagClient.m @@ -281,6 +281,8 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { self.breadcrumbs = [[BugsnagBreadcrumbs alloc] initWithConfiguration:self.configuration]; + [BSGJSONSerialization writeJSONObject:configuration.dictionaryRepresentation toFile:_configMetadataFile options:0 error:nil]; + // Start with a copy of the configuration metadata self.metadata = [[configuration metadata] deepCopy]; // add metadata about app/device @@ -289,7 +291,6 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { [self.metadata addMetadata:BSGParseDeviceMetadata(@{@"system": systemInfo}) toSection:BSGKeyDevice]; // sync initial state [self metadataChanged:self.metadata]; - [self metadataChanged:self.configuration.config]; [self metadataChanged:self.state]; // add observers for future metadata changes @@ -301,7 +302,6 @@ - (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration { [weakSelf metadataChanged:event.data]; }; [self.metadata addObserverWithBlock:observer]; - [self.configuration.config addObserverWithBlock:observer]; [self.state addObserverWithBlock:observer]; self.pluginClient = [[BugsnagPluginClient alloc] initWithPlugins:self.configuration.plugins @@ -920,7 +920,7 @@ - (void)notifyInternal:(BugsnagEvent *_Nonnull)event callbackOverrides:event.overrides eventOverrides:eventOverrides metadata:[event.metadata toDictionary] - config:[self.configuration.config toDictionary]]; + config:self.configuration.dictionaryRepresentation]; // A basic set of event metadata NSMutableDictionary *metadata = [@{ @@ -973,8 +973,6 @@ - (void)metadataChanged:(BugsnagMetadata *)metadata { @synchronized(metadata) { if (metadata == self.metadata) { [BSGJSONSerialization writeJSONObject:[metadata toDictionary] toFile:self.metadataFile options:0 error:nil]; - } else if (metadata == self.configuration.config) { - [BSGJSONSerialization writeJSONObject:[metadata getMetadataFromSection:BSGKeyConfig] toFile:self.configMetadataFile options:0 error:nil]; } else if (metadata == self.state) { [BSGJSONSerialization writeJSONObject:[metadata toDictionary] toFile:self.stateMetadataFile options:0 error:nil]; } diff --git a/Bugsnag/Configuration/BugsnagConfiguration+Private.h b/Bugsnag/Configuration/BugsnagConfiguration+Private.h index d40fed2ce..ad2a2585d 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration+Private.h +++ b/Bugsnag/Configuration/BugsnagConfiguration+Private.h @@ -14,16 +14,14 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Initializers -/// Initializes the configuration with values previously stored in metadata. -- (instancetype)initWithMetadata:(NSDictionary *)JSONObject NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithDictionaryRepresentation:(NSDictionary *)JSONObject NS_DESIGNATED_INITIALIZER; #pragma mark Properties /// The user defaults database to use for persistence of user information. @property (class, nonatomic) NSUserDefaults *userDefaults; -/// Meta-information about the state of Bugsnag -@property (retain, nullable) BugsnagMetadata *config; +@property (readonly) NSDictionary *dictionaryRepresentation; @property (readonly) NSDictionary *errorApiHeaders; diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index b0eea1366..ebeb23883 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -83,7 +83,6 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone { [copy setAutoDetectErrors:self.autoDetectErrors]; [copy setAutoTrackSessions:self.autoTrackSessions]; [copy setBundleVersion:self.bundleVersion]; - [copy setConfig:[[BugsnagMetadata alloc] initWithDictionary:[[self.config toDictionary] mutableCopy]]]; [copy setContext:self.context]; [copy setEnabledBreadcrumbTypes:self.enabledBreadcrumbTypes]; [copy setEnabledErrorTypes:self.enabledErrorTypes]; @@ -164,7 +163,6 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { [self setApiKey:apiKey]; } _metadata = [[BugsnagMetadata alloc] init]; - _config = [[BugsnagMetadata alloc] init]; _endpoints = [BugsnagEndpointConfiguration new]; _sessionURL = [NSURL URLWithString:@"https://sessions.bugsnag.com"]; _autoDetectErrors = YES; @@ -224,16 +222,16 @@ - (instancetype)initWithApiKey:(NSString *)apiKey { return self; } -- (instancetype)initWithMetadata:(NSDictionary *)metadata { +- (instancetype)initWithDictionaryRepresentation:(NSDictionary *)dictionaryRepresentation { if (!(self = [super init])) { return nil; } - _appType = metadata[BSGKeyAppType]; - _appVersion = metadata[BSGKeyAppVersion]; - _context = metadata[BSGKeyContext]; - _bundleVersion = metadata[BSGKeyBundleVersion]; - _enabledReleaseStages = metadata[BSGKeyEnabledReleaseStages]; - _releaseStage = metadata[BSGKeyReleaseStage]; + _appType = dictionaryRepresentation[BSGKeyAppType]; + _appVersion = dictionaryRepresentation[BSGKeyAppVersion]; + _bundleVersion = dictionaryRepresentation[BSGKeyBundleVersion]; + _context = dictionaryRepresentation[BSGKeyContext]; + _enabledReleaseStages = dictionaryRepresentation[BSGKeyEnabledReleaseStages]; + _releaseStage = dictionaryRepresentation[BSGKeyReleaseStage]; return self; } @@ -241,6 +239,17 @@ - (instancetype)initWithMetadata:(NSDictionary *)metadata { // MARK: - Instance Methods // ----------------------------------------------------------------------------- +- (NSDictionary *)dictionaryRepresentation { + NSMutableDictionary *dictionaryRepresentation = [NSMutableDictionary dictionary]; + dictionaryRepresentation[BSGKeyAppType] = self.appType; + dictionaryRepresentation[BSGKeyAppVersion] = self.appVersion; + dictionaryRepresentation[BSGKeyBundleVersion] = self.bundleVersion; + dictionaryRepresentation[BSGKeyContext] = self.context; + dictionaryRepresentation[BSGKeyEnabledReleaseStages] = self.enabledReleaseStages.allObjects; + dictionaryRepresentation[BSGKeyReleaseStage] = self.releaseStage; + return dictionaryRepresentation; +} + /** * Whether reports should be sent, based on release stage options * @@ -540,49 +549,6 @@ - (BOOL)shouldRecordBreadcrumbType:(BSGBreadcrumbType)type { return NO; } -// MARK: - - -@synthesize releaseStage = _releaseStage; - -- (NSString *)releaseStage { - @synchronized (self) { - return _releaseStage; - } -} - -- (void)setReleaseStage:(NSString *)newReleaseStage { - @synchronized (self) { - NSString *key = NSStringFromSelector(@selector(releaseStage)); - [self willChangeValueForKey:key]; - _releaseStage = newReleaseStage; - [self didChangeValueForKey:key]; - [self.config addMetadata:newReleaseStage - withKey:BSGKeyReleaseStage - toSection:BSGKeyConfig]; - } -} - -// MARK: - - -@synthesize enabledReleaseStages = _enabledReleaseStages; - -- (NSSet *)enabledReleaseStages { - @synchronized (self) { - return _enabledReleaseStages; - } -} - -- (void)setEnabledReleaseStages:(NSSet *)newReleaseStages -{ - @synchronized (self) { - NSSet *releaseStagesCopy = [newReleaseStages copy]; - _enabledReleaseStages = releaseStagesCopy; - [self.config addMetadata:[releaseStagesCopy allObjects] - withKey:BSGKeyEnabledReleaseStages - toSection:BSGKeyConfig]; - } -} - // MARK: - enabledBreadcrumbTypes @synthesize enabledBreadcrumbTypes = _enabledBreadcrumbTypes; @@ -601,80 +567,6 @@ - (void)setEnabledBreadcrumbTypes:(BSGEnabledBreadcrumbType)enabledBreadcrumbTyp // MARK: - -@synthesize context = _context; - -- (NSString *)context { - @synchronized (self) { - return _context; - } -} - -- (void)setContext:(NSString *)newContext { - @synchronized (self) { - _context = newContext; - [self.config addMetadata:newContext - withKey:BSGKeyContext - toSection:BSGKeyConfig]; - } -} - -// MARK: - - -@synthesize appType = _appType; - -- (NSString *)appType { - @synchronized (self) { - return _appType; - } -} - -- (void)setAppType:(NSString *)appType { - @synchronized (self) { - _appType = [appType copy]; - [self.config addMetadata:appType withKey:BSGKeyAppType toSection:BSGKeyConfig]; - } -} - -// MARK: - - -@synthesize appVersion = _appVersion; - -- (NSString *)appVersion { - @synchronized (self) { - return _appVersion; - } -} - -- (void)setAppVersion:(NSString *)newVersion { - @synchronized (self) { - _appVersion = newVersion; - [self.config addMetadata:newVersion - withKey:BSGKeyAppVersion - toSection:BSGKeyConfig]; - } -} - -// MARK: - - -@synthesize bundleVersion = _bundleVersion; - -- (NSString *)bundleVersion { - @synchronized (self) { - return _bundleVersion; - } -} - -- (void)setBundleVersion:(NSString *)newVersion { - @synchronized (self) { - _bundleVersion = newVersion; - [self.config addMetadata:newVersion - withKey:BSGKeyBundleVersion - toSection:BSGKeyConfig]; - } -} - -// MARK: - - - (void)validate { if (self.apiKey.length == 0) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason: diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index d64ad4a26..32136bf68 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -356,7 +356,7 @@ - (instancetype)initWithKSCrashData:(NSDictionary *)event { NSString *deviceAppHash = [event valueForKeyPath:@"system.device_app_hash"]; BugsnagDeviceWithState *device = [BugsnagDeviceWithState deviceWithDictionary:event]; BugsnagUser *user = [self parseUser:event deviceAppHash:deviceAppHash deviceId:device.id]; - BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithMetadata:[event valueForKeyPath:@"user.config"]]; + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithDictionaryRepresentation:[event valueForKeyPath:@"user.config"]]; BugsnagAppWithState *app = [BugsnagAppWithState appWithDictionary:event config:config codeBundleId:self.codeBundleId]; BugsnagEvent *obj = [self initWithApp:app device:device diff --git a/Tests/BugsnagAppTest.m b/Tests/BugsnagAppTest.m index a981d4bef..2d47c3658 100644 --- a/Tests/BugsnagAppTest.m +++ b/Tests/BugsnagAppTest.m @@ -45,7 +45,7 @@ - (void)setUp { } }; - self.config = [[BugsnagConfiguration alloc] initWithMetadata:self.data[@"user"][@"config"]]; + self.config = [[BugsnagConfiguration alloc] initWithDictionaryRepresentation:self.data[@"user"][@"config"]]; self.config.appType = @"iOS"; self.config.bundleVersion = nil; self.config.appVersion = @"3.14.159"; diff --git a/Tests/BugsnagConfigurationTests.m b/Tests/BugsnagConfigurationTests.m index 53b67390f..358ddcf96 100644 --- a/Tests/BugsnagConfigurationTests.m +++ b/Tests/BugsnagConfigurationTests.m @@ -671,6 +671,29 @@ - (void)testDefaultConfigurationValues { // MARK: - Other tests // ============================================================================= +- (void)testDictionaryRepresentation { + BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + XCTAssertNotNil(configuration.dictionaryRepresentation[@"appType"]); + XCTAssertNotNil(configuration.dictionaryRepresentation[@"releaseStage"]); + + configuration.appVersion = @"1.2.3"; + XCTAssertEqualObjects(configuration.dictionaryRepresentation[@"appVersion"], @"1.2.3"); + + configuration.bundleVersion = @"2001"; + XCTAssertEqualObjects(configuration.dictionaryRepresentation[@"bundleVersion"], @"2001"); + + XCTAssertNil(configuration.dictionaryRepresentation[@"context"]); + configuration.context = @"lorem ipsum"; + XCTAssertEqualObjects(configuration.dictionaryRepresentation[@"context"], @"lorem ipsum"); + + configuration.releaseStage = @"release"; + XCTAssertEqualObjects(configuration.dictionaryRepresentation[@"releaseStage"], @"release"); + + XCTAssertNil(configuration.dictionaryRepresentation[@"enabledReleaseStages"]); + configuration.enabledReleaseStages = [NSSet setWithArray:@[@"release"]]; + XCTAssertEqualObjects(configuration.dictionaryRepresentation[@"enabledReleaseStages"], @[@"release"]); +} + - (void)testValidateThrowsWhenMissingApiKey { NSString *nilKey = nil; diff --git a/Tests/BugsnagSessionTest.m b/Tests/BugsnagSessionTest.m index a7241a897..87063c05f 100644 --- a/Tests/BugsnagSessionTest.m +++ b/Tests/BugsnagSessionTest.m @@ -51,7 +51,7 @@ - (BugsnagApp *)generateApp { } }; - BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithMetadata:appData[@"user"][@"config"]]; + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithDictionaryRepresentation:appData[@"user"][@"config"]]; config.appType = @"iOS"; config.bundleVersion = nil; return [BugsnagApp appWithDictionary:appData config:config codeBundleId:@"bundle-123"]; diff --git a/Tests/BugsnagSessionTrackingPayloadTest.m b/Tests/BugsnagSessionTrackingPayloadTest.m index e4dd91b7b..314f19464 100644 --- a/Tests/BugsnagSessionTrackingPayloadTest.m +++ b/Tests/BugsnagSessionTrackingPayloadTest.m @@ -66,7 +66,7 @@ - (BugsnagApp *)generateApp { } }; - BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithMetadata:appData[@"user"][@"config"]]; + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithDictionaryRepresentation:appData[@"user"][@"config"]]; config.appType = @"iOS"; config.bundleVersion = nil; return [BugsnagApp appWithDictionary:appData config:config codeBundleId:@"bundle-123"]; From d4c81486ebd67e4b50cf62be4a363855c6e90657 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Tue, 22 Dec 2020 16:19:47 +0000 Subject: [PATCH 27/33] Remove BugsnagClient from BugsnagErrorReportSinkTests --- Bugsnag/Payload/BugsnagEvent.m | 4 ---- Bugsnag/include/Bugsnag/BugsnagEvent.h | 2 +- Tests/BugsnagErrorReportSinkTests.m | 32 +++++++++++++------------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Bugsnag/Payload/BugsnagEvent.m b/Bugsnag/Payload/BugsnagEvent.m index 32136bf68..ff9094b06 100644 --- a/Bugsnag/Payload/BugsnagEvent.m +++ b/Bugsnag/Payload/BugsnagEvent.m @@ -472,13 +472,9 @@ - (NSMutableDictionary *)parseOnCrashData:(NSDictionary *)report { @synthesize apiKey = _apiKey; - (NSString *)apiKey { - if (! _apiKey) { - _apiKey = Bugsnag.configuration.apiKey; - } return _apiKey; } - - (void)setApiKey:(NSString *)apiKey { if ([BugsnagConfiguration isValidApiKey:apiKey]) { _apiKey = apiKey; diff --git a/Bugsnag/include/Bugsnag/BugsnagEvent.h b/Bugsnag/include/Bugsnag/BugsnagEvent.h index a3c85a307..9c07c3c64 100644 --- a/Bugsnag/include/Bugsnag/BugsnagEvent.h +++ b/Bugsnag/include/Bugsnag/BugsnagEvent.h @@ -62,7 +62,7 @@ typedef NS_ENUM(NSUInteger, BSGSeverity) { /** * A per-event override for the apiKey. - * - Reads default to the BugsnagConfiguration apiKey value unless explicitly set. + * - The default value of nil results in the BugsnagConfiguration apiKey being used. * - Writes are not persisted to BugsnagConfiguration. */ @property(readwrite, copy, nullable) NSString *apiKey; diff --git a/Tests/BugsnagErrorReportSinkTests.m b/Tests/BugsnagErrorReportSinkTests.m index 298b8ec51..3fb508626 100644 --- a/Tests/BugsnagErrorReportSinkTests.m +++ b/Tests/BugsnagErrorReportSinkTests.m @@ -8,15 +8,16 @@ #import "BugsnagPlatformConditional.h" -#import +#import #import -#import "Bugsnag.h" -#import "BugsnagClient+Private.h" -#import "BugsnagHandledState.h" +#import "BugsnagErrorReportApiClient.h" #import "BugsnagErrorReportSink+Private.h" #import "BugsnagEvent+Private.h" +#import "BugsnagHandledState.h" +#import "BugsnagNotifier.h" #import "BugsnagTestConstants.h" +#import "URLSessionMock.h" @interface BugsnagErrorReportSinkTests : XCTestCase @property NSDictionary *rawReportData; @@ -27,6 +28,7 @@ @implementation BugsnagErrorReportSinkTests - (void)setUp { [super setUp]; + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *path = [bundle pathForResource:@"report" ofType:@"json"]; NSString *contents = [NSString stringWithContentsOfFile:path @@ -41,20 +43,18 @@ - (void)setUp { // This value should not appear in the assertions, as it is not equal to // the release stage in the serialized report config.releaseStage = @"MagicalTestingTime"; - - // set a dummy endpoint, avoid hitting production - config.endpoints = [[BugsnagEndpointConfiguration alloc] initWithNotify:@"http://localhost:1234" - sessions:@"http://localhost:1234"]; - BugsnagClient *client = [[BugsnagClient alloc] initWithConfiguration:config]; - [client start]; - BugsnagEvent *report = - [[BugsnagEvent alloc] initWithKSReport:self.rawReportData]; - BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:client.errorReportApiClient - configuration:client.configuration - notifier:client.notifier]; + id session = [[URLSessionMock alloc] init]; + + BugsnagErrorReportApiClient *apiClient = [[BugsnagErrorReportApiClient alloc] initWithSession:session queueName:@""]; + + BugsnagNotifier *notifier = [[BugsnagNotifier alloc] init]; + + BugsnagEvent *event = [[BugsnagEvent alloc] initWithKSReport:self.rawReportData]; + + BugsnagErrorReportSink *sink = [[BugsnagErrorReportSink alloc] initWithApiClient:apiClient configuration:config notifier:notifier]; - self.processedData = [sink prepareEventPayload:report]; + self.processedData = [sink prepareEventPayload:event]; } - (void)tearDown { From 0031c3a845b0ba8d9d246649ae7f3093d6033bf2 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 23 Dec 2020 11:55:51 +0000 Subject: [PATCH 28/33] Add [Bugsnag] prefix to bsg_log output --- Bugsnag/Configuration/BugsnagConfiguration.m | 2 +- .../Delivery/BugsnagSessionTrackingApiClient.m | 6 +++--- Bugsnag/Helpers/BugsnagLogger.h | 17 +++++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Bugsnag/Configuration/BugsnagConfiguration.m b/Bugsnag/Configuration/BugsnagConfiguration.m index ebeb23883..001e893a7 100644 --- a/Bugsnag/Configuration/BugsnagConfiguration.m +++ b/Bugsnag/Configuration/BugsnagConfiguration.m @@ -574,7 +574,7 @@ - (void)validate { } if (![BugsnagConfiguration isValidApiKey:self.apiKey]) { - bsg_log_warn(@"Invalid Bugsnag apiKey: expected a 32-character hexademical string, got \"%@\"", self.apiKey); + bsg_log_warn(@"Invalid apiKey: expected a 32-character hexademical string, got \"%@\"", self.apiKey); } } diff --git a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m index f30e3394f..67f783b60 100644 --- a/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m +++ b/Bugsnag/Delivery/BugsnagSessionTrackingApiClient.m @@ -74,14 +74,14 @@ - (void)deliverSessionsInStore:(BugsnagSessionFileStore *)store { completionHandler:^(BugsnagApiClientDeliveryStatus status, NSError *error) { switch (status) { case BugsnagApiClientDeliveryStatusDelivered: - bsg_log_info(@"Sent session %@ to Bugsnag", session.id); + bsg_log_info(@"Sent session %@", session.id); [store deleteFileWithId:fileId]; break; case BugsnagApiClientDeliveryStatusFailed: - bsg_log_warn(@"Failed to send sessions to Bugsnag: %@", error); + bsg_log_warn(@"Failed to send sessions: %@", error); break; case BugsnagApiClientDeliveryStatusUndeliverable: - bsg_log_warn(@"Failed to send sessions to Bugsnag: %@", error); + bsg_log_warn(@"Failed to send sessions: %@", error); [store deleteFileWithId:fileId]; break; } diff --git a/Bugsnag/Helpers/BugsnagLogger.h b/Bugsnag/Helpers/BugsnagLogger.h index f4bb4e833..c1ad6212f 100644 --- a/Bugsnag/Helpers/BugsnagLogger.h +++ b/Bugsnag/Helpers/BugsnagLogger.h @@ -24,9 +24,6 @@ * That file includes this one. No further configuration is required. */ -#ifndef BugsnagLogger_h -#define BugsnagLogger_h - #define BSG_LOGLEVEL_NONE 0 #define BSG_LOGLEVEL_ERR 10 #define BSG_LOGLEVEL_WARN 20 @@ -38,28 +35,32 @@ #define BSG_LOG_LEVEL BSG_LOGLEVEL_INFO #endif +#ifdef __OBJC__ + +#import + #if BSG_LOG_LEVEL >= BSG_LOGLEVEL_ERR -#define bsg_log_err NSLog +#define bsg_log_err(...) NSLog(@"[Bugsnag] [ERROR] " __VA_ARGS__) #else #define bsg_log_err(format, ...) #endif #if BSG_LOG_LEVEL >= BSG_LOGLEVEL_WARN -#define bsg_log_warn NSLog +#define bsg_log_warn(...) NSLog(@"[Bugsnag] [WARN] " __VA_ARGS__) #else #define bsg_log_warn(format, ...) #endif #if BSG_LOG_LEVEL >= BSG_LOGLEVEL_INFO -#define bsg_log_info NSLog +#define bsg_log_info(...) NSLog(@"[Bugsnag] [INFO] " __VA_ARGS__) #else #define bsg_log_info(format, ...) #endif #if BSG_LOG_LEVEL >= BSG_LOGLEVEL_DEBUG -#define bsg_log_debug NSLog +#define bsg_log_debug(...) NSLog(@"[Bugsnag] [DEBUG] " __VA_ARGS__) #else #define bsg_log_debug(format, ...) #endif -#endif /* BugsnagLogger_h */ +#endif From 5651aea93a881f239fbd6ace88d5e42e2c25af37 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 23 Dec 2020 11:56:55 +0000 Subject: [PATCH 29/33] Use BugsnagLogger.h instead of BSG_KSLogger.h in Bugsnag code --- Bugsnag/BugsnagSessionTracker.m | 13 +++++---- .../Configuration/BSGConfigurationBuilder.m | 4 +-- Bugsnag/Helpers/BSGCachesDirectory.m | 9 ++++--- Bugsnag/Storage/BugsnagFileStore.m | 27 ++++++++++--------- Bugsnag/Storage/BugsnagSessionFileStore.m | 4 +-- 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Bugsnag/BugsnagSessionTracker.m b/Bugsnag/BugsnagSessionTracker.m index 06db4fc8f..c110aed6c 100644 --- a/Bugsnag/BugsnagSessionTracker.m +++ b/Bugsnag/BugsnagSessionTracker.m @@ -8,18 +8,17 @@ #import "BugsnagSessionTracker+Private.h" +#import "BSG_KSSystemInfo.h" #import "BugsnagApp+Private.h" #import "BugsnagClient+Private.h" +#import "BugsnagCollections.h" #import "BugsnagConfiguration+Private.h" #import "BugsnagDevice+Private.h" -#import "BugsnagSessionFileStore.h" -#import "BSG_KSLogger.h" -#import "BugsnagSessionTrackingPayload.h" -#import "BugsnagSessionTrackingApiClient.h" #import "BugsnagLogger.h" #import "BugsnagSession+Private.h" -#import "BSG_KSSystemInfo.h" -#import "BugsnagCollections.h" +#import "BugsnagSessionFileStore.h" +#import "BugsnagSessionTrackingApiClient.h" +#import "BugsnagSessionTrackingPayload.h" /** Number of seconds in background required to make a new session @@ -56,7 +55,7 @@ - (instancetype)initWithConfig:(BugsnagConfiguration *)config NSString *storePath = [BugsnagFileStore findReportStorePath:@"Sessions"]; if (!storePath) { - BSG_KSLOG_ERROR(@"Failed to initialize session store."); + bsg_log_err(@"Failed to initialize session store."); } _sessionStore = [BugsnagSessionFileStore storeWithPath:storePath maxPersistedSessions:config.maxPersistedSessions]; diff --git a/Bugsnag/Configuration/BSGConfigurationBuilder.m b/Bugsnag/Configuration/BSGConfigurationBuilder.m index 8574605ec..52312be73 100644 --- a/Bugsnag/Configuration/BSGConfigurationBuilder.m +++ b/Bugsnag/Configuration/BSGConfigurationBuilder.m @@ -1,9 +1,9 @@ #import "BSGConfigurationBuilder.h" -#import "BSG_KSLogger.h" #import "BugsnagConfiguration.h" #import "BugsnagEndpointConfiguration.h" #import "BugsnagKeys.h" +#import "BugsnagLogger.h" static BOOL BSGValueIsBoolean(id object) { return object != nil && [object isKindOfClass:[NSNumber class]] @@ -41,7 +41,7 @@ + (BugsnagConfiguration *)configurationFromOptions:(NSDictionary *)options { NSMutableSet *unknownKeys = [NSMutableSet setWithArray:options.allKeys]; [unknownKeys minusSet:[NSSet setWithArray:validKeys]]; if (unknownKeys.count > 0) { - BSG_KSLOG_WARN(@"Unknown dictionary keys passed in configuration options: %@", unknownKeys); + bsg_log_warn(@"Unknown dictionary keys passed in configuration options: %@", unknownKeys); } [self loadString:config options:options key:BSGKeyAppType]; diff --git a/Bugsnag/Helpers/BSGCachesDirectory.m b/Bugsnag/Helpers/BSGCachesDirectory.m index b0a9d0ea2..b19541056 100644 --- a/Bugsnag/Helpers/BSGCachesDirectory.m +++ b/Bugsnag/Helpers/BSGCachesDirectory.m @@ -7,7 +7,8 @@ // #import "BSGCachesDirectory.h" -#import "BSG_KSLogger.h" + +#import "BugsnagLogger.h" @implementation BSGCachesDirectory @@ -21,12 +22,12 @@ + (NSString *)cachesDirectory { dispatch_once(&onceToken, ^{ NSArray *dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); if ([dirs count] == 0) { - BSG_KSLOG_ERROR(@"Could not locate cache directory path."); + bsg_log_err(@"Could not locate cache directory path."); return; } if ([dirs[0] length] == 0) { - BSG_KSLOG_ERROR(@"Could not locate cache directory path."); + bsg_log_err(@"Could not locate cache directory path."); return; } cachesPath = dirs[0]; @@ -42,7 +43,7 @@ + (NSString *)getSubdirPath:(NSString *)relativePath { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; if(![fileManager createDirectoryAtPath:subdirPath withIntermediateDirectories:YES attributes:nil error:&error]) { - BSG_KSLOG_ERROR(@"Could not create caches subdir %@: %@", subdirPath, error); + bsg_log_err(@"Could not create caches subdir %@: %@", subdirPath, error); // Make the best of it, just return the top-level caches dir. return cachesDir; } diff --git a/Bugsnag/Storage/BugsnagFileStore.m b/Bugsnag/Storage/BugsnagFileStore.m index 66fe065a1..b938747f8 100644 --- a/Bugsnag/Storage/BugsnagFileStore.m +++ b/Bugsnag/Storage/BugsnagFileStore.m @@ -4,11 +4,12 @@ // #import "BugsnagFileStore.h" + +#import "BSGCachesDirectory.h" #import "BSG_KSCrashReportFields.h" #import "BSG_KSJSONCodecObjC.h" +#import "BugsnagLogger.h" #import "NSError+BSG_SimpleConstructor.h" -#import "BSG_KSLogger.h" -#import "BSGCachesDirectory.h" #pragma mark - Meta Data @@ -90,7 +91,7 @@ - (NSArray *)fileIds { NSFileManager *fm = [NSFileManager defaultManager]; NSArray *filenames = [fm contentsOfDirectoryAtPath:self.path error:&error]; if (filenames == nil) { - BSG_KSLOG_ERROR(@"Could not get contents of directory %@: %@", + bsg_log_err(@"Could not get contents of directory %@: %@", self.path, error); return nil; } @@ -105,7 +106,7 @@ - (NSArray *)fileIds { NSDictionary *fileAttribs = [fm attributesOfItemAtPath:fullPath error:&error]; if (fileAttribs == nil) { - BSG_KSLOG_ERROR(@"Could not read file attributes for %@: %@", + bsg_log_err(@"Could not read file attributes for %@: %@", fullPath, error); } else { FileStoreInfo *info = [FileStoreInfo fileStoreInfoWithId:fileId @@ -165,11 +166,11 @@ - (NSDictionary *)fileWithId:(NSString *)fileId { NSMutableDictionary *fileContents = [self readFile:[self pathToFileWithId:fileId] error:&error]; if (error != nil) { - BSG_KSLOG_ERROR(@"Encountered error loading file %@: %@", + bsg_log_err(@"Encountered error loading file %@: %@", fileId, error); } if (fileContents == nil) { - BSG_KSLOG_ERROR(@"Could not load file"); + bsg_log_err(@"Could not load file"); return nil; } return fileContents; @@ -181,7 +182,7 @@ - (void)deleteFileWithId:(NSString *)fileId { [[NSFileManager defaultManager] removeItemAtPath:filename error:&error]; if (error != nil) { - BSG_KSLOG_ERROR(@"Could not delete file %@: %@", filename, error); + bsg_log_err(@"Could not delete file %@: %@", filename, error); } } @@ -195,11 +196,11 @@ + (NSString *)findReportStorePath:(NSString *)customDirectory { [[BSGCachesDirectory cachesDirectory] stringByAppendingPathComponent:storePathEnd]; if ([storePath length] == 0) { - BSG_KSLOG_ERROR(@"Could not determine report files path."); + bsg_log_err(@"Could not determine report files path."); return nil; } if (![self ensureDirectoryExists:storePath]) { - BSG_KSLOG_ERROR(@"Store Directory does not exist."); + bsg_log_err(@"Store Directory does not exist."); return nil; } return storePath; @@ -214,7 +215,7 @@ + (BOOL)ensureDirectoryExists:(NSString *)path { withIntermediateDirectories:YES attributes:nil error:&error]) { - BSG_KSLOG_ERROR(@"Could not create directory %@: %@.", path, error); + bsg_log_err(@"Could not create directory %@: %@.", path, error); return NO; } } @@ -229,7 +230,7 @@ - (void)performOnFields:(NSArray *)fieldPath operation:(void (^)(id parent, id field))operation okIfNotFound:(BOOL)isOkIfNotFound { if (fieldPath.count == 0) { - BSG_KSLOG_ERROR(@"Unexpected end of field path"); + bsg_log_err(@"Unexpected end of field path"); return; } @@ -244,7 +245,7 @@ - (void)performOnFields:(NSArray *)fieldPath id field = file[currentField]; if (field == nil) { if (!isOkIfNotFound) { - BSG_KSLOG_ERROR(@"%@: No such field in file. Candidates are: %@", + bsg_log_err(@"%@: No such field in file. Candidates are: %@", currentField, file.allKeys); } return; @@ -300,7 +301,7 @@ - (NSMutableDictionary *)readFile:(NSString *)path error:error]; if (error != nil && *error != nil) { - BSG_KSLOG_ERROR(@"Error decoding JSON data from %@: %@", path, *error); + bsg_log_err(@"Error decoding JSON data from %@: %@", path, *error); fileContents[@BSG_KSCrashField_Incomplete] = @YES; } return fileContents; diff --git a/Bugsnag/Storage/BugsnagSessionFileStore.m b/Bugsnag/Storage/BugsnagSessionFileStore.m index 49b984426..3ab62d019 100644 --- a/Bugsnag/Storage/BugsnagSessionFileStore.m +++ b/Bugsnag/Storage/BugsnagSessionFileStore.m @@ -5,8 +5,8 @@ #import "BugsnagSessionFileStore.h" -#import "BSG_KSLogger.h" #import "BSGJSONSerialization.h" +#import "BugsnagLogger.h" #import "BugsnagSession+Private.h" static NSString *const kSessionStoreSuffix = @"-Session-"; @@ -43,7 +43,7 @@ - (void)write:(BugsnagSession *)session { NSData *json = [BSGJSONSerialization dataWithJSONObject:dict options:0 error:&error]; if (error != nil || ![json writeToFile:filepath atomically:YES]) { - BSG_KSLOG_ERROR(@"Failed to write session %@", error); + bsg_log_err(@"Failed to write session %@", error); return; } From 194faa9adc2453117e19b8974497cf0d879e47ec Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 23 Dec 2020 13:11:12 +0000 Subject: [PATCH 30/33] Redirect BSG_KSLOG_* to bsg_log_* --- .../KSCrash/Recording/Tools/BSG_KSLogger.h | 18 ++++++++++++++++++ Tests/KSCrash/KSLogger_Tests.m | 5 ----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSLogger.h b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSLogger.h index 7676817d3..470026060 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSLogger.h +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/Tools/BSG_KSLogger.h @@ -356,6 +356,24 @@ bool bsg_kslog_setLogFilename(const char *filename, bool overwrite); #undef BSG_KSLOG_BAK_TRACE #endif +#ifdef __OBJC__ + +#pragma mark - Redirect BSG_KSLOG_* to bsg_log_* so that logging output has a unified format. + +#undef BSG_KSLOG_ERROR +#undef BSG_KSLOG_WARN +#undef BSG_KSLOG_INFO +#undef BSG_KSLOG_DEBUG +#undef BSG_KSLOG_TRACE + +#define BSG_KSLOG_ERROR bsg_log_err +#define BSG_KSLOG_WARN bsg_log_warn +#define BSG_KSLOG_INFO bsg_log_info +#define BSG_KSLOG_DEBUG bsg_log_debug +#define BSG_KSLOG_TRACE bsg_log_debug + +#endif // __OBJC__ + #ifdef __cplusplus } #endif diff --git a/Tests/KSCrash/KSLogger_Tests.m b/Tests/KSCrash/KSLogger_Tests.m index ba2cecd04..c5f877716 100755 --- a/Tests/KSCrash/KSLogger_Tests.m +++ b/Tests/KSCrash/KSLogger_Tests.m @@ -58,11 +58,6 @@ - (void) testLogError BSG_KSLOG_ERROR(@"TEST"); } -- (void) testLogErrorNull -{ - BSG_KSLOG_ERROR(nil); -} - - (void) testLogAlways { BSG_KSLOG_ALWAYS(@"TEST"); From ea6333e3d68ca75b321beab9f2d54c2f03f4cf24 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Mon, 4 Jan 2021 13:28:43 +0000 Subject: [PATCH 31/33] Report iOS Framework size change in pull requests --- .github/workflows/framework-size.danger | 19 ++++++++++ .github/workflows/framework-size.yml | 49 +++++++++++++++++++++++++ Gemfile | 1 + Gemfile.lock | 48 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 .github/workflows/framework-size.danger create mode 100644 .github/workflows/framework-size.yml diff --git a/.github/workflows/framework-size.danger b/.github/workflows/framework-size.danger new file mode 100644 index 000000000..c2ee0e0f8 --- /dev/null +++ b/.github/workflows/framework-size.danger @@ -0,0 +1,19 @@ +# Dangerfile for reporting change of framework size + +# Formats a number with thousands separated by ',' +def format_number(number) + number.to_s.reverse.scan(/.{1,3}/).join(',').reverse +end + +# The .size_after and .size_before files must be created prior to running this Dangerfile. +size_after = File.read('.size_after').to_i +size_before = File.read('.size_before').to_i + +markdown(" +### Bugsnag.framework binary size +| | iOS arm64 | +|--------|-------------------------------------| +| Before | #{format_number(size_before)} bytes | +| After | #{format_number(size_after)} bytes | +| Diff | #{sprintf("%+d", size_after - size_before)} | +") diff --git a/.github/workflows/framework-size.yml b/.github/workflows/framework-size.yml new file mode 100644 index 000000000..70bac934a --- /dev/null +++ b/.github/workflows/framework-size.yml @@ -0,0 +1,49 @@ +name: "Framework size" +on: [pull_request] + +jobs: + build: + name: iOS arm64 + runs-on: macos-latest + steps: + - name: Checkout base branch + uses: actions/checkout@v1 + with: + ref: ${{ github.base_ref }} + clean: false + + - name: Build framework + run: | + xcodebuild -project Bugsnag.xcodeproj -configuration Release -scheme Bugsnag-iOS -destination generic/platform=iOS -derivedDataPath DerivedData -quiet clean build VALID_ARCHS=arm64 + eval $(stat -s DerivedData/Build/Products/Release-iphoneos/Bugsnag.framework/Bugsnag) + echo $st_size > .size_before + + - name: Checkout PR branch + uses: actions/checkout@v1 + with: + ref: ${{ github.ref }} + clean: false + + - name: Build framework + run: | + xcodebuild -project Bugsnag.xcodeproj -configuration Release -scheme Bugsnag-iOS -destination generic/platform=iOS -derivedDataPath DerivedData -quiet clean build VALID_ARCHS=arm64 + eval $(stat -s DerivedData/Build/Products/Release-iphoneos/Bugsnag.framework/Bugsnag) + echo $st_size > .size_after + + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + + - name: Danger + run: bundle exec danger --dangerfile=.github/workflows/framework-size.danger + env: + # https://docs.github.com/en/free-pro-team@latest/actions/reference/authentication-in-a-workflow + DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Gemfile b/Gemfile index 1de2b7834..56a125753 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' gem 'cocoapods' +gem 'danger' gem 'xcpretty' # A reference to Maze Runner is only needed for running tests locally and if committed it must be diff --git a/Gemfile.lock b/Gemfile.lock index d54a9a6f4..7d2bce39c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,10 @@ GEM builder (3.2.4) childprocess (3.0.0) claide (1.0.3) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) cocoapods (1.10.0) addressable (~> 2.6) claide (>= 1.0.2, < 2.0) @@ -81,6 +85,8 @@ GEM cocoapods-try (1.2.0) colored2 (3.1.2) concurrent-ruby (1.1.7) + cork (0.3.0) + colored2 (~> 3.1) cucumber (3.1.2) builder (>= 2.1.2) cucumber-core (~> 3.2.0) @@ -98,11 +104,31 @@ GEM cucumber-tag_expressions (1.1.1) cucumber-wire (0.0.1) curb (0.9.11) + danger (8.2.1) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 2.0) + faraday-http-cache (~> 2.0) + git (~> 1.7) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + no_proxy_fix + octokit (~> 4.7) + terminal-table (~> 1) diff-lcs (1.4.4) escape (0.0.4) ethon (0.12.0) ffi (>= 1.3.0) eventmachine (1.2.7) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-http-cache (2.2.0) + faraday (>= 0.8) + faraday-net_http (1.0.0) faye-websocket (0.11.0) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) @@ -111,31 +137,51 @@ GEM fuzzy_match (2.0.4) gh_inspector (1.1.3) gherkin (5.1.0) + git (1.8.1) + rchardet (~> 1.8) httpclient (2.8.3) i18n (1.8.5) concurrent-ruby (~> 1.0) json (2.3.1) + kramdown (2.3.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) mini_portile2 (2.4.0) minitest (5.14.2) molinillo (0.6.6) multi_json (1.15.0) multi_test (0.1.2) + multipart-post (2.1.1) nanaimo (0.3.0) nap (1.1.0) netrc (0.11.0) + no_proxy_fix (0.1.2) nokogiri (1.10.10) mini_portile2 (~> 2.4.0) + octokit (4.20.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + open4 (1.3.4) optimist (3.0.1) os (1.0.1) power_assert (1.2.0) public_suffix (4.0.6) rake (12.3.3) + rchardet (1.8.0) + rexml (3.2.4) rouge (2.0.7) ruby-macho (1.4.0) + ruby2_keywords (0.0.2) rubyzip (2.3.0) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) selenium-webdriver (3.142.7) childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) test-unit (3.3.7) power_assert thread_safe (0.3.6) @@ -144,6 +190,7 @@ GEM ethon (>= 0.9.0) tzinfo (1.2.7) thread_safe (~> 0.1) + unicode-display_width (1.7.0) websocket-driver (0.7.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -162,6 +209,7 @@ PLATFORMS DEPENDENCIES bugsnag-maze-runner! cocoapods + danger xcpretty BUNDLED WITH From 8e832e8fb72006ae52213a7f1eb67c1b71c214a5 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Tue, 5 Jan 2021 15:07:40 +0000 Subject: [PATCH 32/33] Fall back to raw message if regex exception thrown --- Bugsnag/Payload/BugsnagError.m | 5 ++++- Tests/BugsnagErrorTest.m | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Bugsnag/Payload/BugsnagError.m b/Bugsnag/Payload/BugsnagError.m index d44cd2f4d..5771a3c1c 100644 --- a/Bugsnag/Payload/BugsnagError.m +++ b/Bugsnag/Payload/BugsnagError.m @@ -151,7 +151,10 @@ - (void)updateWithCrashInfoMessage:(NSString *)crashInfoMessage { } } @catch (NSException *exception) { bsg_log_err(@"Exception thrown while parsing crash info message: %@", exception); - return; + if (!self.errorMessage.length) { + // It's better to fall back to the raw string than have an empty errorMessage. + self.errorMessage = crashInfoMessage; + } } } diff --git a/Tests/BugsnagErrorTest.m b/Tests/BugsnagErrorTest.m index 4a2ce5e7d..ac727bb75 100644 --- a/Tests/BugsnagErrorTest.m +++ b/Tests/BugsnagErrorTest.m @@ -193,6 +193,12 @@ - (void)testUpdateWithCrashInfoMessage { // Swift fatal errors without a message. // The errorClass should be overwritten but the errorMessage left as-is. + error.errorClass = nil; + error.errorMessage = nil; + [error updateWithCrashInfoMessage:@"Assertion failed: file bugsnag_example/AnotherClass.swift, line 24\n"]; + XCTAssertEqualObjects(error.errorClass, @"Assertion failed"); + XCTAssertEqualObjects(error.errorMessage, nil); + error.errorClass = nil; error.errorMessage = @"Expected message"; [error updateWithCrashInfoMessage:@"Assertion failed: file bugsnag_example/AnotherClass.swift, line 24\n"]; From 903b2a7e1fef5ac0bc622a750fa0131bdf8396e6 Mon Sep 17 00:00:00 2001 From: Nick Dowell Date: Wed, 6 Jan 2021 11:30:46 +0000 Subject: [PATCH 33/33] Release v6.5.0 --- Bugsnag.podspec.json | 4 ++-- .../Source/KSCrash/Recording/BSG_KSCrash.m | 2 +- Bugsnag/Payload/BugsnagNotifier.m | 2 +- CHANGELOG.md | 16 +++++++++++++++- Framework/Info.plist | 2 +- Tests/Info.plist | 2 +- VERSION | 2 +- 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Bugsnag.podspec.json b/Bugsnag.podspec.json index 5de71ad21..fdb946e55 100644 --- a/Bugsnag.podspec.json +++ b/Bugsnag.podspec.json @@ -1,6 +1,6 @@ { "name": "Bugsnag", - "version": "6.4.1", + "version": "6.5.0", "summary": "The Bugsnag crash reporting framework for Apple platforms.", "homepage": "https://bugsnag.com", "license": "MIT", @@ -9,7 +9,7 @@ }, "source": { "git": "https://github.com/bugsnag/bugsnag-cocoa.git", - "tag": "v6.4.1" + "tag": "v6.5.0" }, "frameworks": [ "Foundation", diff --git a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m index a001905b5..1c47cf990 100644 --- a/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m +++ b/Bugsnag/KSCrash/Source/KSCrash/Recording/BSG_KSCrash.m @@ -283,7 +283,7 @@ - (void)sendAllReports { NSDictionary *reports = [self allReportsByFilename]; - BSG_KSLOG_INFO(@"Sending %d crash reports", [reports count]); + BSG_KSLOG_INFO(@"Sending %lu crash reports", (unsigned long)reports.count); [self sendReports:reports withBlock:^(NSString *filename, BOOL completed, diff --git a/Bugsnag/Payload/BugsnagNotifier.m b/Bugsnag/Payload/BugsnagNotifier.m index 09e93fa75..e59034259 100644 --- a/Bugsnag/Payload/BugsnagNotifier.m +++ b/Bugsnag/Payload/BugsnagNotifier.m @@ -23,7 +23,7 @@ - (instancetype)init { #else self.name = @"Bugsnag Objective-C"; #endif - self.version = @"6.4.1"; + self.version = @"6.5.0"; self.url = @"https://github.com/bugsnag/bugsnag-cocoa"; self.dependencies = [NSMutableArray new]; } diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b0c0163..0a85d4a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,27 @@ Changelog ========= -## TBD +## 6.5.0 (2021-01-06) ### Enhancements * Errors may now be discarded based on their `errorClass` using the new `discardClasses` configuration option. [#938](https://github.com/bugsnag/bugsnag-cocoa/pull/938) +* The maxiumum number of persisted errors / events may now be configured using the new `maxPersistedEvents` configuration option. + [#936](https://github.com/bugsnag/bugsnag-cocoa/pull/936) + +* The maxiumum number of persisted sessions may now be configured using the new `maxPersistedSessions` configuration option. + [#943](https://github.com/bugsnag/bugsnag-cocoa/pull/943) + +* Bugsnag log messages are now prefixed with `[Bugsnag]` for easier searching & filtering. + [#955](https://github.com/bugsnag/bugsnag-cocoa/pull/955) + +## Bug fixes + +* Fix reliability of Swift fatal error message reporting. + [#948](https://github.com/bugsnag/bugsnag-cocoa/pull/948) + ## 6.4.1 (2020-12-14) ### Bug fixes diff --git a/Framework/Info.plist b/Framework/Info.plist index a75e8d871..666246065 100644 --- a/Framework/Info.plist +++ b/Framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.4.1 + 6.5.0 CFBundleVersion 1 diff --git a/Tests/Info.plist b/Tests/Info.plist index 7d94ab547..457d931f6 100644 --- a/Tests/Info.plist +++ b/Tests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 6.4.1 + 6.5.0 CFBundleVersion 1 diff --git a/VERSION b/VERSION index 4c77920fd..f22d756da 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.4.1 +6.5.0