Skip to content

Commit

Permalink
feat: add OnBreadcrumb callback
Browse files Browse the repository at this point in the history
  • Loading branch information
fractalwrench committed Mar 27, 2020
1 parent 83c98a8 commit e58f54e
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions OSX/Bugsnag.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
E7CE78871FD94E5F001D07E0 /* KSMach_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSMach_Tests.m; sourceTree = "<group>"; };
E7CE78881FD94E5F001D07E0 /* NSError+SimpleConstructor_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+SimpleConstructor_Tests.m"; sourceTree = "<group>"; };
E7CE78891FD94E5F001D07E0 /* KSFileUtils_Tests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KSFileUtils_Tests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -503,6 +505,7 @@
8A2C8FAF1C6BC1F700846019 /* Tests */ = {
isa = PBXGroup;
children = (
E7AB4B9D2423E184004F015A /* BugsnagOnBreadcrumbTest.m */,
00F9393B23FD2D9B008C7073 /* BugsnagTestsDummyClass.h */,
00F9393A23FD2D9B008C7073 /* BugsnagTestsDummyClass.m */,
00F9393123FC168F008C7073 /* BugsnagBaseUnitTest.h */,
Expand Down Expand Up @@ -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 */,
Expand Down
19 changes: 19 additions & 0 deletions Source/Bugsnag.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions Source/Bugsnag.m
Original file line number Diff line number Diff line change
Expand Up @@ -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

//
Expand Down
21 changes: 20 additions & 1 deletion Source/BugsnagBreadcrumbs.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand All @@ -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, ^{
Expand Down
26 changes: 26 additions & 0 deletions Source/BugsnagConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -244,6 +251,25 @@ typedef NS_OPTIONS(NSUInteger, BSGErrorType) {

- (void)addPlugin:(id<BugsnagPlugin> _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?
*
Expand Down
14 changes: 14 additions & 0 deletions Source/BugsnagConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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",
Expand Down
195 changes: 195 additions & 0 deletions Tests/BugsnagOnBreadcrumbTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//
// BugsnagOnBreadcrumbTest.m
// Tests
//
// Created by Jamie Lynch on 19/03/2020.
// Copyright © 2020 Bugsnag. All rights reserved.
//

#import <XCTest/XCTest.h>

#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
Loading

0 comments on commit e58f54e

Please sign in to comment.