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 55963275b..b4e9414d8 100644 --- a/Source/BugsnagBreadcrumb.h +++ b/Source/BugsnagBreadcrumb.h @@ -33,14 +33,55 @@ #endif #endif +typedef NS_ENUM(NSUInteger, BSGBreadcrumbType) { + /** + * Any breadcrumb sent via Bugsnag.leaveBreadcrumb() + */ + BSGBreadcrumbTypeManual, + /** + * 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, +}; + +@class BugsnagBreadcrumb; + +typedef void(^BSGBreadcrumbConfiguration)(BugsnagBreadcrumb *_Nonnull); + @interface BugsnagBreadcrumb : NSObject -@property(readonly, nullable) NSDate *timestamp; -@property(readonly, copy, nullable) NSString *message; +@property(readonly, nonatomic, nullable) NSDate *timestamp; +@property(readwrite) BSGBreadcrumbType type; +@property(readwrite, nonatomic, copy, nonnull) NSString *name; +@property(readwrite, nonatomic, copy, nonnull) NSDictionary *metadata; + ++ (instancetype _Nullable)breadcrumbWithBlock:(BSGBreadcrumbConfiguration _Nonnull)block; -- (instancetype _Nullable)initWithMessage:(NSString *_Nullable)message - timestamp:(NSDate *_Nullable)date - NS_DESIGNATED_INITIALIZER; @end @interface BugsnagBreadcrumbs : NSObject @@ -60,6 +101,14 @@ */ - (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 2311a8b60..fdba3dfbe 100644 --- a/Source/BugsnagBreadcrumb.m +++ b/Source/BugsnagBreadcrumb.m @@ -26,27 +26,80 @@ #import "BugsnagBreadcrumb.h" #import "Bugsnag.h" +NSString *const BSGBreadcrumbDefaultName = @"manual"; +NSUInteger const BSGBreadcrumbMaxByteSize = 4096; + +NSString *BSGBreadcrumbTypeValue(BSGBreadcrumbType type) { + switch (type) { + case BSGBreadcrumbTypeLog: + return @"log"; + case BSGBreadcrumbTypeUser: + return @"user"; + case BSGBreadcrumbTypeError: + return @"error"; + case BSGBreadcrumbTypeState: + return @"state"; + case BSGBreadcrumbTypeManual: + return @"manual"; + case BSGBreadcrumbTypeProcess: + return @"process"; + case BSGBreadcrumbTypeRequest: + return @"request"; + case BSGBreadcrumbTypeNavigation: + return @"navigation"; + } +} + @interface BugsnagBreadcrumbs() @property (nonatomic,readwrite,strong) NSMutableArray* breadcrumbs; + +@end + +@interface BugsnagBreadcrumb () + +- (NSDictionary *_Nullable)objectValue; @end @implementation BugsnagBreadcrumb - (instancetype)init { - self = [self initWithMessage:nil timestamp:nil]; + if (self = [super init]) { + _timestamp = [NSDate date]; + _name = BSGBreadcrumbDefaultName; + _type = BSGBreadcrumbTypeManual; + _metadata = @{}; + } return self; } -- (instancetype)initWithMessage:(NSString *)message timestamp:(NSDate *)date { - if (message.length == 0) - return nil; +- (BOOL)isValid { + return self.name.length > 0 && self.timestamp != nil; +} - if (self = [super init]) { - _message = [message copy]; - _timestamp = date; +- (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 self; + return nil; +} + ++ (instancetype)breadcrumbWithBlock:(BSGBreadcrumbConfiguration)block { + BugsnagBreadcrumb *crumb = [self new]; + if (block) + block(crumb); + if ([crumb isValid]) { + return crumb; + } + return nil; } @end @@ -64,11 +117,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]; @@ -108,9 +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.message.length > 0) { - [contents addObject:@[timestamp,crumb.message]]; + 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/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/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/Source/BugsnagNotifier.h b/Source/BugsnagNotifier.h index 476f60206..305e1a65d 100644 --- a/Source/BugsnagNotifier.h +++ b/Source/BugsnagNotifier.h @@ -74,4 +74,16 @@ * 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; + +/** + * Enable or disable automatic breadcrumb collection based on configuration + */ +- (void)updateAutomaticBreadcrumbDetectionSettings; @end diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index a89238cd6..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"; @@ -45,6 +47,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. @@ -149,22 +152,38 @@ - (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"]; - - [[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]; + + [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"]; #endif } @@ -214,6 +233,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 +280,222 @@ - (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"]; + if ([self.configuration automaticallyCollectBreadcrumbs]) { + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:notif.name]; + breadcrumb.metadata = @{ @"orientation" : orientation }; + }]; } - [[self state] addAttribute:@"orientation" withValue:orientation toTabWithName:@"deviceState"]; } - (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"]; + 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 + 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] + addObserver:self + selector:@selector(sendBreadcrumbForNotification:) + name:notificationName + object:nil]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } + +- (void)sendBreadcrumbForNotification:(NSNotification *)note { + [Bugsnag leaveBreadcrumbWithBlock:^(BugsnagBreadcrumb *_Nonnull breadcrumb) { + breadcrumb.type = BSGBreadcrumbTypeState; + breadcrumb.name = [self breadcrumbNameForNotificationName:note.name]; + }]; + [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:@""]; +} @end diff --git a/Tests/BugsnagBreadcrumbsTest.m b/Tests/BugsnagBreadcrumbsTest.m index 5dd1f4752..cf583e1dd 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,44 @@ - (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"], @"manual"); + XCTAssertEqualObjects(item[@"type"], @"manual"); + 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"); +} + +- (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 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 {