From e58f54e06cc64bc2850a292b139b73563a808ecf Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 19 Mar 2020 17:16:42 +0000 Subject: [PATCH] feat: add OnBreadcrumb callback --- CHANGELOG.md | 3 + OSX/Bugsnag.xcodeproj/project.pbxproj | 4 + Source/Bugsnag.h | 19 +++ Source/Bugsnag.m | 13 ++ Source/BugsnagBreadcrumbs.m | 21 ++- Source/BugsnagConfiguration.h | 26 ++++ Source/BugsnagConfiguration.m | 14 ++ Tests/BugsnagOnBreadcrumbTest.m | 195 ++++++++++++++++++++++++++ UPGRADING.md | 3 + iOS/Bugsnag.xcodeproj/project.pbxproj | 4 + 10 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 Tests/BugsnagOnBreadcrumbTest.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9c254b2..5818e66c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Bugsnag Notifiers on other platforms. ## Enhancements +* Allow addition/removal of `OnBreadcrumb` callbacks +[#508](https://github.com/bugsnag/bugsnag-cocoa/pull/508) + * Remove unused APIs from `BugsnagMetadata` interface [#501](https://github.com/bugsnag/bugsnag-cocoa/pull/501) diff --git a/OSX/Bugsnag.xcodeproj/project.pbxproj b/OSX/Bugsnag.xcodeproj/project.pbxproj index 682d1c876..cbec5ffde 100644 --- a/OSX/Bugsnag.xcodeproj/project.pbxproj +++ b/OSX/Bugsnag.xcodeproj/project.pbxproj @@ -162,6 +162,7 @@ E79E6BDA1F4E3850002B35F9 /* BSG_RFC3339DateTool.m in Sources */ = {isa = PBXBuildFile; fileRef = E79E6B6E1F4E3850002B35F9 /* BSG_RFC3339DateTool.m */; }; E79E6BDB1F4E3850002B35F9 /* BSG_KSCrashReportFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = E79E6B711F4E3850002B35F9 /* BSG_KSCrashReportFilter.h */; }; E79E6BDC1F4E3850002B35F9 /* BSG_KSCrashReportFilterCompletion.h in Headers */ = {isa = PBXBuildFile; fileRef = E79E6B721F4E3850002B35F9 /* BSG_KSCrashReportFilterCompletion.h */; }; + E7AB4B9E2423E184004F015A /* BugsnagOnBreadcrumbTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7AB4B9D2423E184004F015A /* BugsnagOnBreadcrumbTest.m */; }; E7CE78BB1FD94E77001D07E0 /* KSCrashReportConverter_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7CE78991FD94E60001D07E0 /* KSCrashReportConverter_Tests.m */; }; E7CE78BC1FD94E77001D07E0 /* KSCrashReportStore_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7CE78951FD94E5F001D07E0 /* KSCrashReportStore_Tests.m */; }; E7CE78BE1FD94E77001D07E0 /* KSCrashSentry_NSException_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7CE78911FD94E5F001D07E0 /* KSCrashSentry_NSException_Tests.m */; }; @@ -359,6 +360,7 @@ E79E6B6E1F4E3850002B35F9 /* BSG_RFC3339DateTool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BSG_RFC3339DateTool.m; path = ../Source/KSCrash/Source/KSCrash/Recording/Tools/BSG_RFC3339DateTool.m; sourceTree = SOURCE_ROOT; }; E79E6B711F4E3850002B35F9 /* BSG_KSCrashReportFilter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSG_KSCrashReportFilter.h; path = ../Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilter.h; sourceTree = SOURCE_ROOT; }; E79E6B721F4E3850002B35F9 /* BSG_KSCrashReportFilterCompletion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSG_KSCrashReportFilterCompletion.h; path = ../Source/KSCrash/Source/KSCrash/Reporting/Filters/BSG_KSCrashReportFilterCompletion.h; sourceTree = SOURCE_ROOT; }; + E7AB4B9D2423E184004F015A /* BugsnagOnBreadcrumbTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagOnBreadcrumbTest.m; sourceTree = ""; }; E7CE78871FD94E5F001D07E0 /* KSMach_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSMach_Tests.m; sourceTree = ""; }; E7CE78881FD94E5F001D07E0 /* NSError+SimpleConstructor_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+SimpleConstructor_Tests.m"; sourceTree = ""; }; E7CE78891FD94E5F001D07E0 /* KSFileUtils_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSFileUtils_Tests.m; sourceTree = ""; }; @@ -503,6 +505,7 @@ 8A2C8FAF1C6BC1F700846019 /* Tests */ = { isa = PBXGroup; children = ( + E7AB4B9D2423E184004F015A /* BugsnagOnBreadcrumbTest.m */, 00F9393B23FD2D9B008C7073 /* BugsnagTestsDummyClass.h */, 00F9393A23FD2D9B008C7073 /* BugsnagTestsDummyClass.m */, 00F9393123FC168F008C7073 /* BugsnagBaseUnitTest.h */, @@ -999,6 +1002,7 @@ E762E9F91F73F7F300E82B43 /* BugsnagHandledStateTest.m in Sources */, E7CE78C51FD94E77001D07E0 /* KSLogger_Tests.m in Sources */, E7CE78C11FD94E77001D07E0 /* KSCrashState_Tests.m in Sources */, + E7AB4B9E2423E184004F015A /* BugsnagOnBreadcrumbTest.m in Sources */, E7CE78C31FD94E77001D07E0 /* KSFileUtils_Tests.m in Sources */, E7CE78BC1FD94E77001D07E0 /* KSCrashReportStore_Tests.m in Sources */, E79148611FD82BB7003EFEBF /* BugsnagSessionTrackerTest.m in Sources */, diff --git a/Source/Bugsnag.h b/Source/Bugsnag.h index 8d007b366..a26c79f17 100644 --- a/Source/Bugsnag.h +++ b/Source/Bugsnag.h @@ -329,4 +329,23 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; */ + (void)removeOnSendBlock:(BugsnagOnSendBlock _Nonnull)block; +// ============================================================================= +// MARK: - onBreadcrumb +// ============================================================================= + +/** + * Add a callback to be invoked when a breadcrumb is captured by Bugsnag, to + * change the breadcrumb contents as needed + * + * @param block A block which returns YES if the breadcrumb should be captured + */ ++ (void)addOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block; + +/** + * Remove the callback that would be invoked when a breadcrumb is captured. + * + * @param block The block to be removed. + */ ++ (void)removeOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block; + @end diff --git a/Source/Bugsnag.m b/Source/Bugsnag.m index f80c7f7a0..4b989f0cd 100644 --- a/Source/Bugsnag.m +++ b/Source/Bugsnag.m @@ -314,6 +314,19 @@ + (void)removeOnSendBlock:(BugsnagOnSendBlock _Nonnull)block [[self configuration] removeOnSendBlock:block]; } +// ============================================================================= +// MARK: - OnBreadcrumb +// ============================================================================= + ++ (void)addOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block { + [[self configuration] addOnBreadcrumbBlock:block]; +} + + ++ (void)removeOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block { + [[self configuration] removeOnBreadcrumbBlock:block]; +} + @end // diff --git a/Source/BugsnagBreadcrumbs.m b/Source/BugsnagBreadcrumbs.m index 96bda7761..70abe5b77 100644 --- a/Source/BugsnagBreadcrumbs.m +++ b/Source/BugsnagBreadcrumbs.m @@ -12,6 +12,10 @@ #import "BugsnagLogger.h" #import "Private.h" +@interface BugsnagConfiguration () +@property(nonatomic) NSMutableArray *onBreadcrumbBlocks; +@end + @interface BugsnagBreadcrumb () + (instancetype _Nullable)breadcrumbWithBlock: (BSGBreadcrumbConfiguration _Nonnull)block; @@ -58,7 +62,8 @@ - (void)addBreadcrumbWithBlock: return; } BugsnagBreadcrumb *crumb = [BugsnagBreadcrumb breadcrumbWithBlock:block]; - if (crumb) { + + if (crumb != nil && [self shouldSendBreadcrumb:crumb]) { [self resizeToFitCapacity:self.capacity - 1]; dispatch_barrier_sync(self.readWriteQueue, ^{ [self.breadcrumbs addObject:crumb]; @@ -82,6 +87,20 @@ - (void)addBreadcrumbWithBlock: } } +- (BOOL)shouldSendBreadcrumb:(BugsnagBreadcrumb *)crumb { + BugsnagConfiguration *configuration = [Bugsnag configuration]; + for (BugsnagOnBreadcrumbBlock block in configuration.onBreadcrumbBlocks) { + @try { + if (!block(crumb)) { + return NO; + } + } @catch (NSException *exception) { + bsg_log_err(@"Error from onBreadcrumb callback: %@", exception); + } + } + return YES; +} + - (NSArray *)cachedBreadcrumbs { __block NSArray *cache = nil; dispatch_barrier_sync(self.readWriteQueue, ^{ diff --git a/Source/BugsnagConfiguration.h b/Source/BugsnagConfiguration.h index 99c7df85c..237ae52b6 100644 --- a/Source/BugsnagConfiguration.h +++ b/Source/BugsnagConfiguration.h @@ -60,6 +60,13 @@ typedef void (^BugsnagOnErrorBlock)(BugsnagEvent *_Nonnull event); */ typedef bool (^BugsnagOnSendBlock)(BugsnagEvent *_Nonnull event); +/** + * A configuration block for modifying a captured breadcrumb + * + * @param breadcrumb The breadcrumb + */ +typedef BOOL (^BugsnagOnBreadcrumbBlock)(BugsnagBreadcrumb *_Nonnull breadcrumb); + /** * A configuration block for modifying a session. Intended for internal usage only. * @@ -244,6 +251,25 @@ typedef NS_OPTIONS(NSUInteger, BSGErrorType) { - (void)addPlugin:(id _Nonnull)plugin; +// ============================================================================= +// MARK: - onBreadcrumb +// ============================================================================= + +/** + * Add a callback to be invoked when a breadcrumb is captured by Bugsnag, to + * change the breadcrumb contents as needed + * + * @param block A block which returns YES if the breadcrumb should be captured + */ +- (void)addOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block; + +/** + * Remove the callback that would be invoked when a breadcrumb is captured. + * + * @param block The block to be removed. + */ +- (void)removeOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block; + /** * Should the specified type of breadcrumb be recorded? * diff --git a/Source/BugsnagConfiguration.m b/Source/BugsnagConfiguration.m index bd03e193e..d0652b26a 100644 --- a/Source/BugsnagConfiguration.m +++ b/Source/BugsnagConfiguration.m @@ -67,6 +67,7 @@ @interface BugsnagConfiguration () * Hooks for modifying sessions before they are sent to Bugsnag. Intended for internal use only by React Native/Unity. */ @property(nonatomic, readwrite, strong) NSMutableArray *onSessionBlocks; +@property(nonatomic, readwrite, strong) NSMutableArray *onBreadcrumbBlocks; @property(nonatomic, readwrite, strong) NSMutableSet *plugins; @property(readonly, retain, nullable) NSURL *notifyURL; @property(readonly, retain, nullable) NSURL *sessionURL; @@ -138,6 +139,7 @@ - (instancetype _Nonnull)initWithApiKey:(NSString *_Nonnull)apiKey _notifyURL = [NSURL URLWithString:BSGDefaultNotifyUrl]; _onSendBlocks = [NSMutableArray new]; _onSessionBlocks = [NSMutableArray new]; + _onBreadcrumbBlocks = [NSMutableArray new]; _plugins = [NSMutableSet new]; _notifyReleaseStages = nil; _breadcrumbs = [BugsnagBreadcrumbs new]; @@ -238,6 +240,18 @@ - (void)removeOnSessionBlock:(BugsnagOnSessionBlock)block { [(NSMutableArray *)self.onSessionBlocks removeObject:block]; } +// ============================================================================= +// MARK: - onBreadcrumbBlock +// ============================================================================= + +- (void)addOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block { + [(NSMutableArray *)self.onBreadcrumbBlocks addObject:[block copy]]; +} + +- (void)removeOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock _Nonnull)block { + [(NSMutableArray *)self.onBreadcrumbBlocks removeObject:block]; +} + - (NSDictionary *)errorApiHeaders { return @{ kHeaderApiPayloadVersion: @"4.0", diff --git a/Tests/BugsnagOnBreadcrumbTest.m b/Tests/BugsnagOnBreadcrumbTest.m new file mode 100644 index 000000000..06fd03940 --- /dev/null +++ b/Tests/BugsnagOnBreadcrumbTest.m @@ -0,0 +1,195 @@ +// +// BugsnagOnBreadcrumbTest.m +// Tests +// +// Created by Jamie Lynch on 19/03/2020. +// Copyright © 2020 Bugsnag. All rights reserved. +// + +#import + +#import "Bugsnag.h" +#import "BugsnagConfiguration.h" +#import "BugsnagTestConstants.h" +#import "BugsnagBreadcrumbs.h" + +@interface BugsnagConfiguration () +@property NSMutableArray *onBreadcrumbBlocks; +@property BugsnagBreadcrumbs *breadcrumbs; +@end + +@interface BugsnagBreadcrumbs () +@property(nonatomic, readwrite, strong) NSMutableArray *breadcrumbs; +@end + +@interface BugsnagOnBreadcrumbTest : XCTestCase +@end + +@implementation BugsnagOnBreadcrumbTest + + +/** + * Test that onBreadcrumb blocks get called once added + */ +- (void)testAddOnBreadcrumbBlock { + + // Setup + __block XCTestExpectation *expectation = [self expectationWithDescription:@"Remove On Breadcrumb Block"]; + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + [config setEndpointsForNotify:@"http://notreal.bugsnag.com" sessions:@"http://notreal.bugsnag.com"]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + BugsnagOnBreadcrumbBlock crumbBlock = ^(BugsnagBreadcrumb * _Nonnull crumb) { + // We expect the breadcrumb block to be called + [expectation fulfill]; + return YES; + }; + [config addOnBreadcrumbBlock:crumbBlock]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + + // Call onbreadcrumb blocks + [Bugsnag startBugsnagWithConfiguration:config]; + [Bugsnag leaveBreadcrumbWithMessage:@"Hello"]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +/** + * Test that onBreadcrumb blocks do not get called once they've been removed + */ +- (void)testRemoveOnBreadcrumbBlock { + // Setup + // We expect NOT to be called + __block XCTestExpectation *calledExpectation = [self expectationWithDescription:@"Remove On Breadcrumb Block"]; + calledExpectation.inverted = YES; + + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + [config setEndpointsForNotify:@"http://notreal.bugsnag.com" sessions:@"http://notreal.bugsnag.com"]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + BugsnagOnBreadcrumbBlock crumbBlock = ^(BugsnagBreadcrumb * _Nonnull crumb) { + [calledExpectation fulfill]; + return YES; + }; + + // It's there (and from other tests we know it gets called) and then it's not there + [config addOnBreadcrumbBlock:crumbBlock]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + [config removeOnBreadcrumbBlock:crumbBlock]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + + [Bugsnag startBugsnagWithConfiguration:config]; + [Bugsnag leaveBreadcrumbWithMessage:@"Hello"]; + + // Wait a second NOT to be called + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} +/** + * Test that an onBreadcrumb block is called after being added, then NOT called after being removed. + * This test could be expanded to verify the behaviour when multiple blocks are added. + */ +- (void)testAddOnBreadcrumbBlockThenRemove { + + __block int called = 0; // A counter + + // Setup + __block XCTestExpectation *expectation1 = [self expectationWithDescription:@"Remove On Breadcrumb Block 1"]; + __block XCTestExpectation *expectation2 = [self expectationWithDescription:@"Remove On Breadcrumb Block 2"]; + expectation2.inverted = YES; + + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + [config setEndpointsForNotify:@"http://notreal.bugsnag.com" sessions:@"http://notreal.bugsnag.com"]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + + BugsnagOnBreadcrumbBlock crumbBlock = ^(BugsnagBreadcrumb * _Nonnull crumb) { + switch (called) { + case 0: + [expectation1 fulfill]; + break; + case 1: + [expectation2 fulfill]; + break; + } + return YES; + }; + + [config addOnBreadcrumbBlock:crumbBlock]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + + // Call onbreadcrumb blocks + [Bugsnag startBugsnagWithConfiguration:config]; + [Bugsnag leaveBreadcrumbWithMessage:@"Hello"]; + [self waitForExpectations:@[expectation1] timeout:1.0]; + + // Check it's NOT called once the block's deleted + called++; + [config removeOnBreadcrumbBlock:crumbBlock]; + [Bugsnag leaveBreadcrumbWithMessage:@"Hello"]; + [self waitForExpectations:@[expectation2] timeout:1.0]; +} + +/** + * Make sure slightly invalid removals and duplicate additions don't break things + */ +- (void)testRemoveNonexistentOnBreadcrumbBlocks { + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + BugsnagOnBreadcrumbBlock crumbBlock1 = ^(BugsnagBreadcrumb * _Nonnull crumb) { + return YES; + }; + BugsnagOnBreadcrumbBlock crumbBlock2 = ^(BugsnagBreadcrumb * _Nonnull crumb) { + return YES; + }; + + [config addOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + [config removeOnBreadcrumbBlock:crumbBlock2]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + [config removeOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + [config removeOnBreadcrumbBlock:crumbBlock2]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + [config removeOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 0); + + [config addOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + [config addOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 2); + [config addOnBreadcrumbBlock:crumbBlock1]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 3); +} + +/** + * Test that onBreadcrumb blocks mutate a crumb + */ +- (void)testAddOnBreadcrumbMutation { + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + [config setEndpointsForNotify:@"http://notreal.bugsnag.com" sessions:@"http://notreal.bugsnag.com"]; + [config addOnBreadcrumbBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + crumb.message = @"Foo"; + return YES; + }]; + + // Call onbreadcrumb blocks + [Bugsnag startBugsnagWithConfiguration:config]; + XCTAssertEqual([[config onBreadcrumbBlocks] count], 1); + BugsnagBreadcrumb *crumb = [[config breadcrumbs].breadcrumbs firstObject]; + XCTAssertEqualObjects(@"Foo", crumb.message); +} + +/** + * Test that onBreadcrumb blocks can discard crumbs + */ +- (void)testAddOnBreadcrumbRejection { + BugsnagConfiguration *config = [[BugsnagConfiguration alloc] initWithApiKey:DUMMY_APIKEY_32CHAR_1]; + [config setEndpointsForNotify:@"http://notreal.bugsnag.com" sessions:@"http://notreal.bugsnag.com"]; + [config addOnBreadcrumbBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + return NO; + }]; + + // Call onbreadcrumb blocks + [Bugsnag startBugsnagWithConfiguration:config]; + XCTAssertEqual([[config breadcrumbs].breadcrumbs count], 0); + [Bugsnag leaveBreadcrumbWithMessage:@"Hello"]; + XCTAssertEqual([[config breadcrumbs].breadcrumbs count], 0); +} + +@end diff --git a/UPGRADING.md b/UPGRADING.md index 382759a46..2e410f52d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -33,6 +33,9 @@ The exact error is available using the `BSGConfigurationErrorDomain` and + config.persistUserData() + config.deletePersistedUserData() + ++ config.add(onBreadcrumb:) ++ config.remove(onBreadcrumb:) ``` #### Renames diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index 8fa7049b5..a7c54cd7d 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -304,6 +304,7 @@ E79148291FD828E6003EFEBF /* BugsnagUser.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = E72BF77D1FC86A7A004BE82F /* BugsnagUser.h */; }; E791482A1FD828E6003EFEBF /* BugsnagKSCrashSysInfoParser.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = E7EB06721FCDAF2000C076A6 /* BugsnagKSCrashSysInfoParser.h */; }; E794E8031F9F743D00A67EE7 /* BugsnagKeys.h in Headers */ = {isa = PBXBuildFile; fileRef = E794E8021F9F743D00A67EE7 /* BugsnagKeys.h */; }; + E7AB4B9C2423E16C004F015A /* BugsnagOnBreadcrumbTest.m in Sources */ = {isa = PBXBuildFile; fileRef = E7AB4B9B2423E16C004F015A /* BugsnagOnBreadcrumbTest.m */; }; E7B3291A1FD707EC0098FC47 /* KSCrashReportConverter_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7B329171FD707EC0098FC47 /* KSCrashReportConverter_Tests.m */; }; E7B970311FD702DA00590C27 /* KSLogger_Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7B970301FD702DA00590C27 /* KSLogger_Tests.m */; }; E7B970341FD7031500590C27 /* XCTestCase+KSCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = E7B970331FD7031500590C27 /* XCTestCase+KSCrash.m */; }; @@ -615,6 +616,7 @@ E78C1EFD1FCC778700B976D3 /* BugsnagUserTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagUserTest.m; path = ../Tests/BugsnagUserTest.m; sourceTree = SOURCE_ROOT; }; E794E8021F9F743D00A67EE7 /* BugsnagKeys.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BugsnagKeys.h; path = ../Source/BugsnagKeys.h; sourceTree = SOURCE_ROOT; }; E79FEBE61F4CB1320048FAD6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + E7AB4B9B2423E16C004F015A /* BugsnagOnBreadcrumbTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BugsnagOnBreadcrumbTest.m; path = ../../Tests/BugsnagOnBreadcrumbTest.m; sourceTree = ""; }; E7B329171FD707EC0098FC47 /* KSCrashReportConverter_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KSCrashReportConverter_Tests.m; path = ../Tests/KSCrash/KSCrashReportConverter_Tests.m; sourceTree = SOURCE_ROOT; }; E7B970301FD702DA00590C27 /* KSLogger_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = KSLogger_Tests.m; path = ../Tests/KSCrash/KSLogger_Tests.m; sourceTree = SOURCE_ROOT; }; E7B970321FD7031500590C27 /* XCTestCase+KSCrash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "XCTestCase+KSCrash.h"; path = "../Tests/KSCrash/XCTestCase+KSCrash.h"; sourceTree = SOURCE_ROOT; }; @@ -818,6 +820,7 @@ 00F9393723FC4F63008C7073 /* BugsnagTestsDummyClass.h */, 00F9393823FC4F64008C7073 /* BugsnagTestsDummyClass.m */, E72AE1FF241A57B100ED8972 /* BugsnagPluginTest.m */, + E7AB4B9B2423E16C004F015A /* BugsnagOnBreadcrumbTest.m */, ); name = Tests; path = BugsnagTests; @@ -1303,6 +1306,7 @@ 000E6E9E23D8690F009D8194 /* BugsnagSwiftConfigurationTests.swift in Sources */, E70EE07E1FD703D600FA745C /* NSError+SimpleConstructor_Tests.m in Sources */, E70EE0931FD706C700FA745C /* KSFileUtils_Tests.m in Sources */, + E7AB4B9C2423E16C004F015A /* BugsnagOnBreadcrumbTest.m in Sources */, E70EE08E1FD705A700FA745C /* KSSignalInfo_Tests.m in Sources */, E733A76B1FD7091F003EAA29 /* KSCrashSentry_Signal_Tests.m in Sources */, E784D2561FD70B3E004B01E1 /* KSMach_Tests.m in Sources */,