From 3419670867c45779c8a431f78de12d51b05d5075 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 7 Jul 2016 17:21:18 -0700 Subject: [PATCH 1/5] Update payload version --- Source/BugsnagCrashReport.m | 2 +- Tests/BugsnagSinkTests.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/BugsnagCrashReport.m b/Source/BugsnagCrashReport.m index e7119c2c0..779df57bd 100644 --- a/Source/BugsnagCrashReport.m +++ b/Source/BugsnagCrashReport.m @@ -339,7 +339,7 @@ - (NSDictionary *)serializableValueWithTopLevelData: BSGDictInsertIfNotNil(event, [self dsymUUID], @"dsymUUID"); BSGDictSetSafeObject(event, BSGFormatSeverity(self.severity), @"severity"); BSGDictSetSafeObject(event, [self breadcrumbs], @"breadcrumbs"); - BSGDictSetSafeObject(event, @"2", @"payloadVersion"); + BSGDictSetSafeObject(event, @"3", @"payloadVersion"); BSGDictSetSafeObject(event, metaData, @"metaData"); BSGDictSetSafeObject(event, [self deviceState], @"deviceState"); BSGDictSetSafeObject(event, [self device], @"device"); diff --git a/Tests/BugsnagSinkTests.m b/Tests/BugsnagSinkTests.m index 439281868..a28230a98 100644 --- a/Tests/BugsnagSinkTests.m +++ b/Tests/BugsnagSinkTests.m @@ -127,7 +127,7 @@ - (void)testEventDsymUUID { - (void)testEventPayloadVersion { NSString *payloadVersion = [self.processedData[@"events"] firstObject][@"payloadVersion"]; - XCTAssertEqualObjects(payloadVersion, @"2"); + XCTAssertEqualObjects(payloadVersion, @"3"); } - (void)testEventSeverity { From b8426277c78382e474490167b7767fe453812fe2 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 7 Jul 2016 17:21:41 -0700 Subject: [PATCH 2/5] Convert breadcrumb payload format --- Source/BugsnagBreadcrumb.h | 49 ++++++++++++++++++++++++++++++-- Source/BugsnagBreadcrumb.m | 51 ++++++++++++++++++++++++++++++++-- Tests/BugsnagBreadcrumbsTest.m | 26 +++++++++-------- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/Source/BugsnagBreadcrumb.h b/Source/BugsnagBreadcrumb.h index 55963275b..7d976536f 100644 --- a/Source/BugsnagBreadcrumb.h +++ b/Source/BugsnagBreadcrumb.h @@ -33,14 +33,59 @@ #endif #endif +typedef NS_ENUM(NSUInteger, BSGBreadcrumbType) { + /** + * Any breadcrumb sent via Bugsnag.leaveBreadcrumb() + */ + BSGBreadcrumbTypeCustom, + /** + * A call to Bugsnag.notify() (internal use only) + */ + BSGBreadcrumbTypeError, + /** + * A log message + */ + BSGBreadcrumbTypeLog, + /** + * A navigation action, such as pushing a view controller or dismissing an + * alert + */ + BSGBreadcrumbTypeNavigation, + /** + * A background process, such performing a database query + */ + BSGBreadcrumbTypeProcess, + /** + * A network request + */ + BSGBreadcrumbTypeRequest, + /** + * Change in application or view state + */ + BSGBreadcrumbTypeState, + /** + * A user event, such as authentication or control events + */ + BSGBreadcrumbTypeUser, +}; + @interface BugsnagBreadcrumb : NSObject -@property(readonly, nullable) NSDate *timestamp; -@property(readonly, copy, nullable) NSString *message; +@property(readonly, nonatomic, nullable) NSDate *timestamp; +@property(readonly) BSGBreadcrumbType type; +@property(readonly, nonatomic, copy, nonnull) NSString *name; +@property(readonly, nonatomic, copy, nonnull) NSDictionary *metadata; - (instancetype _Nullable)initWithMessage:(NSString *_Nullable)message timestamp:(NSDate *_Nullable)date NS_DESIGNATED_INITIALIZER; + +- (instancetype _Nullable)initWithName:(NSString *_Nonnull)name + timestamp:(NSDate *_Nonnull)date + type:(BSGBreadcrumbType)type + metadata:(NSDictionary *_Nullable)metadata + NS_DESIGNATED_INITIALIZER; + @end @interface BugsnagBreadcrumbs : NSObject diff --git a/Source/BugsnagBreadcrumb.m b/Source/BugsnagBreadcrumb.m index 2311a8b60..e748780a1 100644 --- a/Source/BugsnagBreadcrumb.m +++ b/Source/BugsnagBreadcrumb.m @@ -26,6 +26,29 @@ #import "BugsnagBreadcrumb.h" #import "Bugsnag.h" +NSString *const BSGBreadcrumbDefaultName = @"Custom"; + +NSString *BSGBreadcrumbTypeValue(BSGBreadcrumbType type) { + switch (type) { + case BSGBreadcrumbTypeLog: + return @"log"; + case BSGBreadcrumbTypeUser: + return @"user"; + case BSGBreadcrumbTypeError: + return @"error"; + case BSGBreadcrumbTypeState: + return @"state"; + case BSGBreadcrumbTypeCustom: + return @"custom"; + case BSGBreadcrumbTypeProcess: + return @"process"; + case BSGBreadcrumbTypeRequest: + return @"request"; + case BSGBreadcrumbTypeNavigation: + return @"navigation"; + } +} + @interface BugsnagBreadcrumbs() @property (nonatomic,readwrite,strong) NSMutableArray* breadcrumbs; @@ -43,8 +66,23 @@ - (instancetype)initWithMessage:(NSString *)message timestamp:(NSDate *)date { return nil; if (self = [super init]) { - _message = [message copy]; + _metadata = @{@"message": message}; + _timestamp = date; + _type = BSGBreadcrumbTypeCustom; + _name = BSGBreadcrumbDefaultName; + } + return self; +} + +- (instancetype)initWithName:(NSString *)name + timestamp:(NSDate *)date + type:(BSGBreadcrumbType)type + metadata:(NSDictionary *)metadata { + if (self = [super init]) { + _name = name; _timestamp = date; + _type = type; + _metadata = metadata; } return self; } @@ -109,8 +147,15 @@ - (NSArray *)arrayValue { NSMutableArray* contents = [[NSMutableArray alloc] initWithCapacity:[self count]]; for (BugsnagBreadcrumb* crumb in self.breadcrumbs) { NSString* timestamp = [[Bugsnag payloadDateFormatter] stringFromDate:crumb.timestamp]; - if (timestamp && crumb.message.length > 0) { - [contents addObject:@[timestamp,crumb.message]]; + if (timestamp && crumb.name.length > 0) { + NSMutableDictionary *data = @{ + @"name": crumb.name, + @"timestamp": timestamp, + @"type": BSGBreadcrumbTypeValue(crumb.type) + }.mutableCopy; + if (crumb.metadata) + data[@"metaData"] = crumb.metadata; + [contents addObject:data]; } } return contents; diff --git a/Tests/BugsnagBreadcrumbsTest.m b/Tests/BugsnagBreadcrumbsTest.m index 5dd1f4752..c89b180c3 100644 --- a/Tests/BugsnagBreadcrumbsTest.m +++ b/Tests/BugsnagBreadcrumbsTest.m @@ -36,9 +36,9 @@ - (void)testMaxBreadcrumbs { self.crumbs.capacity = 3; [self.crumbs addBreadcrumb:@"Clear notifications"]; XCTAssertTrue(self.crumbs.count == 3); - XCTAssertEqualObjects(self.crumbs[0].message, @"Tap button"); - XCTAssertEqualObjects(self.crumbs[1].message, @"Close tutorial"); - XCTAssertEqualObjects(self.crumbs[2].message, @"Clear notifications"); + XCTAssertEqualObjects(self.crumbs[0].metadata[@"message"], @"Tap button"); + XCTAssertEqualObjects(self.crumbs[1].metadata[@"message"], @"Close tutorial"); + XCTAssertEqualObjects(self.crumbs[2].metadata[@"message"], @"Clear notifications"); XCTAssertNil(self.crumbs[3]); } @@ -58,8 +58,8 @@ - (void)testEmptyCapacity { - (void)testResizeBreadcrumbs { self.crumbs.capacity = 2; XCTAssertTrue(self.crumbs.count == 2); - XCTAssertEqualObjects(self.crumbs[0].message, @"Tap button"); - XCTAssertEqualObjects(self.crumbs[1].message, @"Close tutorial"); + XCTAssertEqualObjects(self.crumbs[0].metadata[@"message"], @"Tap button"); + XCTAssertEqualObjects(self.crumbs[1].metadata[@"message"], @"Close tutorial"); XCTAssertNil(self.crumbs[2]); } @@ -69,14 +69,16 @@ - (void)testArrayValue { XCTAssertTrue(value.count == 3); NSDateFormatter *formatter = [NSDateFormatter new]; formatter.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssX5"; - for (NSArray* item in value) { - XCTAssertTrue([item isKindOfClass:[NSArray class]]); - XCTAssertTrue(item.count == 2); - XCTAssertTrue([[formatter dateFromString:item[0]] isKindOfClass:[NSDate class]]); + for (int i = 0; i < value.count; i++) { + NSDictionary *item = value[i]; + XCTAssertTrue([item isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(item[@"name"], @"Custom"); + XCTAssertEqualObjects(item[@"type"], @"custom"); + XCTAssertTrue([[formatter dateFromString:item[@"timestamp"]] isKindOfClass:[NSDate class]]); } - XCTAssertEqualObjects(value[0][1], @"Launch app"); - XCTAssertEqualObjects(value[1][1], @"Tap button"); - XCTAssertEqualObjects(value[2][1], @"Close tutorial"); + XCTAssertEqualObjects(value[0][@"metaData"][@"message"], @"Launch app"); + XCTAssertEqualObjects(value[1][@"metaData"][@"message"], @"Tap button"); + XCTAssertEqualObjects(value[2][@"metaData"][@"message"], @"Close tutorial"); } @end From 14ca6c2386119069e6c4c121a88adebf6d51086b Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 19 Jul 2016 11:21:11 -0700 Subject: [PATCH 3/5] Add automatic breadcrumb logging for common UI events --- Source/Bugsnag.h | 16 +++ Source/Bugsnag.m | 17 ++- Source/BugsnagBreadcrumb.h | 30 +++--- Source/BugsnagBreadcrumb.m | 49 +++++---- Source/BugsnagNotifier.h | 7 ++ Source/BugsnagNotifier.m | 191 ++++++++++++++++++++++++++------- Tests/BugsnagBreadcrumbsTest.m | 4 +- 7 files changed, 231 insertions(+), 83 deletions(-) diff --git a/Source/Bugsnag.h b/Source/Bugsnag.h index dc8d96883..338d5678c 100644 --- a/Source/Bugsnag.h +++ b/Source/Bugsnag.h @@ -161,6 +161,22 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; */ + (void)leaveBreadcrumbWithMessage:(NSString *_Nonnull)message; +/** + * Leave a "breadcrumb" log message with additional information about the + * environment at the time the breadcrumb was captured. + * + * @param block configuration block + */ ++ (void)leaveBreadcrumbWithBlock:(void(^ _Nonnull)(BugsnagBreadcrumb *_Nonnull))block; + +/** + * Leave a "breadcrumb" log message each time a notification with a provided + * name is received by the application + * + * @param notificationName name of the notification to capture + */ ++ (void)leaveBreadcrumbForNotificationName:(NSString *_Nonnull)notificationName; + /** * Set the maximum number of breadcrumbs to keep and sent to Bugsnag. * By default, we'll keep and send the 20 most recent breadcrumb log diff --git a/Source/Bugsnag.m b/Source/Bugsnag.m index 68687942d..a70015630 100644 --- a/Source/Bugsnag.m +++ b/Source/Bugsnag.m @@ -141,16 +141,27 @@ + (BOOL) bugsnagStarted { if (self.notifier == nil) { NSLog(@"Ensure you have started Bugsnag with startWithApiKey: before calling any other Bugsnag functions."); - return false; + return NO; } - return true; + return YES; } + (void) leaveBreadcrumbWithMessage:(NSString *)message { - [self.notifier.configuration.breadcrumbs addBreadcrumb:message]; + [self leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb * _Nonnull crumbs) { + crumbs.metadata = @{ @"message": message }; + }]; +} + ++ (void)leaveBreadcrumbWithBlock:(void(^ _Nonnull)(BugsnagBreadcrumb *_Nonnull))block { + BugsnagBreadcrumbs *crumbs = self.notifier.configuration.breadcrumbs; + [crumbs addBreadcrumbWithBlock:block]; [self.notifier serializeBreadcrumbs]; } ++ (void)leaveBreadcrumbForNotificationName:(NSString *_Nonnull)notificationName { + [self.notifier crumbleNotification:notificationName]; +} + + (void) setBreadcrumbCapacity:(NSUInteger)capacity { self.notifier.configuration.breadcrumbs.capacity = capacity; } diff --git a/Source/BugsnagBreadcrumb.h b/Source/BugsnagBreadcrumb.h index 7d976536f..b4e9414d8 100644 --- a/Source/BugsnagBreadcrumb.h +++ b/Source/BugsnagBreadcrumb.h @@ -37,7 +37,7 @@ typedef NS_ENUM(NSUInteger, BSGBreadcrumbType) { /** * Any breadcrumb sent via Bugsnag.leaveBreadcrumb() */ - BSGBreadcrumbTypeCustom, + BSGBreadcrumbTypeManual, /** * A call to Bugsnag.notify() (internal use only) */ @@ -69,22 +69,18 @@ typedef NS_ENUM(NSUInteger, BSGBreadcrumbType) { BSGBreadcrumbTypeUser, }; +@class BugsnagBreadcrumb; + +typedef void(^BSGBreadcrumbConfiguration)(BugsnagBreadcrumb *_Nonnull); + @interface BugsnagBreadcrumb : NSObject @property(readonly, nonatomic, nullable) NSDate *timestamp; -@property(readonly) BSGBreadcrumbType type; -@property(readonly, nonatomic, copy, nonnull) NSString *name; -@property(readonly, nonatomic, copy, nonnull) NSDictionary *metadata; +@property(readwrite) BSGBreadcrumbType type; +@property(readwrite, nonatomic, copy, nonnull) NSString *name; +@property(readwrite, nonatomic, copy, nonnull) NSDictionary *metadata; -- (instancetype _Nullable)initWithMessage:(NSString *_Nullable)message - timestamp:(NSDate *_Nullable)date - NS_DESIGNATED_INITIALIZER; - -- (instancetype _Nullable)initWithName:(NSString *_Nonnull)name - timestamp:(NSDate *_Nonnull)date - type:(BSGBreadcrumbType)type - metadata:(NSDictionary *_Nullable)metadata - NS_DESIGNATED_INITIALIZER; ++ (instancetype _Nullable)breadcrumbWithBlock:(BSGBreadcrumbConfiguration _Nonnull)block; @end @@ -105,6 +101,14 @@ typedef NS_ENUM(NSUInteger, BSGBreadcrumbType) { */ - (void)addBreadcrumb:(NSString *_Nonnull)breadcrumbMessage; +/** + * Store a new breadcrumb configured via block. Must be called from the main + * thread + * + * @param block configuration block + */ +- (void)addBreadcrumbWithBlock:(void(^ _Nonnull)(BugsnagBreadcrumb *_Nonnull))block; + /** * Clear all stored breadcrumbs. Must be called from the main thread. */ diff --git a/Source/BugsnagBreadcrumb.m b/Source/BugsnagBreadcrumb.m index e748780a1..470dbce72 100644 --- a/Source/BugsnagBreadcrumb.m +++ b/Source/BugsnagBreadcrumb.m @@ -26,7 +26,7 @@ #import "BugsnagBreadcrumb.h" #import "Bugsnag.h" -NSString *const BSGBreadcrumbDefaultName = @"Custom"; +NSString *const BSGBreadcrumbDefaultName = @"manual"; NSString *BSGBreadcrumbTypeValue(BSGBreadcrumbType type) { switch (type) { @@ -38,8 +38,8 @@ return @"error"; case BSGBreadcrumbTypeState: return @"state"; - case BSGBreadcrumbTypeCustom: - return @"custom"; + case BSGBreadcrumbTypeManual: + return @"manual"; case BSGBreadcrumbTypeProcess: return @"process"; case BSGBreadcrumbTypeRequest: @@ -57,34 +57,27 @@ @interface BugsnagBreadcrumbs() @implementation BugsnagBreadcrumb - (instancetype)init { - self = [self initWithMessage:nil timestamp:nil]; - return self; -} - -- (instancetype)initWithMessage:(NSString *)message timestamp:(NSDate *)date { - if (message.length == 0) - return nil; - if (self = [super init]) { - _metadata = @{@"message": message}; - _timestamp = date; - _type = BSGBreadcrumbTypeCustom; + _timestamp = [NSDate date]; _name = BSGBreadcrumbDefaultName; + _type = BSGBreadcrumbTypeManual; + _metadata = @{}; } return self; } -- (instancetype)initWithName:(NSString *)name - timestamp:(NSDate *)date - type:(BSGBreadcrumbType)type - metadata:(NSDictionary *)metadata { - if (self = [super init]) { - _name = name; - _timestamp = date; - _type = type; - _metadata = metadata; +- (BOOL)isValid { + return self.name.length > 0 && self.timestamp != nil; +} + ++ (instancetype)breadcrumbWithBlock:(BSGBreadcrumbConfiguration)block { + BugsnagBreadcrumb *crumb = [self new]; + if (block) + block(crumb); + if ([crumb isValid]) { + return crumb; } - return self; + return nil; } @end @@ -102,11 +95,17 @@ - (instancetype)init { } - (void)addBreadcrumb:(NSString *)breadcrumbMessage { + [self addBreadcrumbWithBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + crumb.metadata = @{ @"message": breadcrumbMessage }; + }]; +} + +- (void)addBreadcrumbWithBlock:(void(^ _Nonnull)(BugsnagBreadcrumb *_Nonnull))block { NSAssert([[NSThread currentThread] isMainThread], @"Breadcrumbs must be mutated on the main thread."); if (self.capacity == 0) { return; } - BugsnagBreadcrumb* crumb = [[BugsnagBreadcrumb alloc] initWithMessage:breadcrumbMessage timestamp:[NSDate date]]; + BugsnagBreadcrumb* crumb = [BugsnagBreadcrumb breadcrumbWithBlock:block]; if (crumb) { [self resizeToFitCapacity:self.capacity - 1]; [self.breadcrumbs addObject:crumb]; diff --git a/Source/BugsnagNotifier.h b/Source/BugsnagNotifier.h index 476f60206..70002f802 100644 --- a/Source/BugsnagNotifier.h +++ b/Source/BugsnagNotifier.h @@ -74,4 +74,11 @@ * Write breadcrumbs to the cached metadata for error reports */ - (void)serializeBreadcrumbs; + +/** + * Listen for notifications and attach breadcrumbs when received + * + * @param notificationName name of the notification + */ +- (void)crumbleNotification:(NSString *_Nonnull)notificationName; @end diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index a89238cd6..878224684 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -45,6 +45,7 @@ NSString *const BSAttributeSeverity = @"severity"; NSString *const BSAttributeDepth = @"depth"; NSString *const BSAttributeBreadcrumbs = @"breadcrumbs"; +NSString *const BSEventLowMemoryWarning = @"lowMemoryWarning"; struct bugsnag_data_t { // Contains the user-specified metaData, including the user tab from config. @@ -151,20 +152,73 @@ - (void) start { [self performSelectorInBackground:@selector(sendPendingReports) withObject:nil]; #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE - [self.details setValue: @"iOS Bugsnag Notifier" forKey:@"name"]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryChanged:) name:UIDeviceBatteryStateDidChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryChanged:) name:UIDeviceBatteryLevelDidChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(lowMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - - [UIDevice currentDevice].batteryMonitoringEnabled = TRUE; + [self.details setValue:@"iOS Bugsnag Notifier" forKey:@"name"]; + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(batteryChanged:) + name:UIDeviceBatteryStateDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(batteryChanged:) + name:UIDeviceBatteryLevelDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(lowMemoryWarning:) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + [self crumbleNotification:UIWindowDidBecomeHiddenNotification]; + [self crumbleNotification:UIWindowDidBecomeVisibleNotification]; + [self crumbleNotification:UIApplicationWillTerminateNotification]; + [self crumbleNotification:UIApplicationWillEnterForegroundNotification]; + [self crumbleNotification:UIApplicationDidEnterBackgroundNotification]; + [self crumbleNotification:UIApplicationUserDidTakeScreenshotNotification]; + [self crumbleNotification:UIKeyboardDidShowNotification]; + [self crumbleNotification:UIKeyboardDidHideNotification]; + [self crumbleNotification:UITextFieldTextDidBeginEditingNotification]; + [self crumbleNotification:UITextViewTextDidBeginEditingNotification]; + [self crumbleNotification:UITextFieldTextDidEndEditingNotification]; + [self crumbleNotification:UITextViewTextDidEndEditingNotification]; + [self crumbleNotification:UIMenuControllerDidShowMenuNotification]; + [self crumbleNotification:UIMenuControllerDidHideMenuNotification]; + [self crumbleNotification:NSUndoManagerDidUndoChangeNotification]; + [self crumbleNotification:NSUndoManagerDidRedoChangeNotification]; + [self crumbleNotification:UITableViewSelectionDidChangeNotification]; + + [UIDevice currentDevice].batteryMonitoringEnabled = YES; [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; [self batteryChanged:nil]; [self orientationChanged:nil]; #elif TARGET_OS_MAC - [self.details setValue: @"OSX Bugsnag Notifier" forKey:@"name"]; + [self.details setValue:@"OSX Bugsnag Notifier" forKey:@"name"]; + [self crumbleNotification:NSApplicationDidBecomeActiveNotification]; + [self crumbleNotification:NSApplicationDidResignActiveNotification]; + [self crumbleNotification:NSApplicationDidHideNotification]; + [self crumbleNotification:NSApplicationDidUnhideNotification]; + [self crumbleNotification:NSApplicationWillTerminateNotification]; + [self crumbleNotification:NSWorkspaceScreensDidSleepNotification]; + [self crumbleNotification:NSWorkspaceScreensDidWakeNotification]; + [self crumbleNotification:NSWindowWillCloseNotification]; + [self crumbleNotification:NSWindowDidBecomeKeyNotification]; + [self crumbleNotification:NSWindowWillMiniaturizeNotification]; + [self crumbleNotification:NSWindowDidEnterFullScreenNotification]; + [self crumbleNotification:NSWindowDidExitFullScreenNotification]; + [self crumbleNotification:NSControlTextDidBeginEditingNotification]; + [self crumbleNotification:NSControlTextDidEndEditingNotification]; + [self crumbleNotification:NSTableViewSelectionDidChangeNotification]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(didReceiveMenuAction:) + name:NSMenuWillSendActionNotification + object:nil]; #endif } @@ -214,6 +268,10 @@ - (void)notify:(NSString *)exceptionName [self.metaDataLock unlock]; [self metaDataChanged:self.configuration.metaData]; [[self state] clearTab:BSTabCrash]; + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + crumb.type = BSGBreadcrumbTypeError; + crumb.name = exceptionName; + }]; [self performSelectorInBackground:@selector(sendPendingReports) withObject:nil]; } @@ -257,50 +315,103 @@ - (void) metaDataChanged:(BugsnagMetaData *)metaData { } #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE -- (void) batteryChanged:(NSNotification *)notif { - NSNumber *batteryLevel = [NSNumber numberWithFloat:[UIDevice currentDevice].batteryLevel]; - NSNumber *charging = [NSNumber numberWithBool: [UIDevice currentDevice].batteryState == UIDeviceBatteryStateCharging]; - - [[self state] addAttribute: @"batteryLevel" withValue: batteryLevel toTabWithName:@"deviceState"]; - [[self state] addAttribute: @"charging" withValue: charging toTabWithName:@"deviceState"]; +- (void)batteryChanged:(NSNotification *)notif { + NSNumber *batteryLevel = + [NSNumber numberWithFloat:[UIDevice currentDevice].batteryLevel]; + NSNumber *charging = + [NSNumber numberWithBool:[UIDevice currentDevice].batteryState == + UIDeviceBatteryStateCharging]; + + [[self state] addAttribute:@"batteryLevel" + withValue:batteryLevel + toTabWithName:@"deviceState"]; + [[self state] addAttribute:@"charging" + withValue:charging + toTabWithName:@"deviceState"]; } - (void)orientationChanged:(NSNotification *)notif { NSString *orientation; - switch([UIDevice currentDevice].orientation) { - case UIDeviceOrientationPortraitUpsideDown: - orientation = @"portraitupsidedown"; - break; - case UIDeviceOrientationPortrait: - orientation = @"portrait"; - break; - case UIDeviceOrientationLandscapeRight: - orientation = @"landscaperight"; - break; - case UIDeviceOrientationLandscapeLeft: - orientation = @"landscapeleft"; - break; - case UIDeviceOrientationFaceUp: - orientation = @"faceup"; - break; - case UIDeviceOrientationFaceDown: - orientation = @"facedown"; - break; - case UIDeviceOrientationUnknown: - default: - orientation = @"unknown"; + switch ([UIDevice currentDevice].orientation) { + case UIDeviceOrientationPortraitUpsideDown: + orientation = @"portraitupsidedown"; + break; + case UIDeviceOrientationPortrait: + orientation = @"portrait"; + break; + case UIDeviceOrientationLandscapeRight: + orientation = @"landscaperight"; + break; + case UIDeviceOrientationLandscapeLeft: + orientation = @"landscapeleft"; + break; + case UIDeviceOrientationFaceUp: + orientation = @"faceup"; + break; + case UIDeviceOrientationFaceDown: + orientation = @"facedown"; + break; + case UIDeviceOrientationUnknown: + default: + orientation = @"unknown"; } - [[self state] addAttribute:@"orientation" withValue:orientation toTabWithName:@"deviceState"]; + [[self state] addAttribute:@"orientation" + withValue:orientation + toTabWithName:@"deviceState"]; + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; + breadcrumb.metadata = @{ @"orientation" : orientation }; + }]; } - (void)lowMemoryWarning:(NSNotification *)notif { - [[self state] addAttribute: @"lowMemoryWarning" withValue: [[Bugsnag payloadDateFormatter] stringFromDate:[NSDate date]] toTabWithName:@"deviceState"]; + [[self state] addAttribute:BSEventLowMemoryWarning + withValue:[[Bugsnag payloadDateFormatter] + stringFromDate:[NSDate date]] + toTabWithName:@"deviceState"]; + [self sendBreadcrumbForNotification:notif]; +} + +#elif TARGET_OS_MAC +- (void)didReceiveMenuAction:(NSNotification *)notif { + NSMenuItem *menuItem = [[notif userInfo] valueForKey:@"MenuItem"]; + if ([menuItem isKindOfClass:[NSMenuItem class]]) { + [Bugsnag + leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; + if (menuItem.title.length > 0) + breadcrumb.metadata = @{ @"action" : menuItem.title }; + }]; + } +} +#endif + +- (void)crumbleNotification:(NSString *)notificationName { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(sendBreadcrumbForNotification:) + name:notificationName + object:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -#endif + +- (void)sendBreadcrumbForNotification:(NSNotification *)note { + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:note.name]; + }]; + [self serializeBreadcrumbs]; +} + +- (NSString *)breadcrumbNameForNotificationName:(NSString *)name { + return [name stringByReplacingOccurrencesOfString:@"Notification" + withString:@""]; +} @end diff --git a/Tests/BugsnagBreadcrumbsTest.m b/Tests/BugsnagBreadcrumbsTest.m index c89b180c3..a068b8a6b 100644 --- a/Tests/BugsnagBreadcrumbsTest.m +++ b/Tests/BugsnagBreadcrumbsTest.m @@ -72,8 +72,8 @@ - (void)testArrayValue { for (int i = 0; i < value.count; i++) { NSDictionary *item = value[i]; XCTAssertTrue([item isKindOfClass:[NSDictionary class]]); - XCTAssertEqualObjects(item[@"name"], @"Custom"); - XCTAssertEqualObjects(item[@"type"], @"custom"); + XCTAssertEqualObjects(item[@"name"], @"manual"); + XCTAssertEqualObjects(item[@"type"], @"manual"); XCTAssertTrue([[formatter dateFromString:item[@"timestamp"]] isKindOfClass:[NSDate class]]); } XCTAssertEqualObjects(value[0][@"metaData"][@"message"], @"Launch app"); From debc6abfcacae93b5b77f34882d9b05b5b9565b4 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 19 Jul 2016 11:48:04 -0700 Subject: [PATCH 4/5] Wrap automatic collection --- Source/BugsnagConfiguration.h | 7 ++ Source/BugsnagConfiguration.m | 17 ++- Source/BugsnagNotifier.h | 5 + Source/BugsnagNotifier.m | 194 ++++++++++++++++++++++++---------- 4 files changed, 167 insertions(+), 56 deletions(-) diff --git a/Source/BugsnagConfiguration.h b/Source/BugsnagConfiguration.h index fdeed4616..ddb30065c 100644 --- a/Source/BugsnagConfiguration.h +++ b/Source/BugsnagConfiguration.h @@ -90,6 +90,7 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( * The version of the application */ @property(nonatomic, readwrite, retain, nullable) NSString *appVersion; + /** * Additional information about the state of the app or environment at the * time the report was generated @@ -104,6 +105,12 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( */ @property(nonatomic, readonly, strong, nullable) BugsnagBreadcrumbs *breadcrumbs; + +/** + * Whether to allow collection of automatic breadcrumbs for notable events + */ +@property(nonatomic, readwrite) BOOL automaticallyCollectBreadcrumbs; + /** * Hooks for modifying crash reports before it is sent to Bugsnag */ diff --git a/Source/BugsnagConfiguration.m b/Source/BugsnagConfiguration.m index e59997bfb..bfae933e6 100644 --- a/Source/BugsnagConfiguration.m +++ b/Source/BugsnagConfiguration.m @@ -29,6 +29,12 @@ #import "BugsnagBreadcrumb.h" #import "BugsnagConfiguration.h" #import "BugsnagMetaData.h" +#import "Bugsnag.h" +#import "BugsnagNotifier.h" + +@interface Bugsnag () ++ (BugsnagNotifier*)notifier; +@end @interface BugsnagConfiguration () @property(nonatomic, readwrite, strong) NSMutableArray *beforeNotifyHooks; @@ -42,12 +48,13 @@ - (id)init { _metaData = [[BugsnagMetaData alloc] init]; _config = [[BugsnagMetaData alloc] init]; _apiKey = @""; - _autoNotify = true; + _autoNotify = YES; _notifyURL = [NSURL URLWithString:@"https://notify.bugsnag.com/"]; _beforeNotifyHooks = [NSMutableArray new]; _BugsnagBeforeSendBlock = [NSMutableArray new]; _notifyReleaseStages = nil; _breadcrumbs = [BugsnagBreadcrumbs new]; + _automaticallyCollectBreadcrumbs = YES; #if DEBUG _releaseStage = @"development"; #else @@ -96,6 +103,14 @@ - (void)setNotifyReleaseStages:(NSArray *)newNotifyReleaseStages; toTabWithName:@"config"]; } +- (void)setAutomaticallyCollectBreadcrumbs:(BOOL)automaticallyCollectBreadcrumbs { + if (automaticallyCollectBreadcrumbs == _automaticallyCollectBreadcrumbs) + return; + + _automaticallyCollectBreadcrumbs = automaticallyCollectBreadcrumbs; + [[Bugsnag notifier] updateAutomaticBreadcrumbDetectionSettings]; +} + - (void)setContext:(NSString *)newContext { _context = newContext; [self.config addAttribute:@"context" diff --git a/Source/BugsnagNotifier.h b/Source/BugsnagNotifier.h index 70002f802..305e1a65d 100644 --- a/Source/BugsnagNotifier.h +++ b/Source/BugsnagNotifier.h @@ -81,4 +81,9 @@ * @param notificationName name of the notification */ - (void)crumbleNotification:(NSString *_Nonnull)notificationName; + +/** + * Enable or disable automatic breadcrumb collection based on configuration + */ +- (void)updateAutomaticBreadcrumbDetectionSettings; @end diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index 878224684..f14e27005 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -36,6 +36,8 @@ #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE #import #include +#elif TARGET_OS_MAC +#import #endif NSString *const NOTIFIER_VERSION = @"5.3.0"; @@ -150,7 +152,7 @@ - (void) start { } [self performSelectorInBackground:@selector(sendPendingReports) withObject:nil]; - + [self updateAutomaticBreadcrumbDetectionSettings]; #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE [self.details setValue:@"iOS Bugsnag Notifier" forKey:@"name"]; @@ -174,23 +176,6 @@ - (void) start { selector:@selector(lowMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; - [self crumbleNotification:UIWindowDidBecomeHiddenNotification]; - [self crumbleNotification:UIWindowDidBecomeVisibleNotification]; - [self crumbleNotification:UIApplicationWillTerminateNotification]; - [self crumbleNotification:UIApplicationWillEnterForegroundNotification]; - [self crumbleNotification:UIApplicationDidEnterBackgroundNotification]; - [self crumbleNotification:UIApplicationUserDidTakeScreenshotNotification]; - [self crumbleNotification:UIKeyboardDidShowNotification]; - [self crumbleNotification:UIKeyboardDidHideNotification]; - [self crumbleNotification:UITextFieldTextDidBeginEditingNotification]; - [self crumbleNotification:UITextViewTextDidBeginEditingNotification]; - [self crumbleNotification:UITextFieldTextDidEndEditingNotification]; - [self crumbleNotification:UITextViewTextDidEndEditingNotification]; - [self crumbleNotification:UIMenuControllerDidShowMenuNotification]; - [self crumbleNotification:UIMenuControllerDidHideMenuNotification]; - [self crumbleNotification:NSUndoManagerDidUndoChangeNotification]; - [self crumbleNotification:NSUndoManagerDidRedoChangeNotification]; - [self crumbleNotification:UITableViewSelectionDidChangeNotification]; [UIDevice currentDevice].batteryMonitoringEnabled = YES; [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; @@ -199,26 +184,6 @@ - (void) start { [self orientationChanged:nil]; #elif TARGET_OS_MAC [self.details setValue:@"OSX Bugsnag Notifier" forKey:@"name"]; - [self crumbleNotification:NSApplicationDidBecomeActiveNotification]; - [self crumbleNotification:NSApplicationDidResignActiveNotification]; - [self crumbleNotification:NSApplicationDidHideNotification]; - [self crumbleNotification:NSApplicationDidUnhideNotification]; - [self crumbleNotification:NSApplicationWillTerminateNotification]; - [self crumbleNotification:NSWorkspaceScreensDidSleepNotification]; - [self crumbleNotification:NSWorkspaceScreensDidWakeNotification]; - [self crumbleNotification:NSWindowWillCloseNotification]; - [self crumbleNotification:NSWindowDidBecomeKeyNotification]; - [self crumbleNotification:NSWindowWillMiniaturizeNotification]; - [self crumbleNotification:NSWindowDidEnterFullScreenNotification]; - [self crumbleNotification:NSWindowDidExitFullScreenNotification]; - [self crumbleNotification:NSControlTextDidBeginEditingNotification]; - [self crumbleNotification:NSControlTextDidEndEditingNotification]; - [self crumbleNotification:NSTableViewSelectionDidChangeNotification]; - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(didReceiveMenuAction:) - name:NSMenuWillSendActionNotification - object:nil]; #endif } @@ -358,11 +323,13 @@ - (void)orientationChanged:(NSNotification *)notif { [[self state] addAttribute:@"orientation" withValue:orientation toTabWithName:@"deviceState"]; - [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeState; - breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; - breadcrumb.metadata = @{ @"orientation" : orientation }; - }]; + if ([self.configuration automaticallyCollectBreadcrumbs]) { + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; + breadcrumb.metadata = @{ @"orientation" : orientation }; + }]; + } } - (void)lowMemoryWarning:(NSNotification *)notif { @@ -370,23 +337,100 @@ - (void)lowMemoryWarning:(NSNotification *)notif { withValue:[[Bugsnag payloadDateFormatter] stringFromDate:[NSDate date]] toTabWithName:@"deviceState"]; - [self sendBreadcrumbForNotification:notif]; + if ([self.configuration automaticallyCollectBreadcrumbs]) { + [self sendBreadcrumbForNotification:notif]; + } +} +#endif + +- (void)updateAutomaticBreadcrumbDetectionSettings { + if ([self.configuration automaticallyCollectBreadcrumbs]) { + for (NSString *name in [self automaticBreadcrumbStateEvents]) { + [self crumbleNotification:name]; + } + for (NSString *name in [self automaticBreadcrumbControlEvents]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(sendBreadcrumbForControlNotification:) + name:name + object:nil]; + } + for (NSString *name in [self automaticBreadcrumbMenuItemEvents]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(sendBreadcrumbForMenuItemNotification:) + name:name + object:nil]; + } + } else { + NSArray* eventNames = [[[self automaticBreadcrumbStateEvents] + arrayByAddingObjectsFromArray:[self automaticBreadcrumbControlEvents]] + arrayByAddingObjectsFromArray:[self automaticBreadcrumbMenuItemEvents]]; + for (NSString *name in eventNames) { + [[NSNotificationCenter defaultCenter] removeObserver:self + name:name + object:nil]; + } + } } +- (NSArray *)automaticBreadcrumbStateEvents { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + return @[UIWindowDidBecomeHiddenNotification, + UIWindowDidBecomeVisibleNotification, + UIApplicationWillTerminateNotification, + UIApplicationWillEnterForegroundNotification, + UIApplicationDidEnterBackgroundNotification, + UIApplicationUserDidTakeScreenshotNotification, + UIKeyboardDidShowNotification, + UIKeyboardDidHideNotification, + UIMenuControllerDidShowMenuNotification, + UIMenuControllerDidHideMenuNotification, + NSUndoManagerDidUndoChangeNotification, + NSUndoManagerDidRedoChangeNotification, + UITableViewSelectionDidChangeNotification]; #elif TARGET_OS_MAC -- (void)didReceiveMenuAction:(NSNotification *)notif { - NSMenuItem *menuItem = [[notif userInfo] valueForKey:@"MenuItem"]; - if ([menuItem isKindOfClass:[NSMenuItem class]]) { - [Bugsnag - leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { - breadcrumb.type = BSGBreadcrumbTypeState; - breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; - if (menuItem.title.length > 0) - breadcrumb.metadata = @{ @"action" : menuItem.title }; - }]; - } + return @[NSApplicationDidBecomeActiveNotification, + NSApplicationDidResignActiveNotification, + NSApplicationDidHideNotification, + NSApplicationDidUnhideNotification, + NSApplicationWillTerminateNotification, + NSWorkspaceScreensDidSleepNotification, + NSWorkspaceScreensDidWakeNotification, + NSWindowWillCloseNotification, + NSWindowDidBecomeKeyNotification, + NSWindowWillMiniaturizeNotification, + NSWindowDidEnterFullScreenNotification, + NSWindowDidExitFullScreenNotification, + NSTableViewSelectionDidChangeNotification]; +#else + return nil; +#endif } + +- (NSArray *)automaticBreadcrumbControlEvents { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + return @[UITextFieldTextDidBeginEditingNotification, + UITextViewTextDidBeginEditingNotification, + UITextFieldTextDidEndEditingNotification, + UITextViewTextDidEndEditingNotification]; +#elif TARGET_OS_MAC + return @[NSControlTextDidBeginEditingNotification, + NSControlTextDidEndEditingNotification]; +#else + return nil; +#endif +} + +- (NSArray *)automaticBreadcrumbMenuItemEvents { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + return nil; +#elif TARGET_OS_MAC + return @[NSMenuWillSendActionNotification]; +#else + return nil; #endif +} - (void)crumbleNotification:(NSString *)notificationName { [[NSNotificationCenter defaultCenter] @@ -408,6 +452,46 @@ - (void)sendBreadcrumbForNotification:(NSNotification *)note { [self serializeBreadcrumbs]; } +- (void)sendBreadcrumbForMenuItemNotification:(NSNotification *)notif { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE +#elif TARGET_OS_MAC + NSMenuItem *menuItem = [[notif userInfo] valueForKey:@"MenuItem"]; + if ([menuItem isKindOfClass:[NSMenuItem class]]) { + [Bugsnag + leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; + if (menuItem.title.length > 0) + breadcrumb.metadata = @{ @"action" : menuItem.title }; + }]; + } +#endif +} + +- (void)sendBreadcrumbForControlNotification:(NSNotification *)note { +#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE + UIControl* control = note.object; + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeUser; + breadcrumb.name = [self breadcrumbNameForNotificationName:note.name]; + NSString *label = control.accessibilityLabel; + if (label.length > 0) { + breadcrumb.metadata = @{ @"label": label }; + } + }]; +#elif TARGET_OS_MAC + NSControl *control = note.object; + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeUser; + breadcrumb.name = [self breadcrumbNameForNotificationName:note.name]; + NSString *label = control.accessibilityLabel; + if (label.length > 0) { + breadcrumb.metadata = @{ @"label": label }; + } + }]; +#endif +} + - (NSString *)breadcrumbNameForNotificationName:(NSString *)name { return [name stringByReplacingOccurrencesOfString:@"Notification" withString:@""]; From 47c99850bba4d8e14081f2e30bf0b35dc1458b98 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 21 Jul 2016 11:56:06 -0700 Subject: [PATCH 5/5] Enforce size limit --- Source/BugsnagBreadcrumb.m | 42 ++++++++++++++++++++++++++-------- Tests/BugsnagBreadcrumbsTest.m | 28 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/Source/BugsnagBreadcrumb.m b/Source/BugsnagBreadcrumb.m index 470dbce72..fdba3dfbe 100644 --- a/Source/BugsnagBreadcrumb.m +++ b/Source/BugsnagBreadcrumb.m @@ -27,6 +27,7 @@ #import "Bugsnag.h" NSString *const BSGBreadcrumbDefaultName = @"manual"; +NSUInteger const BSGBreadcrumbMaxByteSize = 4096; NSString *BSGBreadcrumbTypeValue(BSGBreadcrumbType type) { switch (type) { @@ -52,6 +53,12 @@ @interface BugsnagBreadcrumbs() @property (nonatomic,readwrite,strong) NSMutableArray* breadcrumbs; + +@end + +@interface BugsnagBreadcrumb () + +- (NSDictionary *_Nullable)objectValue; @end @implementation BugsnagBreadcrumb @@ -70,6 +77,21 @@ - (BOOL)isValid { return self.name.length > 0 && self.timestamp != nil; } +- (NSDictionary *)objectValue { + NSString* timestamp = [[Bugsnag payloadDateFormatter] stringFromDate:self.timestamp]; + if (timestamp && self.name.length > 0) { + NSMutableDictionary *data = @{ + @"name": self.name, + @"timestamp": timestamp, + @"type": BSGBreadcrumbTypeValue(self.type) + }.mutableCopy; + if (self.metadata) + data[@"metaData"] = self.metadata; + return data; + } + return nil; +} + + (instancetype)breadcrumbWithBlock:(BSGBreadcrumbConfiguration)block { BugsnagBreadcrumb *crumb = [self new]; if (block) @@ -145,16 +167,16 @@ - (NSArray *)arrayValue { } NSMutableArray* contents = [[NSMutableArray alloc] initWithCapacity:[self count]]; for (BugsnagBreadcrumb* crumb in self.breadcrumbs) { - NSString* timestamp = [[Bugsnag payloadDateFormatter] stringFromDate:crumb.timestamp]; - if (timestamp && crumb.name.length > 0) { - NSMutableDictionary *data = @{ - @"name": crumb.name, - @"timestamp": timestamp, - @"type": BSGBreadcrumbTypeValue(crumb.type) - }.mutableCopy; - if (crumb.metadata) - data[@"metaData"] = crumb.metadata; - [contents addObject:data]; + NSDictionary *objectValue = [crumb objectValue]; + NSError *error = nil; + @try { + NSData* data = [NSJSONSerialization dataWithJSONObject:objectValue options:0 error:&error]; + if (data.length <= BSGBreadcrumbMaxByteSize) + [contents addObject:objectValue]; + else + NSLog(@"Dropping Bugsnag breadcrumb (%@) exceeding %lu byte size limit", crumb.name, (unsigned long)BSGBreadcrumbMaxByteSize); + } @catch (NSException *exception) { + NSLog(@"Unable to serialize breadcrumb for Bugsnag: %@", error); } } return contents; diff --git a/Tests/BugsnagBreadcrumbsTest.m b/Tests/BugsnagBreadcrumbsTest.m index a068b8a6b..cf583e1dd 100644 --- a/Tests/BugsnagBreadcrumbsTest.m +++ b/Tests/BugsnagBreadcrumbsTest.m @@ -81,4 +81,32 @@ - (void)testArrayValue { XCTAssertEqualObjects(value[2][@"metaData"][@"message"], @"Close tutorial"); } +- (void)testStateType { + BugsnagBreadcrumbs* crumbs = [BugsnagBreadcrumbs new]; + [crumbs addBreadcrumbWithBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + crumb.type = BSGBreadcrumbTypeState; + crumb.name = @"Rotated Menu"; + crumb.metadata = @{ @"direction": @"right" }; + }]; + NSArray* value = [crumbs arrayValue]; + XCTAssertEqualObjects(value[0][@"metaData"][@"direction"], @"right"); + XCTAssertEqualObjects(value[0][@"name"], @"Rotated Menu"); + XCTAssertEqualObjects(value[0][@"type"], @"state"); +} + +- (void)testByteSizeLimit { + BugsnagBreadcrumbs* crumbs = [BugsnagBreadcrumbs new]; + [crumbs addBreadcrumbWithBlock:^(BugsnagBreadcrumb * _Nonnull crumb) { + crumb.type = BSGBreadcrumbTypeState; + crumb.name = @"Rotated Menu"; + NSMutableDictionary *metadata = @{}.mutableCopy; + for (int i = 0; i < 400; i++) { + metadata[[NSString stringWithFormat:@"%d", i]] = @"!!"; + } + crumb.metadata = metadata; + }]; + NSArray* value = [crumbs arrayValue]; + XCTAssertTrue(value.count == 0); +} + @end