From 3a49950caa0f26cb3b25c0911279d3ad8ae0209e Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Tue, 28 Jun 2016 18:20:32 +0100 Subject: [PATCH 01/12] Move raw crash report data into writable properties --- Source/BugsnagCrashReport.h | 105 ++++++++++- Source/BugsnagCrashReport.m | 367 +++++++++++++++++++----------------- 2 files changed, 294 insertions(+), 178 deletions(-) diff --git a/Source/BugsnagCrashReport.h b/Source/BugsnagCrashReport.h index aaa941999..1e049b02f 100644 --- a/Source/BugsnagCrashReport.h +++ b/Source/BugsnagCrashReport.h @@ -8,15 +8,112 @@ #import -@interface BugsnagCrashReport : NSObject +@class BugsnagConfiguration; + +typedef NS_ENUM(NSUInteger, BSGSeverity) { + BSGSeverityError, + BSGSeverityWarning, + BSGSeverityInfo, +}; + +/** + * Convert a string to a severity value + * + * @param severity Intended severity value, such as info, warning, or error + * + * @return converted severity level or BSGSeverityError if no conversion is found + */ +BSGSeverity BSGParseSeverity(NSString *_Nonnull severity); + +/** + * Serialize a severity for JSON payloads + * + * @param severity a severity + * + * @return the equivalent string value + */ +NSString *_Nonnull BSGFormatSeverity(BSGSeverity severity); -@property(readonly, nonnull) NSDictionary *ksReport; +@interface BugsnagCrashReport : NSObject +/** + * Create a new crash report from a JSON crash report generated by KSCrash + * + * @param report a KSCrash JSON report + * + * @return a Bugsnag crash report + */ - (instancetype _Nonnull)initWithKSReport:(NSDictionary *_Nonnull)report; + +/** + * Create a basic crash report from raw parts + * + * @param name The name of the exception + * @param message The reason or message from the exception + * @param config Bugsnag configuration + * @param metaData additional data to attach to the report + * @param severity severity of the error + * + * @return a Bugsnag crash report + */ +- (instancetype _Nonnull)initWithErrorName:(NSString *_Nonnull)name + errorMessage:(NSString *_Nonnull)message + configuration:(BugsnagConfiguration *_Nonnull)config + metaData:(NSDictionary *_Nonnull)metaData + severity:(BSGSeverity)severity; + +/** + * Serialize a crash report as a JSON payload + * + * @param data top level report data, may need to be modified based on environment + * + * @return a crash report + */ - (NSDictionary *_Nonnull)serializableValueWithTopLevelData: (NSMutableDictionary *_Nonnull)data; -- (NSString *_Nullable)releaseStage; -- (NSArray *_Nullable)notifyReleaseStages; +/** + * Whether this report should be sent, based on release stage information + * cached at crash time and within the application currently + * + * @return YES if the report should be sent + */ +- (BOOL)shouldBeSent; + +/** + * The release stages used to notify at the time this report is captured + */ +@property (nonatomic, readwrite, copy, nullable) NSArray *notifyReleaseStages; +/** + * A loose representation of what was happening in the application at the time + * of the event + */ +@property (nonatomic, readwrite, copy, nullable) NSString *context; +/** + * The severity of the error generating the report + */ +@property (nonatomic, readwrite) BSGSeverity severity; +/** + * The release stage of the application + */ +@property (nonatomic, readwrite, copy, nullable) NSString *releaseStage; +/** + * The class of the error generating the report + */ +@property (nonatomic, readwrite, copy, nullable) NSString *errorClass; +/** + * The message of or reason for the error generating the report + */ +@property (nonatomic, readwrite, copy, nullable) NSString *errorMessage; +/** + * Breadcrumbs from user events leading up to the error + */ +@property (nonatomic, readwrite, copy, nullable) NSArray *breadcrumbs; +/** + * Further information attached to an error report, where each top level key + * generates a section on bugsnag, displaying key/value pairs + */ +@property (nonatomic, readwrite, copy, nonnull) NSDictionary *metaData; +@property (nonatomic, readwrite) NSUInteger depth; @end diff --git a/Source/BugsnagCrashReport.m b/Source/BugsnagCrashReport.m index c7fdba854..044445762 100644 --- a/Source/BugsnagCrashReport.m +++ b/Source/BugsnagCrashReport.m @@ -54,126 +54,219 @@ return nil; } -@implementation BugsnagCrashReport - -- (id)initWithKSReport:(NSDictionary *)report { - if ((self = [super init])) { - _ksReport = report; - } - return self; -} - -- (NSString *)releaseStage { - return self.config[@"releaseStage"]; -} - -- (NSArray *)notifyReleaseStages { - return self.config[@"notifyReleaseStages"]; -} - -- (NSString *)context { - if ([self.config[@"context"] isKindOfClass:[NSString class]]) { - return self.config[@"context"]; - } - // TODO:SM Get other contexts if possible - return nil; -} - -- (NSString *)appVersion { - if ([self.config[@"appVersion"] isKindOfClass:[NSString class]]) { - return self.config[@"appVersion"]; - } - return nil; -} - -- (NSArray *)binaryImages { - return self.ksReport[@"binary_images"]; -} - -- (NSArray *)threads { - return self.crash[@"threads"]; -} - -- (NSDictionary *)error { - return self.crash[@"error"]; -} - -- (NSString *)errorType { - return self.error[@"type"]; -} - -- (NSString *)errorClass { - if ([self.errorType isEqualToString:@"cpp_exception"]) { - return self.error[@"cpp_exception"][@"name"]; - } else if ([self.errorType isEqualToString:@"mach"]) { - return self.error[@"mach"][@"exception_name"]; - } else if ([self.errorType isEqualToString:@"signal"]) { - return self.error[@"signal"][@"name"]; - } else if ([self.errorType isEqualToString:@"nsexception"]) { - return self.error[@"nsexception"][@"name"]; - } else if ([self.errorType isEqualToString:@"user"]) { - return self.error[@"user_reported"][@"name"]; - } - return @"Exception"; +NSString *BSGParseErrorClass(NSDictionary *error, NSString *errorType) { + if ([errorType isEqualToString:@"cpp_exception"]) { + return error[@"cpp_exception"][@"name"]; + } else if ([errorType isEqualToString:@"mach"]) { + return error[@"mach"][@"exception_name"]; + } else if ([errorType isEqualToString:@"signal"]) { + return error[@"signal"][@"name"]; + } else if ([errorType isEqualToString:@"nsexception"]) { + return error[@"nsexception"][@"name"]; + } else if ([errorType isEqualToString:@"user"]) { + return error[@"user_reported"][@"name"]; + } + return @"Exception"; } -- (NSString *)errorMessage { - if ([self.errorType isEqualToString:@"mach"]) { - NSString *diagnosis = self.crash[@"diagnosis"]; - if (diagnosis && ![diagnosis hasPrefix:@"No diagnosis"]) { - return [[diagnosis componentsSeparatedByString:@"\n"] firstObject]; +NSString *BSGParseErrorMessage(NSDictionary *report, NSDictionary *error, NSString *errorType) { + if ([errorType isEqualToString:@"mach"]) { + NSString *diagnosis = [report valueForKeyPath:@"crash.diagnosis"]; + if (diagnosis && ![diagnosis hasPrefix:@"No diagnosis"]) { + return [[diagnosis componentsSeparatedByString:@"\n"] firstObject]; + } } - } - return self.error[@"reason"]; + return error[@"reason"]; } -- (NSArray *)breadcrumbs { - return self.state[@"crash"][@"breadcrumbs"]; +NSDictionary *BSGParseDevice(NSDictionary *report) { + NSDictionary *system = report[@"system"]; + NSMutableDictionary *device = [NSMutableDictionary dictionary]; + + BSGDictSetSafeObject(device, @"Apple", @"manufacturer"); + BSGDictSetSafeObject(device, [[NSLocale currentLocale] localeIdentifier], + @"locale"); + BSGDictSetSafeObject(device, system[@"device_app_hash"], @"id"); + BSGDictSetSafeObject(device, system[@"time_zone"], @"timezone"); + BSGDictSetSafeObject(device, system[@"model"], @"modelNumber"); + BSGDictSetSafeObject(device, system[@"machine"], @"model"); + BSGDictSetSafeObject(device, system[@"system_name"], @"osName"); + BSGDictSetSafeObject(device, system[@"system_version"], @"osVersion"); + BSGDictSetSafeObject(device, system[@"memory"][@"usable"], + @"totalMemory"); + + return device; } -- (NSString *)severity { - return self.state[@"crash"][@"severity"]; +NSDictionary *BSGParseApp(NSDictionary *report, NSString *appVersion) { + NSDictionary *system = report[@"system"]; + NSMutableDictionary *app = [NSMutableDictionary dictionary]; + + BSGDictSetSafeObject(app, system[@"CFBundleVersion"], @"bundleVersion"); + BSGDictSetSafeObject(app, system[@"CFBundleIdentifier"], @"id"); + BSGDictSetSafeObject(app, system[@"CFBundleExecutable"], @"name"); + BSGDictSetSafeObject(app, [Bugsnag configuration].releaseStage, + @"releaseStage"); + if ([appVersion isKindOfClass:[NSString class]]) { + BSGDictSetSafeObject(app, appVersion, @"version"); + } else { + BSGDictSetSafeObject(app, system[@"CFBundleShortVersionString"], + @"version"); + } + + return app; } -- (NSString *)dsymUUID { - return self.system[@"app_uuid"]; +NSDictionary *BSGParseAppState(NSDictionary *report) { + NSDictionary *appStats = report[@"system"][@"application_stats"]; + NSMutableDictionary *appState = [NSMutableDictionary dictionary]; + NSInteger activeTimeSinceLaunch = + [appStats[@"active_time_since_launch"] doubleValue] * 1000.0; + NSInteger backgroundTimeSinceLaunch = + [appStats[@"background_time_since_launch"] doubleValue] * 1000.0; + + BSGDictSetSafeObject(appState, @(activeTimeSinceLaunch), + @"durationInForeground"); + BSGDictSetSafeObject(appState, + @(activeTimeSinceLaunch + backgroundTimeSinceLaunch), + @"duration"); + BSGDictSetSafeObject(appState, appStats[@"application_in_foreground"], + @"inForeground"); + BSGDictSetSafeObject(appState, appStats, @"stats"); + + return appState; } -- (NSString *)deviceAppHash { - return self.system[@"device_app_hash"]; +NSDictionary *BSGParseDeviceState(NSDictionary *report) { + NSMutableDictionary *deviceState = + [[report valueForKeyPath:@"user.state.deviceState"] mutableCopy]; + BSGDictSetSafeObject(deviceState, + [report valueForKeyPath:@"system.memory.free"], + @"freeMemory"); + return deviceState; } -- (NSUInteger)depth { - return [self.state[@"crash"][@"depth"] unsignedIntegerValue]; +NSString *BSGParseContext(NSDictionary *report, NSDictionary *metaData) { + id context = metaData[@"context"]; + if ([context isKindOfClass:[NSString class]]) + return context; + context = [report valueForKeyPath:@"user.config.context"]; + if ([context isKindOfClass:[NSString class]]) + return context; + return nil; } -- (NSDictionary *)metaData { - return self.ksReport[@"user"][@"metaData"]; +BSGSeverity BSGParseSeverity(NSString *severity) { + if ([severity isEqualToString:@"info"]) + return BSGSeverityInfo; + else if ([severity isEqualToString:@"warning"]) + return BSGSeverityWarning; + return BSGSeverityError; } -- (NSDictionary *)appStats { - return self.system[@"application_stats"]; +NSString *BSGFormatSeverity(BSGSeverity severity) { + switch (severity) { + case BSGSeverityInfo: + return @"info"; + case BSGSeverityError: + return @"error"; + case BSGSeverityWarning: + return @"warning"; + } } -// PRIVATE -- (NSDictionary *)system { - return self.ksReport[@"system"]; -} +@interface BugsnagCrashReport () + +/** + * The type of the error, such as `mach` or `user` + */ +@property (nonatomic, readwrite, copy, nullable) NSString *errorType; +/** + * Raw error data + */ +@property (nonatomic, readwrite, copy, nullable) NSDictionary *error; +/** + * The UUID of the dSYM file + */ +@property (nonatomic, readonly, copy, nullable) NSString *dsymUUID; +/** + * A unique hash identifying this device for the application or vendor + */ +@property (nonatomic, readonly, copy, nullable) NSString *deviceAppHash; +/** + * Binary images used to identify application symbols + */ +@property (nonatomic, readonly, copy, nullable) NSArray *binaryImages; +/** + * Thread information captured at the time of the error + */ +@property (nonatomic, readonly, copy, nullable) NSArray *threads; +/** + * Device information such as OS name and version + */ +@property (nonatomic, readwrite, copy, nullable) NSDictionary *device; +/** + * Device state such as memory allocation at crash time + */ +@property (nonatomic, readwrite, copy, nullable) NSDictionary *deviceState; +/** + * App information such as the name, version, and bundle ID + */ +@property (nonatomic, readwrite, copy, nullable) NSDictionary *app; +/** + * Device state such as oreground status and run duration + */ +@property (nonatomic, readwrite, copy, nullable) NSDictionary *appState; +@end -- (NSDictionary *)state { - return self.ksReport[@"user"][@"state"]; -} +@implementation BugsnagCrashReport -- (NSDictionary *)config { - return self.ksReport[@"user"][@"config"]; +- (instancetype)initWithKSReport:(NSDictionary *)report { + if (self = [super init]) { + _notifyReleaseStages = [report valueForKeyPath:@"user.config.notifyReleaseStages"]; + _releaseStage = [report valueForKeyPath:@"user.config.releaseStage"]; + _error = [report valueForKeyPath:@"crash.error"]; + _errorType = _error[@"type"]; + _errorClass = BSGParseErrorClass(_error, _errorType); + _errorMessage = BSGParseErrorMessage(report, _error, _errorType); + _binaryImages = report[@"binary_images"]; + _threads = [report valueForKeyPath:@"crash.threads"]; + _breadcrumbs = [report valueForKeyPath:@"user.state.crash.breadcrumbs"]; + _severity = BSGParseSeverity([report valueForKeyPath:@"user.state.crash.severity"]); + _depth = [[report valueForKeyPath:@"user.state.crash.depth"] unsignedIntegerValue]; + _dsymUUID = [report valueForKeyPath:@"system.app_uuid"]; + _deviceAppHash = [report valueForKeyPath:@"system.device_app_hash"]; + _metaData = [report valueForKeyPath:@"user.metaData"] ?: [NSDictionary new]; + _context = BSGParseContext(report, _metaData); + _deviceState = BSGParseDeviceState(report); + _device = BSGParseDevice(report); + _app = BSGParseApp(report, [report valueForKeyPath:@"user.config.appVersion"]); + _appState = BSGParseAppState(report); + } + return self; } -- (NSDictionary *)crash { - return self.ksReport[@"crash"]; +- (instancetype)initWithErrorName:(NSString *)name + errorMessage:(NSString *)message + configuration:(BugsnagConfiguration *)config + metaData:(NSDictionary *)metaData + severity:(BSGSeverity)severity { + if (self = [super init]) { + _errorClass = name; + _errorMessage = message; + _metaData = metaData ?: [NSDictionary new]; + _severity = severity; + _releaseStage = config.releaseStage; + _notifyReleaseStages = config.notifyReleaseStages; + _context = BSGParseContext(nil, metaData); + _breadcrumbs = [config.breadcrumbs arrayValue]; + } + return self; } -- (NSDictionary *)unityExceptionReport { - return self.metaData[@"_bugsnag_unity_exception"]; +- (BOOL)shouldBeSent { + return [self.notifyReleaseStages containsObject:self.releaseStage] + || (self.notifyReleaseStages.count == 0 && [[Bugsnag configuration] shouldSendReports]); } - (NSDictionary *)serializableValueWithTopLevelData: @@ -181,13 +274,11 @@ - (NSDictionary *)serializableValueWithTopLevelData: NSMutableDictionary *event = [NSMutableDictionary dictionary]; NSMutableDictionary *exception = [NSMutableDictionary dictionary]; NSMutableDictionary *metaData = [[self metaData] mutableCopy]; - NSString *severity = - [self severity].length > 0 ? [self severity] : BugsnagSeverityError; // Build Event BSGDictSetSafeObject(event, @[ exception ], @"exceptions"); BSGDictInsertIfNotNil(event, [self dsymUUID], @"dsymUUID"); - BSGDictSetSafeObject(event, severity, @"severity"); + BSGDictSetSafeObject(event, BSGFormatSeverity(self.severity), @"severity"); BSGDictSetSafeObject(event, [self breadcrumbs], @"breadcrumbs"); BSGDictSetSafeObject(event, @"2", @"payloadVersion"); BSGDictSetSafeObject(event, metaData, @"metaData"); @@ -195,16 +286,11 @@ - (NSDictionary *)serializableValueWithTopLevelData: BSGDictSetSafeObject(event, [self device], @"device"); BSGDictSetSafeObject(event, [self appState], @"appState"); BSGDictSetSafeObject(event, [self app], @"app"); + BSGDictSetSafeObject(event, [self context], @"context"); - if ([metaData[@"context"] isKindOfClass:[NSString class]]) { - BSGDictSetSafeObject(event, metaData[@"context"], @"context"); - [metaData removeObjectForKey:@"context"]; - - } else { - BSGDictSetSafeObject(event, [self context], @"context"); - } - - // Build MetaData + // Inserted into `context` property + [metaData removeObjectForKey:@"context"]; + // Build metadata BSGDictSetSafeObject(metaData, [self error], @"error"); // Make user mutable and set the id if the user hasn't already @@ -288,71 +374,4 @@ - (NSArray *)serializeThreadsWithException:(NSMutableDictionary *)exception { return bugsnagThreads; } -// Generates the deviceState section of the payload -- (NSDictionary *)deviceState { - NSMutableDictionary *deviceState = [[self state][@"deviceState"] mutableCopy]; - BSGDictSetSafeObject(deviceState, [self system][@"memory"][@"free"], - @"freeMemory"); - return deviceState; -} - -// Generates the device section of the payload -- (NSDictionary *)device { - NSMutableDictionary *device = [NSMutableDictionary dictionary]; - - BSGDictSetSafeObject(device, @"Apple", @"manufacturer"); - BSGDictSetSafeObject(device, [[NSLocale currentLocale] localeIdentifier], - @"locale"); - BSGDictSetSafeObject(device, [self system][@"device_app_hash"], @"id"); - BSGDictSetSafeObject(device, [self system][@"time_zone"], @"timezone"); - BSGDictSetSafeObject(device, [self system][@"model"], @"modelNumber"); - BSGDictSetSafeObject(device, [self system][@"machine"], @"model"); - BSGDictSetSafeObject(device, [self system][@"system_name"], @"osName"); - BSGDictSetSafeObject(device, [self system][@"system_version"], @"osVersion"); - BSGDictSetSafeObject(device, [self system][@"memory"][@"usable"], - @"totalMemory"); - - return device; -} - -// Generates the appState section of the payload -- (NSDictionary *)appState { - NSMutableDictionary *appState = [NSMutableDictionary dictionary]; - NSInteger activeTimeSinceLaunch = - [[self appStats][@"active_time_since_launch"] doubleValue] * 1000.0; - NSInteger backgroundTimeSinceLaunch = - [[self appStats][@"background_time_since_launch"] doubleValue] * 1000.0; - - BSGDictSetSafeObject(appState, @(activeTimeSinceLaunch), - @"durationInForeground"); - BSGDictSetSafeObject(appState, - @(activeTimeSinceLaunch + backgroundTimeSinceLaunch), - @"duration"); - BSGDictSetSafeObject(appState, [self appStats][@"application_in_foreground"], - @"inForeground"); - BSGDictSetSafeObject(appState, [self appStats], @"stats"); - - return appState; -} - -// Generates the app section of the payload -- (NSDictionary *)app { - NSMutableDictionary *app = [NSMutableDictionary dictionary]; - - BSGDictSetSafeObject(app, [self system][@"CFBundleVersion"], - @"bundleVersion"); - BSGDictSetSafeObject(app, [self system][@"CFBundleIdentifier"], @"id"); - BSGDictSetSafeObject(app, [self system][@"CFBundleExecutable"], @"name"); - BSGDictSetSafeObject(app, [Bugsnag configuration].releaseStage, - @"releaseStage"); - if ([self appVersion]) { - BSGDictSetSafeObject(app, [self appVersion], @"version"); - } else { - BSGDictSetSafeObject(app, [self system][@"CFBundleShortVersionString"], - @"version"); - } - - return app; -} - @end From d2fd09562585ec8a3fd6b3d6c97513abc648cef0 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 29 Jun 2016 13:19:19 +0100 Subject: [PATCH 02/12] Add notify:block: selector for easier report config Deprecate notify:withData: and notify:withData:atSeverity: --- Source/Bugsnag.h | 15 +++- Source/Bugsnag.m | 99 ++++++++++++++++++++-- Source/BugsnagConfiguration.h | 97 ++++++++++++++++++++-- Source/BugsnagConfiguration.m | 12 +++ Source/BugsnagNotifier.h | 35 +++++--- Source/BugsnagNotifier.m | 113 +++++++++----------------- Source/BugsnagSink.m | 28 ++++--- iOS/Bugsnag.xcodeproj/project.pbxproj | 4 +- 8 files changed, 289 insertions(+), 114 deletions(-) diff --git a/Source/Bugsnag.h b/Source/Bugsnag.h index c7c251fc6..6f60d37c3 100644 --- a/Source/Bugsnag.h +++ b/Source/Bugsnag.h @@ -76,6 +76,15 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; */ + (void)notify:(NSException *_Nonnull)exception; +/** + * Send a custom or caught exception to Bugsnag + * + * @param exception The exception + * @param block A block for optionally configuring the error report + */ ++ (void)notify:(NSException *_Nonnull)exception + block:(BugsnagNotifyBlock _Nullable)block; + /** Send a custom or caught exception to Bugsnag. * * The exception will be sent to Bugsnag in the background allowing your @@ -87,7 +96,8 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; * report. */ + (void)notify:(NSException *_Nonnull)exception - withData:(NSDictionary *_Nullable)metaData; + withData:(NSDictionary *_Nullable)metaData + __deprecated_msg("Use notify:block: instead and add the metadata to the report directly."); /** Send a custom or caught exception to Bugsnag. * @@ -103,7 +113,8 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; */ + (void)notify:(NSException *_Nonnull)exception withData:(NSDictionary *_Nullable)metaData - atSeverity:(NSString *_Nullable)severity; + atSeverity:(NSString *_Nullable)severity + __deprecated_msg("Use notify:block: instead and add the metadata and severity to the report directly."); /** Add custom data to send to Bugsnag with every exception. If value is nil, * delete the current value for attributeName diff --git a/Source/Bugsnag.m b/Source/Bugsnag.m index cd6111dab..579e5a7f9 100644 --- a/Source/Bugsnag.m +++ b/Source/Bugsnag.m @@ -27,6 +27,7 @@ #import "Bugsnag.h" #import "BugsnagBreadcrumb.h" #import "BugsnagConfiguration.h" +#import "BugsnagCrashReport.h" #import "BugsnagNotifier.h" #import "BugsnagSink.h" #import @@ -38,12 +39,15 @@ + (BugsnagNotifier*)notifier; + (BOOL) bugsnagStarted; @end +@interface NSDictionary (BSGKSMerge) +- (NSDictionary*)BSG_mergedInto:(NSDictionary *)dest; +@end + @implementation Bugsnag + (void)startBugsnagWithApiKey:(NSString*)apiKey { - BugsnagConfiguration *configuration = [[BugsnagConfiguration alloc] init]; + BugsnagConfiguration *configuration = [BugsnagConfiguration new]; configuration.apiKey = apiKey; - [self startBugsnagWithConfiguration:configuration]; } @@ -68,15 +72,40 @@ + (BugsnagNotifier*)notifier { } + (void) notify:(NSException *)exception { - [self.notifier notify:exception withData:nil atSeverity: BugsnagSeverityWarning atDepth: 1]; + [self.notifier notifyException:exception + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + }]; +} + ++ (void)notify:(NSException *)exception block:(BugsnagNotifyBlock)block { + [[self notifier] notifyException:exception + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + if (block) + block(report); + }]; } -+ (void) notify:(NSException *)exception withData:(NSDictionary*)metaData { - [self.notifier notify:exception withData:metaData atSeverity: BugsnagSeverityWarning atDepth: 1]; ++ (void)notify:(NSException *)exception withData:(NSDictionary*)metaData { + [[self notifier] notifyException:exception + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + report.metaData = [metaData BSG_mergedInto: + [self.notifier.configuration.metaData toDictionary]]; + }]; } -+ (void) notify:(NSException *)exception withData:(NSDictionary*)metaData atSeverity:(NSString*)severity { - [self.notifier notify:exception withData:metaData atSeverity: severity atDepth: 1]; ++ (void)notify:(NSException *)exception + withData:(NSDictionary*)metaData + atSeverity:(NSString*)severity { + [[self notifier] notifyException:exception + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + report.metaData = [metaData BSG_mergedInto: + [self.notifier.configuration.metaData toDictionary]]; + report.severity = BSGParseSeverity(severity); + }]; } + (void) addAttribute:(NSString*)attributeName withValue:(id)value toTabWithName:(NSString*)tabName { @@ -125,3 +154,59 @@ + (NSDateFormatter *)payloadDateFormatter { } @end + +// +// NSDictionary+Merge.m +// +// Created by Karl Stenerud on 2012-10-01. +// +// Copyright (c) 2012 Karl Stenerud. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall remain in place +// in this source code. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +@implementation NSDictionary (BSGKSMerge) + +- (NSDictionary*)BSG_mergedInto:(NSDictionary *)dest +{ + if([dest count] == 0) + { + return self; + } + if([self count] == 0) + { + return dest; + } + + NSMutableDictionary* dict = [dest mutableCopy]; + for(id key in [self allKeys]) + { + id srcEntry = [self objectForKey:key]; + id dstEntry = [dest objectForKey:key]; + if([dstEntry isKindOfClass:[NSDictionary class]] && + [srcEntry isKindOfClass:[NSDictionary class]]) + { + srcEntry = [srcEntry BSG_mergedInto:dstEntry]; + } + [dict setObject:srcEntry forKey:key]; + } + return dict; +} + +@end diff --git a/Source/BugsnagConfiguration.h b/Source/BugsnagConfiguration.h index d249221cf..6c87886fe 100644 --- a/Source/BugsnagConfiguration.h +++ b/Source/BugsnagConfiguration.h @@ -26,9 +26,32 @@ #import "BSGKSCrashReportWriter.h" #import "BugsnagBreadcrumb.h" +#import "BugsnagCrashReport.h" #import "BugsnagMetaData.h" #import +@class BugsnagBreadcrumbs; + +/** + * A configuration block for modifying an error report + * + * @param report The default report + */ +typedef void (^BugsnagNotifyBlock)(BugsnagCrashReport *_Nonnull report); + +/** + * A handler for modifying data before sending it to Bugsnag + * + * @param rawEventData The raw event data written at crash time. This + * includes data added in onCrashHandler. + * @param report The report generated from the rawEventData + * + * @return YES if the report should be sent + */ +typedef bool (^BugsnagBeforeNotifyBlock)( + NSDictionary *_Nonnull rawEventData, + BugsnagCrashReport *_Nonnull reports); + /** * A handler for modifying data before sending it to Bugsnag * @@ -41,30 +64,92 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( NSArray *_Nonnull rawEventReports, NSDictionary *_Nonnull report); -@class BugsnagBreadcrumbs; - @interface BugsnagConfiguration : NSObject - +/** + * The API key of a Bugsnag project + */ @property(nonatomic, readwrite, retain, nullable) NSString *apiKey; +/** + * The URL used to notify Bugsnag + */ @property(nonatomic, readwrite, retain, nullable) NSURL *notifyURL; +/** + * The release stage of the application, such as production, development, beta + * et cetera + */ @property(nonatomic, readwrite, retain, nullable) NSString *releaseStage; +/** + * Release stages which are allowed to notify Bugsnag + */ @property(nonatomic, readwrite, retain, nullable) NSArray *notifyReleaseStages; +/** + * A general summary of what was occuring in the application + */ @property(nonatomic, readwrite, retain, nullable) NSString *context; +/** + * 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 + */ @property(nonatomic, readwrite, retain, nullable) BugsnagMetaData *metaData; +/** + * Meta-information about the state of Bugsnag + */ @property(nonatomic, readwrite, retain, nullable) BugsnagMetaData *config; +/** + * Rolling snapshots of user actions leading up to a crash report + */ @property(nonatomic, readonly, strong, nullable) BugsnagBreadcrumbs *breadcrumbs; -@property(nonatomic, readonly, strong, nullable) NSArray *beforeNotifyHooks; +/** + * Hooks for modifying crash reports before it is sent to Bugsnag + */ +@property(nonatomic, readonly, strong, nullable) + NSArray * beforeNotifyBlocks; +/** + * Optional handler invoked when a crash or fatal signal occurs + */ @property(nonatomic) void (*_Nullable onCrashHandler) (const BSGKSCrashReportWriter *_Nonnull writer); - +/** + * YES if uncaught exceptions should be reported automatically + */ @property(nonatomic) BOOL autoNotify; +/** + * Set user metadata + * + * @param userId ID of the user + * @param name Name of the user + * @param email Email address of the user + */ - (void)setUser:(NSString *_Nullable)userId withName:(NSString *_Nullable)name andEmail:(NSString *_Nullable)email; -- (void)addBeforeNotifyHook:(BugsnagBeforeNotifyHook _Nonnull)hook; +/** + * Add a callback to be invoked before a report is sent to Bugsnag, to + * change the report contents as needed + * + * @param block A block which returns YES if the report should be sent + */ +- (void)addBeforeNotifyBlock:(BugsnagBeforeNotifyBlock _Nonnull)block; +/** + * Whether reports shoould be sent, based on release stage options + * + * @return YES if reports should be sent based on this configuration + */ +- (BOOL)shouldSendReports; + +- (void)addBeforeNotifyHook:(BugsnagBeforeNotifyHook _Nonnull)hook + __deprecated_msg("Use addBeforeNotifyBlock: instead."); +/** + * Hooks for processing raw report data before it is sent to Bugsnag + */ +@property(nonatomic, readonly, strong, nullable) NSArray *beforeNotifyHooks + __deprecated_msg("Use beforeNotify instead."); @end diff --git a/Source/BugsnagConfiguration.m b/Source/BugsnagConfiguration.m index 3026db4a1..b585341d3 100644 --- a/Source/BugsnagConfiguration.m +++ b/Source/BugsnagConfiguration.m @@ -32,6 +32,7 @@ @interface BugsnagConfiguration () @property(nonatomic, readwrite, strong) NSMutableArray *beforeNotifyHooks; +@property(nonatomic, readwrite, strong) NSMutableArray *beforeNotifyBlocks; @end @implementation BugsnagConfiguration @@ -44,6 +45,7 @@ - (id)init { _autoNotify = true; _notifyURL = [NSURL URLWithString:@"https://notify.bugsnag.com/"]; _beforeNotifyHooks = [NSMutableArray new]; + _beforeNotifyBlocks = [NSMutableArray new]; _notifyReleaseStages = nil; _breadcrumbs = [BugsnagBreadcrumbs new]; #if DEBUG @@ -54,6 +56,12 @@ - (id)init { } return self; } + +- (BOOL)shouldSendReports { + return self.notifyReleaseStages.count == 0 + || [self.notifyReleaseStages containsObject:self.releaseStage]; +} + - (void)setUser:(NSString *)userId withName:(NSString *)userName andEmail:(NSString *)userEmail { @@ -64,6 +72,10 @@ - (void)setUser:(NSString *)userId toTabWithName:@"user"]; } +- (void)addBeforeNotifyBlock:(BugsnagBeforeNotifyBlock)block { + [(NSMutableArray *)self.beforeNotifyBlocks addObject:[block copy]]; +} + - (void)addBeforeNotifyHook:(BugsnagBeforeNotifyHook)hook { [(NSMutableArray *)self.beforeNotifyHooks addObject:[hook copy]]; } diff --git a/Source/BugsnagNotifier.h b/Source/BugsnagNotifier.h index 7f7ad7236..476f60206 100644 --- a/Source/BugsnagNotifier.h +++ b/Source/BugsnagNotifier.h @@ -41,24 +41,37 @@ (BugsnagConfiguration *_Nonnull)configuration; - (void)start; -/** Notify bugsnag of an exception. - * - * @param exception The NSException +/** + * Notify Bugsnag of an error * - * @param metaData Any metaData to include in the report + * @param erroName Name or class of the error + * @param message Message of or reason for the error + * @param block Configuration block with information for this report + */ +- (void)notify:(NSString *_Nonnull)errorName + message:(NSString *_Nonnull)message + block:(BugsnagNotifyBlock _Nullable)block; + +/** + * Notify Bugsnag of an exception * - * @param severity The severity (default BugsnagSeverityWarning) + * @param exception the exception + * @param block Configuration block for adding additional report information + */ +- (void)notifyException:(NSException *_Nonnull)exception + block:(BugsnagNotifyBlock _Nullable)block; + +/** + * Notify Bugsnag of an error * - * @param depth How many stack frames to remove from the report. + * @param error the error + * @param block Configuration block for adding additional report information */ -- (void)notify:(NSException *_Nonnull)exception - withData:(NSDictionary *_Nullable)metaData - atSeverity:(NSString *_Nullable)severity - atDepth:(NSUInteger)depth; +- (void)notifyError:(NSError *_Nonnull)error + block:(BugsnagNotifyBlock _Nullable)block; /** * Write breadcrumbs to the cached metadata for error reports */ - (void)serializeBreadcrumbs; - @end diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index 3d21db986..a61676157 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -30,6 +30,7 @@ #import "BugsnagBreadcrumb.h" #import "BugsnagNotifier.h" #import "BugsnagCollections.h" +#import "BugsnagCrashReport.h" #import "BugsnagSink.h" #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE @@ -104,10 +105,6 @@ void BSSerializeJSONDictionary(NSDictionary *dictionary, char **destination) { } } -@interface NSDictionary (BSGKSMerge) -- (NSDictionary*)BSG_mergedInto:(NSDictionary *)dest; -@end - @implementation BugsnagNotifier @synthesize configuration; @@ -166,26 +163,49 @@ - (void) start { #endif } -- (void)notify:(NSException *)exception withData:(NSDictionary *)metaData atSeverity:(NSString *)severity atDepth:(NSUInteger) depth { +- (void)notifyError:(NSError *)error block:(void (^)(BugsnagCrashReport *))block { + [self notify:NSStringFromClass([error class]) + message:error.localizedDescription + block:^(BugsnagCrashReport * _Nonnull report) { + NSMutableDictionary *metadata = [report.metaData mutableCopy]; + metadata[@"NSError"] = @{@"code": @(error.code), + @"domain": error.domain, + @"reason": error.localizedFailureReason?: @"" }; + if (block) + block(report); + }]; +} - if (!metaData) { - metaData = [[NSDictionary alloc] init]; - } - metaData = [metaData BSG_mergedInto: [self.configuration.metaData toDictionary]]; - if (!severity) { - severity = BugsnagSeverityWarning; - } +- (void)notifyException:(NSException *)exception + block:(void (^)(BugsnagCrashReport *))block { + [self notify:exception.name ?: NSStringFromClass([exception class]) + message:exception.reason + block:block]; +} - [self.metaDataLock lock]; - BSSerializeJSONDictionary(metaData, &g_bugsnag_data.metaDataJSON); - [self.state addAttribute:BSAttributeSeverity withValue:severity toTabWithName:BSTabCrash]; - [self.state addAttribute:BSAttributeDepth withValue:@(depth + 3) toTabWithName:BSTabCrash]; - NSString *exceptionName = [exception name] ?: NSStringFromClass([NSException class]); - [[KSCrash sharedInstance] reportUserException:exceptionName reason:[exception reason] language:NULL lineOfCode:@"" stackTrace:@[] terminateProgram:NO]; +- (void)notify:(NSString *)exceptionName + message:(NSString *)message + block:(void (^)(BugsnagCrashReport *))block { + BugsnagCrashReport *report = [[BugsnagCrashReport alloc] initWithErrorName:exceptionName + errorMessage:message + configuration:self.configuration + metaData:[self.configuration.metaData toDictionary] + severity:BSGSeverityWarning]; + if (block) + block(report); + [self.metaDataLock lock]; + BSSerializeJSONDictionary(report.metaData, &g_bugsnag_data.metaDataJSON); + [self.state addAttribute:BSAttributeSeverity withValue:BSGFormatSeverity(report.severity) toTabWithName:BSTabCrash]; + [self.state addAttribute:BSAttributeDepth withValue:@(report.depth + 3) toTabWithName:BSTabCrash]; + [[KSCrash sharedInstance] reportUserException:exceptionName ?: NSStringFromClass([NSException class]) + reason:report.errorMessage ?: @"" + language:NULL lineOfCode:@"" + stackTrace:@[] + terminateProgram:NO]; // Restore metaData to pre-crash state. [self.metaDataLock unlock]; - [self metaDataChanged: self.configuration.metaData]; + [self metaDataChanged:self.configuration.metaData]; [[self state] clearTab:BSTabCrash]; [self performSelectorInBackground:@selector(sendPendingReports) withObject:nil]; @@ -275,58 +295,3 @@ - (void)dealloc { @end -// -// NSDictionary+Merge.m -// -// Created by Karl Stenerud on 2012-10-01. -// -// Copyright (c) 2012 Karl Stenerud. All rights reserved. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall remain in place -// in this source code. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -@implementation NSDictionary (BSGKSMerge) - -- (NSDictionary*)BSG_mergedInto:(NSDictionary *)dest -{ - if([dest count] == 0) - { - return self; - } - if([self count] == 0) - { - return dest; - } - - NSMutableDictionary* dict = [dest mutableCopy]; - for(id key in [self allKeys]) - { - id srcEntry = [self objectForKey:key]; - id dstEntry = [dest objectForKey:key]; - if([dstEntry isKindOfClass:[NSDictionary class]] && - [srcEntry isKindOfClass:[NSDictionary class]]) - { - srcEntry = [srcEntry BSG_mergedInto:dstEntry]; - } - [dict setObject:srcEntry forKey:key]; - } - return dict; -} - -@end diff --git a/Source/BugsnagSink.m b/Source/BugsnagSink.m index 54dfeaca5..51750ae7a 100644 --- a/Source/BugsnagSink.m +++ b/Source/BugsnagSink.m @@ -46,21 +46,21 @@ @implementation BugsnagSink // - the report-specific and global `notifyReleaseStages` properties are unset // - the report-specific `notifyReleaseStages` property is unset and the global `notifyReleaseStages` property // and it contains the current stage -- (void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompletion) onCompletion -{ - NSMutableArray *bugsnagReports = [NSMutableArray arrayWithCapacity:[reports count]]; +- (void)filterReports:(NSArray*) reports + onCompletion:(KSCrashReportFilterCompletion) onCompletion { + NSMutableArray *bugsnagReports = [NSMutableArray new]; BugsnagConfiguration *configuration = [Bugsnag configuration]; - BOOL configuredShouldNotify = configuration.notifyReleaseStages.count == 0 - || [configuration.notifyReleaseStages containsObject:configuration.releaseStage]; for (NSDictionary* report in reports) { BugsnagCrashReport *bugsnagReport = [[BugsnagCrashReport alloc] initWithKSReport:report]; - - // Filter the reports here, we have to do it now as we dont want to hack KSCrash to do it at crash time. - // We also in the docs imply that the filtering happens when the crash happens - so we use the values - // saved in the report. - BOOL shouldNotify = [bugsnagReport.notifyReleaseStages containsObject:bugsnagReport.releaseStage] - || (bugsnagReport.notifyReleaseStages.count == 0 && configuredShouldNotify); - if(shouldNotify) { + if (![bugsnagReport shouldBeSent]) + continue; + BOOL shouldSend = YES; + for (BugsnagBeforeNotifyBlock block in configuration.beforeNotifyBlocks) { + shouldSend = block(report, bugsnagReport); + if (!shouldSend) + break; + } + if(shouldSend) { [bugsnagReports addObject:bugsnagReport]; } } @@ -72,6 +72,8 @@ - (void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompl return; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" NSDictionary *reportData = [self getBodyFromReports:bugsnagReports]; for (BugsnagBeforeNotifyHook hook in configuration.beforeNotifyHooks) { if (reportData) { @@ -80,6 +82,8 @@ - (void) filterReports:(NSArray*) reports onCompletion:(KSCrashReportFilterCompl break; } } +#pragma clang diagnostic pop + if (reportData == nil) { if (onCompletion) { onCompletion(@[], YES, nil); diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index 13b4691ad..a59625619 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ 8A2C8F541C6BBE3C00846019 /* BugsnagCollections.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8F421C6BBE3C00846019 /* BugsnagCollections.m */; }; 8A2C8F551C6BBE3C00846019 /* BugsnagConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8F431C6BBE3C00846019 /* BugsnagConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8F561C6BBE3C00846019 /* BugsnagConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8F441C6BBE3C00846019 /* BugsnagConfiguration.m */; }; - 8A2C8F571C6BBE3C00846019 /* BugsnagCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8F451C6BBE3C00846019 /* BugsnagCrashReport.h */; }; + 8A2C8F571C6BBE3C00846019 /* BugsnagCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8F451C6BBE3C00846019 /* BugsnagCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8F581C6BBE3C00846019 /* BugsnagCrashReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8F461C6BBE3C00846019 /* BugsnagCrashReport.m */; }; 8A2C8F5B1C6BBE3C00846019 /* BugsnagMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8F491C6BBE3C00846019 /* BugsnagMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8F5C1C6BBE3C00846019 /* BugsnagMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8F4A1C6BBE3C00846019 /* BugsnagMetaData.m */; }; @@ -185,11 +185,11 @@ 8A2C8F5B1C6BBE3C00846019 /* BugsnagMetaData.h in Headers */, 8A2C8F551C6BBE3C00846019 /* BugsnagConfiguration.h in Headers */, 8A2C8F511C6BBE3C00846019 /* BugsnagBreadcrumb.h in Headers */, + 8A2C8F571C6BBE3C00846019 /* BugsnagCrashReport.h in Headers */, 8A2C8F5D1C6BBE3C00846019 /* BugsnagNotifier.h in Headers */, 8A2C8F531C6BBE3C00846019 /* BugsnagCollections.h in Headers */, 8A87351E1C6D2ADE00EDBD5B /* BSGKSCrashReportWriter.h in Headers */, 8A2C8F5F1C6BBE3C00846019 /* BugsnagSink.h in Headers */, - 8A2C8F571C6BBE3C00846019 /* BugsnagCrashReport.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; From 1241f44d04bd0391fc05376f56ece55260e650ec Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 29 Jun 2016 13:21:22 +0100 Subject: [PATCH 03/12] Add helper selector for notifying about an error Errors have a default severity of warning and automatically attach the error description, failure reason (if any), domain, and code as metadata --- Source/Bugsnag.h | 16 ++++++++++++++++ Source/Bugsnag.m | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/Source/Bugsnag.h b/Source/Bugsnag.h index 6f60d37c3..dc8d96883 100644 --- a/Source/Bugsnag.h +++ b/Source/Bugsnag.h @@ -85,6 +85,22 @@ static NSString *_Nonnull const BugsnagSeverityInfo = @"info"; + (void)notify:(NSException *_Nonnull)exception block:(BugsnagNotifyBlock _Nullable)block; +/** + * Send an error to Bugsnag + * + * @param error The error + */ ++ (void)notifyError:(NSError *_Nonnull)error; + +/** + * Send an error to Bugsnag + * + * @param error The error + * @param block A block for optionally configuring the error report + */ ++ (void)notifyError:(NSError *_Nonnull)error + block:(BugsnagNotifyBlock _Nullable)block; + /** Send a custom or caught exception to Bugsnag. * * The exception will be sent to Bugsnag in the background allowing your diff --git a/Source/Bugsnag.m b/Source/Bugsnag.m index 579e5a7f9..68687942d 100644 --- a/Source/Bugsnag.m +++ b/Source/Bugsnag.m @@ -87,6 +87,23 @@ + (void)notify:(NSException *)exception block:(BugsnagNotifyBlock)block { }]; } ++ (void) notifyError:(NSError *)error { + [self.notifier notifyError:error + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + }]; +} + + ++ (void)notifyError:(NSError *)error block:(BugsnagNotifyBlock)block { + [[self notifier] notifyError:error + block:^(BugsnagCrashReport * _Nonnull report) { + report.depth = 1; + if (block) + block(report); + }]; +} + + (void)notify:(NSException *)exception withData:(NSDictionary*)metaData { [[self notifier] notifyException:exception block:^(BugsnagCrashReport * _Nonnull report) { From 90e9c7ca01136eb4883d9ea57dad49a36e03eb7c Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 29 Jun 2016 13:30:40 +0100 Subject: [PATCH 04/12] Send error reports asynchronously --- Source/BugsnagSink.m | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Source/BugsnagSink.m b/Source/BugsnagSink.m index 51750ae7a..4769e821a 100644 --- a/Source/BugsnagSink.m +++ b/Source/BugsnagSink.m @@ -109,16 +109,12 @@ - (void)filterReports:(NSArray*) reports request.HTTPMethod = @"POST"; request.HTTPBody = jsonData; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [NSURLConnection sendSynchronousRequest:request - returningResponse:NULL - error:&error]; -#pragma clang diagnostic pop - - if (onCompletion) { - onCompletion(reports, error == nil, error); - } + [NSURLConnection sendAsynchronousRequest:request + queue:[NSOperationQueue currentQueue] + completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { + if (onCompletion) + onCompletion(reports, connectionError == nil, connectionError); + }]; } @catch (NSException *exception) { if (onCompletion) { onCompletion(reports, NO, [NSError errorWithDomain:exception.reason From 2eea8337cff91d948b172b2078523fe449fdc633 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 29 Jun 2016 17:46:39 +0100 Subject: [PATCH 05/12] Add tests --- .gitmodules | 3 + OSX.xcworkspace/contents.xcworkspacedata | 3 + .../xcshareddata/OSX.xcscmblueprint | 7 + OSX/Bugsnag.xcodeproj/project.pbxproj | 12 +- Tests/BugsnagSpec.m | 122 ++++++++++++++++++ development/Kiwi | 1 + iOS.xcworkspace/contents.xcworkspacedata | 3 + .../xcshareddata/iOS.xcscmblueprint | 7 + iOS/Bugsnag.xcodeproj/project.pbxproj | 10 ++ 9 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 Tests/BugsnagSpec.m create mode 160000 development/Kiwi diff --git a/.gitmodules b/.gitmodules index 381866ca7..a550fe56f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "Carthage/Checkouts/KSCrash"] path = Carthage/Checkouts/KSCrash url = https://github.com/kstenerud/KSCrash +[submodule "development/Kiwi"] + path = development/Kiwi + url = https://github.com/kiwi-bdd/Kiwi diff --git a/OSX.xcworkspace/contents.xcworkspacedata b/OSX.xcworkspace/contents.xcworkspacedata index c1211a272..6dd258502 100644 --- a/OSX.xcworkspace/contents.xcworkspacedata +++ b/OSX.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/OSX.xcworkspace/xcshareddata/OSX.xcscmblueprint b/OSX.xcworkspace/xcshareddata/OSX.xcscmblueprint index 726cc911f..02e84b142 100644 --- a/OSX.xcworkspace/xcshareddata/OSX.xcscmblueprint +++ b/OSX.xcworkspace/xcshareddata/OSX.xcscmblueprint @@ -4,11 +4,13 @@ }, "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { + "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" : 0, "D2885296440547CB23891301E79B480A22EAC5F6" : 0, "C4519EFDD2C3B96E9BD67A5CED575450FFB87C90" : 0 }, "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "64F71D72-7937-47C7-9FA1-099F1EFA7F49", "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { + "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" : "bugsnag-cocoa\/development\/Kiwi\/", "D2885296440547CB23891301E79B480A22EAC5F6" : "bugsnag-cocoa-r\/Carthage\/Checkouts\/KSCrash\/", "C4519EFDD2C3B96E9BD67A5CED575450FFB87C90" : "bugsnag-cocoa-r\/" }, @@ -16,6 +18,11 @@ "DVTSourceControlWorkspaceBlueprintVersion" : 204, "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "OSX.xcworkspace", "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/kiwi-bdd\/Kiwi", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" + }, { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:bugsnag\/bugsnag-cocoa", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", diff --git a/OSX/Bugsnag.xcodeproj/project.pbxproj b/OSX/Bugsnag.xcodeproj/project.pbxproj index 22556bdb8..a6e0ce6af 100644 --- a/OSX/Bugsnag.xcodeproj/project.pbxproj +++ b/OSX/Bugsnag.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ 8A2C8FD21C6BC2C800846019 /* BugsnagCollections.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8FC01C6BC2C800846019 /* BugsnagCollections.m */; }; 8A2C8FD31C6BC2C800846019 /* BugsnagConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8FC11C6BC2C800846019 /* BugsnagConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8FD41C6BC2C800846019 /* BugsnagConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8FC21C6BC2C800846019 /* BugsnagConfiguration.m */; }; - 8A2C8FD51C6BC2C800846019 /* BugsnagCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8FC31C6BC2C800846019 /* BugsnagCrashReport.h */; }; + 8A2C8FD51C6BC2C800846019 /* BugsnagCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8FC31C6BC2C800846019 /* BugsnagCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8FD61C6BC2C800846019 /* BugsnagCrashReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8FC41C6BC2C800846019 /* BugsnagCrashReport.m */; }; 8A2C8FD71C6BC2C800846019 /* BugsnagMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A2C8FC51C6BC2C800846019 /* BugsnagMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A2C8FD81C6BC2C800846019 /* BugsnagMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A2C8FC61C6BC2C800846019 /* BugsnagMetaData.m */; }; @@ -31,6 +31,8 @@ 8A2C8FF01C6BC3A200846019 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C8FEF1C6BC3A200846019 /* SystemConfiguration.framework */; }; 8A2C90031C6BC3FA00846019 /* libKSCrashLib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C90021C6BC3FA00846019 /* libKSCrashLib.a */; }; 8A2C90441C6C040700846019 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C90401C6C03F000846019 /* Cocoa.framework */; }; + 8A7630E21D2523AE000D6737 /* Kiwi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A7630E11D2523AE000D6737 /* Kiwi.framework */; }; + 8A7630E51D2524DB000D6737 /* BugsnagSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 8A7630DF1D252309000D6737 /* BugsnagSpec.m */; }; 8A87352C1C6D3B1600EDBD5B /* BSGKSCrashReportWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A87352B1C6D3B1600EDBD5B /* BSGKSCrashReportWriter.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ @@ -76,6 +78,8 @@ 8A2C902F1C6BF3AC00846019 /* module.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = SOURCE_ROOT; }; 8A2C90401C6C03F000846019 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 8A2C90421C6C03FF00846019 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 8A7630DF1D252309000D6737 /* BugsnagSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagSpec.m; path = ../Tests/BugsnagSpec.m; sourceTree = SOURCE_ROOT; }; + 8A7630E11D2523AE000D6737 /* Kiwi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kiwi.framework; path = "../../../../../Library/Developer/Xcode/DerivedData/OSX-fywomumlxcstijawyihsvjfwghcm/Build/Products/Debug/Kiwi.framework"; sourceTree = ""; }; 8A87352B1C6D3B1600EDBD5B /* BSGKSCrashReportWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSGKSCrashReportWriter.h; path = ../Source/BSGKSCrashReportWriter.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -94,6 +98,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8A7630E21D2523AE000D6737 /* Kiwi.framework in Frameworks */, 8A2C8FAC1C6BC1F700846019 /* Bugsnag.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -151,6 +156,7 @@ children = ( 8A2C8FE01C6BC38200846019 /* BugsnagBreadcrumbsTest.m */, 8A2C8FE11C6BC38200846019 /* BugsnagCrashReportTests.m */, + 8A7630DF1D252309000D6737 /* BugsnagSpec.m */, 8A2C8FE21C6BC38200846019 /* BugsnagSinkTests.m */, 8A2C8FE41C6BC38200846019 /* report.json */, 8A2C8FB21C6BC1F700846019 /* TestsInfo.plist */, @@ -162,6 +168,7 @@ 8A2C8FF51C6BC3C200846019 /* Frameworks */ = { isa = PBXGroup; children = ( + 8A7630E11D2523AE000D6737 /* Kiwi.framework */, 8A2C90421C6C03FF00846019 /* Foundation.framework */, 8A2C90401C6C03F000846019 /* Cocoa.framework */, 8A2C90021C6BC3FA00846019 /* libKSCrashLib.a */, @@ -183,10 +190,10 @@ 8A2C8FD71C6BC2C800846019 /* BugsnagMetaData.h in Headers */, 8A2C8FCD1C6BC2C800846019 /* Bugsnag.h in Headers */, 8A2C8FCF1C6BC2C800846019 /* BugsnagBreadcrumb.h in Headers */, + 8A2C8FD51C6BC2C800846019 /* BugsnagCrashReport.h in Headers */, 8A2C8FD91C6BC2C800846019 /* BugsnagNotifier.h in Headers */, 8A2C8FDD1C6BC2C800846019 /* BugsnagSink.h in Headers */, 8A87352C1C6D3B1600EDBD5B /* BSGKSCrashReportWriter.h in Headers */, - 8A2C8FD51C6BC2C800846019 /* BugsnagCrashReport.h in Headers */, 8A2C8FD31C6BC2C800846019 /* BugsnagConfiguration.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -305,6 +312,7 @@ files = ( 8A2C8FEA1C6BC38900846019 /* BugsnagBreadcrumbsTest.m in Sources */, 8A2C8FEC1C6BC38900846019 /* BugsnagSinkTests.m in Sources */, + 8A7630E51D2524DB000D6737 /* BugsnagSpec.m in Sources */, 8A2C8FEB1C6BC38900846019 /* BugsnagCrashReportTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/BugsnagSpec.m b/Tests/BugsnagSpec.m new file mode 100644 index 000000000..e683a880a --- /dev/null +++ b/Tests/BugsnagSpec.m @@ -0,0 +1,122 @@ +// +// BugsnagSpec.m +// Bugsnag +// +// Created by Delisa Mason on 6/29/16. +// Copyright 2016 Bugsnag. All rights reserved. +// + +#import +#import "Bugsnag.h" + +#define shouldSoon shouldEventuallyBeforeTimingOutAfter(0.1) + +SPEC_BEGIN(BugsnagSpec) + +beforeAll(^{ + [Bugsnag startBugsnagWithApiKey:@"123"]; +}); + +describe(@"Bugsnag", ^{ + + __block NSURLRequest *request; + + id(^requestEventKeyPath)(NSString *) = ^id(NSString *keyPath) { + NSDictionary *body = [NSJSONSerialization JSONObjectWithData:[request HTTPBody] options:0 error:nil]; + NSDictionary *event = [body valueForKeyPath:@"events.@firstObject"]; + return [event valueForKeyPath:keyPath]; + }; + + id(^requestExceptionValue)(NSString *) = ^id(NSString *keyPath) { + NSDictionary *exception = requestEventKeyPath(@"exceptions.@firstObject"); + return [exception valueForKeyPath:keyPath]; + }; + + beforeEach(^{ + [NSURLConnection stub:@selector(sendAsynchronousRequest:queue:completionHandler:) withBlock:^id(NSArray *params) { + request = [params firstObject]; + void (^block)(NSURLResponse *, NSData *, NSError *) = [params lastObject]; + block(nil, nil, nil); + return nil; + }]; + }); + + afterEach(^{ + request = nil; + }); + + describe(@"notify:", ^{ + + beforeEach(^{ + NSException *exception = [NSException exceptionWithName:@"failure to launch" + reason:@"no pilot" userInfo:nil]; + [Bugsnag notify:exception]; + }); + + it(@"sends to the default endpoint", ^{ + [[expectFutureValue([[request URL] absoluteString]) shouldSoon] equal:@"https://notify.bugsnag.com/"]; + }); + + it(@"sends via POST method", ^{ + [[expectFutureValue([request HTTPMethod]) shouldSoon] equal:@"POST"]; + }); + + it(@"sends the exception name", ^{ + [[expectFutureValue(requestExceptionValue(@"errorClass")) shouldSoon] equal:@"failure to launch"]; + }); + + it(@"sends the exception reason", ^{ + [[expectFutureValue(requestExceptionValue(@"message")) shouldSoon] equal:@"no pilot"]; + }); + }); + + describe(@"notify:block:", ^{ + + __block NSException *exception; + NSArray *breadcrumbs = @[@[[[Bugsnag payloadDateFormatter] stringFromDate:[NSDate date]], @"[kitchen] ate beans"]]; + + beforeEach(^{ + exception = [NSException exceptionWithName:@"failure to launch" + reason:@"no fuel" userInfo:nil]; + [Bugsnag notify:exception block:^(BugsnagCrashReport * _Nonnull report) { + report.context = @"walking to the falafel shop"; + report.groupingHash = @"HUNGRY"; + report.severity = BSGSeverityInfo; + report.errorClass = @"High Class"; + report.errorMessage = @"I forgot to pick up groceries"; + report.metaData = @{ @"labels": @{ @"enabled": @"false" }}; + report.breadcrumbs = breadcrumbs; + }]; + }); + + it(@"sends the context", ^{ + [[expectFutureValue(requestEventKeyPath(@"context")) shouldSoon] equal:@"walking to the falafel shop"]; + }); + + it(@"sends the grouping hash", ^{ + [[expectFutureValue(requestEventKeyPath(@"groupingHash")) shouldSoon] equal:@"HUNGRY"]; + }); + + it(@"sends the severity", ^{ + [[expectFutureValue(requestEventKeyPath(@"severity")) shouldSoon] equal:@"info"]; + }); + + it(@"sends the error class", ^{ + [[expectFutureValue(requestExceptionValue(@"errorClass")) shouldSoon] equal:@"High Class"]; + }); + + it(@"sends the error message", ^{ + [[expectFutureValue(requestExceptionValue(@"message")) shouldSoon] equal:@"I forgot to pick up groceries"]; + }); + + it(@"sends the metadata", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.labels")) shouldSoon] equal: @{ @"enabled": @"false" }]; + }); + + it(@"sends the breadcrumbs", ^{ + [[expectFutureValue(requestEventKeyPath(@"breadcrumbs")) shouldSoon] equal:breadcrumbs]; + }); + }); +}); + +SPEC_END diff --git a/development/Kiwi b/development/Kiwi new file mode 160000 index 000000000..84de39288 --- /dev/null +++ b/development/Kiwi @@ -0,0 +1 @@ +Subproject commit 84de392880b694aa8354864cbfbcb05af80a99f3 diff --git a/iOS.xcworkspace/contents.xcworkspacedata b/iOS.xcworkspace/contents.xcworkspacedata index 3be4e9d72..3cca605fa 100644 --- a/iOS.xcworkspace/contents.xcworkspacedata +++ b/iOS.xcworkspace/contents.xcworkspacedata @@ -7,4 +7,7 @@ + + diff --git a/iOS.xcworkspace/xcshareddata/iOS.xcscmblueprint b/iOS.xcworkspace/xcshareddata/iOS.xcscmblueprint index 09dfbb7ed..d624715ed 100644 --- a/iOS.xcworkspace/xcshareddata/iOS.xcscmblueprint +++ b/iOS.xcworkspace/xcshareddata/iOS.xcscmblueprint @@ -4,11 +4,13 @@ }, "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { + "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" : 0, "D2885296440547CB23891301E79B480A22EAC5F6" : 0, "C4519EFDD2C3B96E9BD67A5CED575450FFB87C90" : 0 }, "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "103C8171-87E2-4033-A0B3-0E14C4C49F02", "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { + "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" : "bugsnag-cocoa\/development\/Kiwi\/", "D2885296440547CB23891301E79B480A22EAC5F6" : "bugsnag-cocoa-r\/Carthage\/Checkouts\/KSCrash\/", "C4519EFDD2C3B96E9BD67A5CED575450FFB87C90" : "bugsnag-cocoa-r\/" }, @@ -16,6 +18,11 @@ "DVTSourceControlWorkspaceBlueprintVersion" : 204, "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "iOS.xcworkspace", "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/kiwi-bdd\/Kiwi", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "4FD7C20106F5BD91089CFF7BC197AC2E448D173B" + }, { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:bugsnag\/bugsnag-cocoa", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", diff --git a/iOS/Bugsnag.xcodeproj/project.pbxproj b/iOS/Bugsnag.xcodeproj/project.pbxproj index a59625619..d2af8bd2d 100644 --- a/iOS/Bugsnag.xcodeproj/project.pbxproj +++ b/iOS/Bugsnag.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 8A2C8F941C6BBFE700846019 /* libKSCrashLib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8A2C8F931C6BBFE700846019 /* libKSCrashLib.a */; }; 8A2C8F961C6BC08600846019 /* report.json in Resources */ = {isa = PBXBuildFile; fileRef = 8A2C8F951C6BC08600846019 /* report.json */; }; 8A87351E1C6D2ADE00EDBD5B /* BSGKSCrashReportWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A87351D1C6D2ADE00EDBD5B /* BSGKSCrashReportWriter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8AAA64011D2407CE00A9A123 /* Kiwi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AAA64001D2407CE00A9A123 /* Kiwi.framework */; }; + 8AAA64081D24088600A9A123 /* BugsnagSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA64071D24088600A9A123 /* BugsnagSpec.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,6 +80,9 @@ 8A2C8F931C6BBFE700846019 /* libKSCrashLib.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libKSCrashLib.a; path = "../Carthage/Checkouts/KSCrash/iOS/build/Debug-iphoneos/libKSCrashLib.a"; sourceTree = ""; }; 8A2C8F951C6BC08600846019 /* report.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = report.json; path = ../Tests/report.json; sourceTree = SOURCE_ROOT; }; 8A87351D1C6D2ADE00EDBD5B /* BSGKSCrashReportWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BSGKSCrashReportWriter.h; path = ../Source/BSGKSCrashReportWriter.h; sourceTree = SOURCE_ROOT; }; + 8AAA63FE1D2404BB00A9A123 /* Nocilla.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nocilla.framework; path = "../development/Nocilla/build/Debug-iphoneos/Nocilla.framework"; sourceTree = ""; }; + 8AAA64001D2407CE00A9A123 /* Kiwi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Kiwi.framework; path = "../development/Kiwi/build/Debug-iphoneos/Kiwi.framework"; sourceTree = ""; }; + 8AAA64071D24088600A9A123 /* BugsnagSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BugsnagSpec.m; path = ../Tests/BugsnagSpec.m; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,6 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8AAA64011D2407CE00A9A123 /* Kiwi.framework in Frameworks */, 8A2C8F231C6BBD2300846019 /* Bugsnag.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -153,6 +159,7 @@ children = ( 8A2C8F8B1C6BBFDD00846019 /* BugsnagBreadcrumbsTest.m */, 8A2C8F8C1C6BBFDD00846019 /* BugsnagCrashReportTests.m */, + 8AAA64071D24088600A9A123 /* BugsnagSpec.m */, 8A2C8F8D1C6BBFDD00846019 /* BugsnagSinkTests.m */, 8A2C8F951C6BC08600846019 /* report.json */, 8A2C8F291C6BBD2300846019 /* TestsInfo.plist */, @@ -164,6 +171,8 @@ 8A2C8F751C6BBEBB00846019 /* Frameworks */ = { isa = PBXGroup; children = ( + 8AAA64001D2407CE00A9A123 /* Kiwi.framework */, + 8AAA63FE1D2404BB00A9A123 /* Nocilla.framework */, 8A2C8F931C6BBFE700846019 /* libKSCrashLib.a */, 8A2C8F731C6BBEAD00846019 /* UIKit.framework */, 8A2C8F711C6BBEA400846019 /* MessageUI.framework */, @@ -307,6 +316,7 @@ files = ( 8A2C8F8F1C6BBFDD00846019 /* BugsnagBreadcrumbsTest.m in Sources */, 8A2C8F911C6BBFDD00846019 /* BugsnagSinkTests.m in Sources */, + 8AAA64081D24088600A9A123 /* BugsnagSpec.m in Sources */, 8A2C8F901C6BBFDD00846019 /* BugsnagCrashReportTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 19daff0df8561b90dd9efe7aea75fc1c5785aa18 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 30 Jun 2016 11:32:58 +0100 Subject: [PATCH 06/12] =?UTF-8?q?Add=20=E2=80=98overrides=E2=80=99=20prope?= =?UTF-8?q?rty=20for=20caching=20properties=20changed=20within=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reduces reuse of the metaData dictionary, which may be overridden itself. Makes tests pass --- Source/BugsnagCrashReport.h | 8 +++++ Source/BugsnagCrashReport.m | 63 +++++++++++++++++++++++++++++++++++-- Source/BugsnagNotifier.m | 8 ++++- Source/BugsnagSink.m | 21 ++++++++++--- 4 files changed, 91 insertions(+), 9 deletions(-) diff --git a/Source/BugsnagCrashReport.h b/Source/BugsnagCrashReport.h index 1e049b02f..f02e3e608 100644 --- a/Source/BugsnagCrashReport.h +++ b/Source/BugsnagCrashReport.h @@ -105,6 +105,10 @@ NSString *_Nonnull BSGFormatSeverity(BSGSeverity severity); * The message of or reason for the error generating the report */ @property (nonatomic, readwrite, copy, nullable) NSString *errorMessage; +/** + * Customized hash for grouping this report with other errors + */ +@property (nonatomic, readwrite, copy, nullable) NSString *groupingHash; /** * Breadcrumbs from user events leading up to the error */ @@ -114,6 +118,10 @@ NSString *_Nonnull BSGFormatSeverity(BSGSeverity severity); * generates a section on bugsnag, displaying key/value pairs */ @property (nonatomic, readwrite, copy, nonnull) NSDictionary *metaData; +/** + * Property overrides + */ +@property (nonatomic, readonly, copy, nonnull) NSDictionary *overrides; @property (nonatomic, readwrite) NSUInteger depth; @end diff --git a/Source/BugsnagCrashReport.m b/Source/BugsnagCrashReport.m index 044445762..170c542cb 100644 --- a/Source/BugsnagCrashReport.m +++ b/Source/BugsnagCrashReport.m @@ -147,7 +147,10 @@ } NSString *BSGParseContext(NSDictionary *report, NSDictionary *metaData) { - id context = metaData[@"context"]; + id context = [report valueForKeyPath:@"user.overrides.context"]; + if ([context isKindOfClass:[NSString class]]) + return context; + context = metaData[@"context"]; if ([context isKindOfClass:[NSString class]]) return context; context = [report valueForKeyPath:@"user.config.context"]; @@ -156,6 +159,26 @@ return nil; } +NSString *BSGParseGroupingHash(NSDictionary *report, NSDictionary *metaData) { + id groupingHash = [report valueForKeyPath:@"user.overrides.groupingHash"]; + if (groupingHash) + return groupingHash; + groupingHash = metaData[@"groupingHash"]; + if ([groupingHash isKindOfClass:[NSString class]]) + return groupingHash; + return nil; +} + +NSArray *BSGParseBreadcrumbs(NSDictionary *report) { + return [report valueForKeyPath:@"user.overrides.breadcrumbs"] ?: + [report valueForKeyPath:@"user.state.crash.breadcrumbs"]; +} + +NSString *BSGParseReleaseStage(NSDictionary *report) { + return [report valueForKeyPath:@"user.overrides.releaseStage"] ?: + [report valueForKeyPath:@"user.config.releaseStage"]; +} + BSGSeverity BSGParseSeverity(NSString *severity) { if ([severity isEqualToString:@"info"]) return BSGSeverityInfo; @@ -224,14 +247,14 @@ @implementation BugsnagCrashReport - (instancetype)initWithKSReport:(NSDictionary *)report { if (self = [super init]) { _notifyReleaseStages = [report valueForKeyPath:@"user.config.notifyReleaseStages"]; - _releaseStage = [report valueForKeyPath:@"user.config.releaseStage"]; + _releaseStage = BSGParseReleaseStage(report); _error = [report valueForKeyPath:@"crash.error"]; _errorType = _error[@"type"]; _errorClass = BSGParseErrorClass(_error, _errorType); _errorMessage = BSGParseErrorMessage(report, _error, _errorType); _binaryImages = report[@"binary_images"]; _threads = [report valueForKeyPath:@"crash.threads"]; - _breadcrumbs = [report valueForKeyPath:@"user.state.crash.breadcrumbs"]; + _breadcrumbs = BSGParseBreadcrumbs(report); _severity = BSGParseSeverity([report valueForKeyPath:@"user.state.crash.severity"]); _depth = [[report valueForKeyPath:@"user.state.crash.depth"] unsignedIntegerValue]; _dsymUUID = [report valueForKeyPath:@"system.app_uuid"]; @@ -242,6 +265,8 @@ - (instancetype)initWithKSReport:(NSDictionary *)report { _device = BSGParseDevice(report); _app = BSGParseApp(report, [report valueForKeyPath:@"user.config.appVersion"]); _appState = BSGParseAppState(report); + _groupingHash = BSGParseGroupingHash(report, _metaData); + _overrides = [report valueForKeyPath:@"user.overrides"]; } return self; } @@ -260,6 +285,7 @@ - (instancetype)initWithErrorName:(NSString *)name _notifyReleaseStages = config.notifyReleaseStages; _context = BSGParseContext(nil, metaData); _breadcrumbs = [config.breadcrumbs arrayValue]; + _overrides = [NSDictionary new]; } return self; } @@ -269,6 +295,36 @@ - (BOOL)shouldBeSent { || (self.notifyReleaseStages.count == 0 && [[Bugsnag configuration] shouldSendReports]); } +- (void)setContext:(NSString *)context { + [self setOverrideProperty:@"context" value:context]; + _context = context; +} + +- (void)setGroupingHash:(NSString *)groupingHash { + [self setOverrideProperty:@"groupingHash" value:groupingHash]; + _groupingHash = groupingHash; +} + +- (void)setBreadcrumbs:(NSArray *)breadcrumbs { + [self setOverrideProperty:@"breadcrumbs" value:breadcrumbs]; + _breadcrumbs = breadcrumbs; +} + +- (void)setReleaseStage:(NSString *)releaseStage { + [self setOverrideProperty:@"releaseStage" value:releaseStage]; + _releaseStage = releaseStage; +} + +- (void)setOverrideProperty:(NSString *)key value:(id)value { + NSMutableDictionary *metadata = [self.overrides mutableCopy]; + if (value) { + metadata[key] = value; + } else { + [metadata removeObjectForKey:key]; + } + _overrides = metadata; +} + - (NSDictionary *)serializableValueWithTopLevelData: (NSMutableDictionary *)data { NSMutableDictionary *event = [NSMutableDictionary dictionary]; @@ -287,6 +343,7 @@ - (NSDictionary *)serializableValueWithTopLevelData: BSGDictSetSafeObject(event, [self appState], @"appState"); BSGDictSetSafeObject(event, [self app], @"app"); BSGDictSetSafeObject(event, [self context], @"context"); + BSGDictInsertIfNotNil(event, self.groupingHash, @"groupingHash"); // Inserted into `context` property [metaData removeObjectForKey:@"context"]; diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index a61676157..b6e27b37b 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -53,6 +53,8 @@ char *configJSON; // Contains notifier state, under "deviceState" and crash-specific information under "crash". char *stateJSON; + // Contains properties in the Bugsnag payload overridden by the user before it was sent + char *userOverridesJSON; // User onCrash handler void (*onCrash)(const KSCrashReportWriter* writer); }; @@ -75,6 +77,9 @@ void BSSerializeDataCrashHandler(const KSCrashReportWriter *writer) { if (g_bugsnag_data.stateJSON) { writer->addJSONElement(writer, "state", g_bugsnag_data.stateJSON); } + if (g_bugsnag_data.userOverridesJSON) { + writer->addJSONElement(writer, "overrides", g_bugsnag_data.userOverridesJSON); + } if (g_bugsnag_data.onCrash) { g_bugsnag_data.onCrash(writer); } @@ -196,9 +201,10 @@ - (void)notify:(NSString *)exceptionName [self.metaDataLock lock]; BSSerializeJSONDictionary(report.metaData, &g_bugsnag_data.metaDataJSON); + BSSerializeJSONDictionary(report.overrides, &g_bugsnag_data.userOverridesJSON); [self.state addAttribute:BSAttributeSeverity withValue:BSGFormatSeverity(report.severity) toTabWithName:BSTabCrash]; [self.state addAttribute:BSAttributeDepth withValue:@(report.depth + 3) toTabWithName:BSTabCrash]; - [[KSCrash sharedInstance] reportUserException:exceptionName ?: NSStringFromClass([NSException class]) + [[KSCrash sharedInstance] reportUserException:report.errorClass ?: NSStringFromClass([NSException class]) reason:report.errorMessage ?: @"" language:NULL lineOfCode:@"" stackTrace:@[] diff --git a/Source/BugsnagSink.m b/Source/BugsnagSink.m index 4769e821a..84712e5f2 100644 --- a/Source/BugsnagSink.m +++ b/Source/BugsnagSink.m @@ -72,9 +72,10 @@ - (void)filterReports:(NSArray*) reports return; } + NSDictionary *reportData = [self getBodyFromReports:bugsnagReports]; + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - NSDictionary *reportData = [self getBodyFromReports:bugsnagReports]; for (BugsnagBeforeNotifyHook hook in configuration.beforeNotifyHooks) { if (reportData) { reportData = hook(reports, reportData); @@ -91,6 +92,16 @@ - (void)filterReports:(NSArray*) reports return; } + [self sendReports:bugsnagReports + payload:reportData + toURL:configuration.notifyURL + onCompletion:onCompletion]; +} + +- (void)sendReports:(NSArray *)reports + payload:(NSDictionary *)reportData + toURL:(NSURL *)url + onCompletion:(KSCrashReportFilterCompletion) onCompletion { @try { NSError *error = nil; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:reportData @@ -103,7 +114,7 @@ - (void)filterReports:(NSArray*) reports } return; } - NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: configuration.notifyURL + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url cachePolicy: NSURLRequestReloadIgnoringLocalCacheData timeoutInterval: 15]; request.HTTPMethod = @"POST"; @@ -112,9 +123,9 @@ - (void)filterReports:(NSArray*) reports [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) { - if (onCompletion) - onCompletion(reports, connectionError == nil, connectionError); - }]; + if (onCompletion) + onCompletion(reports, connectionError == nil, connectionError); + }]; } @catch (NSException *exception) { if (onCompletion) { onCompletion(reports, NO, [NSError errorWithDomain:exception.reason From 20878ca17d046abd36dae72edd30995bfcdf1112 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 30 Jun 2016 14:26:10 +0100 Subject: [PATCH 07/12] Add tests for error reporting --- Source/BugsnagNotifier.m | 3 +- Tests/BugsnagSpec.m | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/Source/BugsnagNotifier.m b/Source/BugsnagNotifier.m index b6e27b37b..1689b0129 100644 --- a/Source/BugsnagNotifier.m +++ b/Source/BugsnagNotifier.m @@ -173,9 +173,10 @@ - (void)notifyError:(NSError *)error block:(void (^)(BugsnagCrashReport *))block message:error.localizedDescription block:^(BugsnagCrashReport * _Nonnull report) { NSMutableDictionary *metadata = [report.metaData mutableCopy]; - metadata[@"NSError"] = @{@"code": @(error.code), + metadata[@"nserror"] = @{@"code": @(error.code), @"domain": error.domain, @"reason": error.localizedFailureReason?: @"" }; + report.metaData = metadata; if (block) block(report); }]; diff --git a/Tests/BugsnagSpec.m b/Tests/BugsnagSpec.m index e683a880a..c6880a935 100644 --- a/Tests/BugsnagSpec.m +++ b/Tests/BugsnagSpec.m @@ -11,6 +11,12 @@ #define shouldSoon shouldEventuallyBeforeTimingOutAfter(0.1) +@interface BugsnagTestError : NSError +@end + +@implementation BugsnagTestError +@end + SPEC_BEGIN(BugsnagSpec) beforeAll(^{ @@ -117,6 +123,73 @@ [[expectFutureValue(requestEventKeyPath(@"breadcrumbs")) shouldSoon] equal:breadcrumbs]; }); }); + + describe(@"notifyError:", ^{ + + beforeEach(^{ + NSError *error = [BugsnagTestError errorWithDomain:@"com.bugsnag.ios-error" + code:420 + userInfo:@{NSLocalizedDescriptionKey: @"Stuff is broken", + NSLocalizedFailureReasonErrorKey: @"The rent is too high"}]; + [Bugsnag notifyError:error]; + }); + + it(@"sends the error class", ^{ + [[expectFutureValue(requestExceptionValue(@"errorClass")) shouldSoon] equal:@"BugsnagTestError"]; + }); + + it(@"sends the error message", ^{ + [[expectFutureValue(requestExceptionValue(@"message")) shouldSoon] equal:@"Stuff is broken"]; + }); + + it(@"sends the domain", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.domain")) shouldSoon] equal: @"com.bugsnag.ios-error"]; + }); + + it(@"sends the code", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.code")) shouldSoon] equal: @420]; + }); + + it(@"sends the failure reason", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.reason")) shouldSoon] equal: @"The rent is too high"]; + }); + }); + + describe(@"notifyError:", ^{ + beforeEach(^{ + NSError *error = [BugsnagTestError errorWithDomain:@"com.bugsnag.ios-error" + code:420 + userInfo:@{NSLocalizedDescriptionKey: @"Stuff is broken", + NSLocalizedFailureReasonErrorKey: @"The rent is too high"}]; + [Bugsnag notifyError:error block:^(BugsnagCrashReport * _Nonnull report) { + NSMutableDictionary *metadata = [report.metaData mutableCopy]; + metadata[@"nserror"] = @{ @"code": @504, @"domain": @"com.example.borg", @"reason": @"None" }; + report.metaData = metadata; + report.errorClass = @"Doughnut"; + report.errorMessage = @"None"; + }]; + }); + + it(@"updates the error class", ^{ + [[expectFutureValue(requestExceptionValue(@"errorClass")) shouldSoon] equal:@"Doughnut"]; + }); + + it(@"updates the error message", ^{ + [[expectFutureValue(requestExceptionValue(@"message")) shouldSoon] equal:@"None"]; + }); + + it(@"updates the code", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.code")) shouldSoon] equal: @504]; + }); + + it(@"updates the domain", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.domain")) shouldSoon] equal: @"com.example.borg"]; + }); + + it(@"update the failure reason", ^{ + [[expectFutureValue(requestEventKeyPath(@"metaData.nserror.reason")) shouldSoon] equal: @"None"]; + }); + }); }); SPEC_END From c2ecc92787388f3abbe3ae4a87e4585139e91ace Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 30 Jun 2016 16:01:41 +0100 Subject: [PATCH 08/12] Update headers in podspec --- Bugsnag.podspec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bugsnag.podspec.json b/Bugsnag.podspec.json index 75ac45770..c7e5e0aba 100644 --- a/Bugsnag.podspec.json +++ b/Bugsnag.podspec.json @@ -22,7 +22,7 @@ "requires_arc": true, "public_header_files": [ "Source/BSGKSCrashReportWriter.h", - "Source/Bugsnag{,MetaData,Configuration,Breadcrumb}.h" + "Source/Bugsnag{,MetaData,Configuration,Breadcrumb,CrashReport}.h" ], "dependencies": { "KSCrash/RecordingAdvanced": [ From 3bf80c9551e9c9bc93d475eca2b91878c8844550 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 6 Jul 2016 16:37:31 -0700 Subject: [PATCH 09/12] Make block intent clearer These callbacks are invoked before the reports are sent, rather than at crash time, like other Bugsnag libraries. Changing the name better reflects this behavior. --- Source/BugsnagConfiguration.h | 6 +++--- Source/BugsnagConfiguration.m | 8 ++++---- Source/BugsnagSink.m | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/BugsnagConfiguration.h b/Source/BugsnagConfiguration.h index 6c87886fe..fdeed4616 100644 --- a/Source/BugsnagConfiguration.h +++ b/Source/BugsnagConfiguration.h @@ -48,7 +48,7 @@ typedef void (^BugsnagNotifyBlock)(BugsnagCrashReport *_Nonnull report); * * @return YES if the report should be sent */ -typedef bool (^BugsnagBeforeNotifyBlock)( +typedef bool (^BugsnagBeforeSendBlock)( NSDictionary *_Nonnull rawEventData, BugsnagCrashReport *_Nonnull reports); @@ -108,7 +108,7 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( * Hooks for modifying crash reports before it is sent to Bugsnag */ @property(nonatomic, readonly, strong, nullable) - NSArray * beforeNotifyBlocks; + NSArray * beforeSendBlocks; /** * Optional handler invoked when a crash or fatal signal occurs */ @@ -136,7 +136,7 @@ typedef NSDictionary *_Nullable (^BugsnagBeforeNotifyHook)( * * @param block A block which returns YES if the report should be sent */ -- (void)addBeforeNotifyBlock:(BugsnagBeforeNotifyBlock _Nonnull)block; +- (void)addBeforeSendBlock:(BugsnagBeforeSendBlock _Nonnull)block; /** * Whether reports shoould be sent, based on release stage options diff --git a/Source/BugsnagConfiguration.m b/Source/BugsnagConfiguration.m index b585341d3..e59997bfb 100644 --- a/Source/BugsnagConfiguration.m +++ b/Source/BugsnagConfiguration.m @@ -32,7 +32,7 @@ @interface BugsnagConfiguration () @property(nonatomic, readwrite, strong) NSMutableArray *beforeNotifyHooks; -@property(nonatomic, readwrite, strong) NSMutableArray *beforeNotifyBlocks; +@property(nonatomic, readwrite, strong) NSMutableArray *BugsnagBeforeSendBlock; @end @implementation BugsnagConfiguration @@ -45,7 +45,7 @@ - (id)init { _autoNotify = true; _notifyURL = [NSURL URLWithString:@"https://notify.bugsnag.com/"]; _beforeNotifyHooks = [NSMutableArray new]; - _beforeNotifyBlocks = [NSMutableArray new]; + _BugsnagBeforeSendBlock = [NSMutableArray new]; _notifyReleaseStages = nil; _breadcrumbs = [BugsnagBreadcrumbs new]; #if DEBUG @@ -72,8 +72,8 @@ - (void)setUser:(NSString *)userId toTabWithName:@"user"]; } -- (void)addBeforeNotifyBlock:(BugsnagBeforeNotifyBlock)block { - [(NSMutableArray *)self.beforeNotifyBlocks addObject:[block copy]]; +- (void)addBeforeSendBlock:(BugsnagBeforeSendBlock)block { + [(NSMutableArray *)self.beforeSendBlocks addObject:[block copy]]; } - (void)addBeforeNotifyHook:(BugsnagBeforeNotifyHook)hook { diff --git a/Source/BugsnagSink.m b/Source/BugsnagSink.m index 84712e5f2..7951323d4 100644 --- a/Source/BugsnagSink.m +++ b/Source/BugsnagSink.m @@ -55,7 +55,7 @@ - (void)filterReports:(NSArray*) reports if (![bugsnagReport shouldBeSent]) continue; BOOL shouldSend = YES; - for (BugsnagBeforeNotifyBlock block in configuration.beforeNotifyBlocks) { + for (BugsnagBeforeSendBlock block in configuration.beforeSendBlocks) { shouldSend = block(report, bugsnagReport); if (!shouldSend) break; From 43f3a7f88cc34dbeaa44adaf30f59424ebb1cfee Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Wed, 6 Jul 2016 18:40:53 -0700 Subject: [PATCH 10/12] Document depth --- Source/BugsnagCrashReport.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/BugsnagCrashReport.h b/Source/BugsnagCrashReport.h index f02e3e608..688444a83 100644 --- a/Source/BugsnagCrashReport.h +++ b/Source/BugsnagCrashReport.h @@ -121,7 +121,9 @@ NSString *_Nonnull BSGFormatSeverity(BSGSeverity severity); /** * Property overrides */ -@property (nonatomic, readonly, copy, nonnull) NSDictionary *overrides; - -@property (nonatomic, readwrite) NSUInteger depth; +@property(nonatomic, readonly, copy, nonnull) NSDictionary *overrides; +/** + * Number of frames to discard at the top of the stacktrace + */ +@property(nonatomic, readwrite) NSUInteger depth; @end From 13e63ef95b987525b035c9c35f18f42dc84e4862 Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 21 Jul 2016 19:48:55 -0700 Subject: [PATCH 11/12] Add time to device state --- Source/BugsnagCrashReport.m | 3 +++ Tests/BugsnagSinkTests.m | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Source/BugsnagCrashReport.m b/Source/BugsnagCrashReport.m index 170c542cb..e7119c2c0 100644 --- a/Source/BugsnagCrashReport.m +++ b/Source/BugsnagCrashReport.m @@ -143,6 +143,9 @@ BSGDictSetSafeObject(deviceState, [report valueForKeyPath:@"system.memory.free"], @"freeMemory"); + BSGDictSetSafeObject(deviceState, + [report valueForKeyPath:@"report.timestamp"], + @"time"); return deviceState; } diff --git a/Tests/BugsnagSinkTests.m b/Tests/BugsnagSinkTests.m index db9c1045e..439281868 100644 --- a/Tests/BugsnagSinkTests.m +++ b/Tests/BugsnagSinkTests.m @@ -171,6 +171,11 @@ - (void)testEventMetadataErrorAddress { XCTAssertEqualObjects(address, @0); } +- (void)testTimestamp { + id timestamp = [[self.processedData[@"events"] firstObject] valueForKeyPath:@"deviceState.time"]; + XCTAssertEqualObjects(timestamp, @"2014-12-02T01:56:13Z"); +} + - (void)testEventMetadataErrorType { id errorType = [[self.processedData[@"events"] firstObject] valueForKeyPath:@"metaData.error.type"]; XCTAssertEqualObjects(errorType, @"user"); From 6b4a7ff8268c8931e7da7a8639f53cd15d35f0af Mon Sep 17 00:00:00 2001 From: Delisa Mason Date: Thu, 21 Jul 2016 20:11:47 -0700 Subject: [PATCH 12/12] Turn up --- Tests/BugsnagSpec.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/BugsnagSpec.m b/Tests/BugsnagSpec.m index c6880a935..6e4c7a7b5 100644 --- a/Tests/BugsnagSpec.m +++ b/Tests/BugsnagSpec.m @@ -9,7 +9,7 @@ #import #import "Bugsnag.h" -#define shouldSoon shouldEventuallyBeforeTimingOutAfter(0.1) +#define shouldSoon shouldEventuallyBeforeTimingOutAfter(0.2) @interface BugsnagTestError : NSError @end