Skip to content

Commit

Permalink
Merge pull request #1450 from bugsnag/nickdowell/api-client
Browse files Browse the repository at this point in the history
Refactor BugsnagApiClient
  • Loading branch information
nickdowell committed Aug 8, 2022
2 parents 2942a09 + 39477aa commit 287d1e9
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 156 deletions.
2 changes: 0 additions & 2 deletions Bugsnag/Configuration/BugsnagConfiguration+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ NS_ASSUME_NONNULL_BEGIN

@property (readonly, nonatomic) BOOL shouldSendReports;

@property (readonly, nonatomic) NSDictionary<NSString *, id> *sessionApiHeaders;

@property (readonly, nullable, nonatomic) NSURL *sessionURL;

@property (readwrite, retain, nonnull, nonatomic) BugsnagUser *user;
Expand Down
8 changes: 0 additions & 8 deletions Bugsnag/Configuration/BugsnagConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

#import "BSGConfigurationBuilder.h"
#import "BSGKeys.h"
#import "BSG_RFC3339DateTool.h"
#import "BugsnagApiClient.h"
#import "BugsnagEndpointConfiguration.h"
#import "BugsnagErrorTypes.h"
Expand Down Expand Up @@ -361,13 +360,6 @@ - (void)removeOnBreadcrumbBlock:(BugsnagOnBreadcrumbBlock)block {
// MARK: -
// =============================================================================

- (NSDictionary *)sessionApiHeaders {
return @{BugsnagHTTPHeaderNameApiKey: self.apiKey ?: @"",
BugsnagHTTPHeaderNamePayloadVersion: @"1.0",
BugsnagHTTPHeaderNameSentAt: [BSG_RFC3339DateTool stringFromDate:[NSDate date]]
};
}

- (void)setEndpoints:(BugsnagEndpointConfiguration *)endpoints {
if ([self isValidURLString:endpoints.notify]) {
_endpoints.notify = [endpoints.notify copy];
Expand Down
2 changes: 0 additions & 2 deletions Bugsnag/Delivery/BSGEventUploadOperation.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ static const NSUInteger MaxPersistedSize = 1000000;

@protocol BSGEventUploadOperationDelegate <NSObject>

@property (readonly, nonatomic) BugsnagApiClient *apiClient;

@property (readonly, nonatomic) BugsnagConfiguration *configuration;

@property (readonly, nonatomic) BugsnagNotifier *notifier;
Expand Down
26 changes: 15 additions & 11 deletions Bugsnag/Delivery/BSGEventUploadOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

#import "BSGFileLocations.h"
#import "BSGInternalErrorReporter.h"
#import "BSGJSONSerialization.h"
#import "BSGKeys.h"
#import "BSG_RFC3339DateTool.h"
#import "BugsnagAppWithState+Private.h"
#import "BugsnagConfiguration+Private.h"
#import "BugsnagError+Private.h"
Expand Down Expand Up @@ -123,7 +123,6 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
NSMutableDictionary *requestHeaders = [NSMutableDictionary dictionary];
requestHeaders[BugsnagHTTPHeaderNameApiKey] = apiKey;
requestHeaders[BugsnagHTTPHeaderNamePayloadVersion] = EventPayloadVersion;
requestHeaders[BugsnagHTTPHeaderNameSentAt] = [BSG_RFC3339DateTool stringFromDate:[NSDate date]];
requestHeaders[BugsnagHTTPHeaderNameStacktraceTypes] = [event.stacktraceTypes componentsJoinedByString:@","];

NSURL *notifyURL = configuration.notifyURL;
Expand All @@ -133,29 +132,34 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion
return;
}

__block NSData *HTTPBody =
[delegate.apiClient sendJSONPayload:requestPayload headers:requestHeaders toURL:notifyURL
completionHandler:^(BugsnagApiClientDeliveryStatus status, __unused NSError *deliveryError) {

NSData *data = BSGJSONDataFromDictionary(requestPayload, NULL);
if (!data) {
bsg_log_debug(@"Encoding failed; will discard event %@", self.name);
[self deleteEvent];
completionHandler();
return;
}

BSGPostJSONData(configuration.session, data, requestHeaders, notifyURL, ^(BSGDeliveryStatus status, __unused NSError *deliveryError) {
switch (status) {
case BugsnagApiClientDeliveryStatusDelivered:
case BSGDeliveryStatusDelivered:
bsg_log_debug(@"Uploaded event %@", self.name);
[self deleteEvent];
break;

case BugsnagApiClientDeliveryStatusFailed:
case BSGDeliveryStatusFailed:
bsg_log_debug(@"Upload failed retryably for event %@", self.name);
[self prepareForRetry:originalPayload ?: eventPayload HTTPBodySize:HTTPBody.length];
[self prepareForRetry:originalPayload ?: eventPayload HTTPBodySize:data.length];
break;

case BugsnagApiClientDeliveryStatusUndeliverable:
case BSGDeliveryStatusUndeliverable:
bsg_log_debug(@"Upload failed; will discard event %@", self.name);
[self deleteEvent];
break;
}

completionHandler();
}];
});
}

// MARK: Subclassing
Expand Down
2 changes: 0 additions & 2 deletions Bugsnag/Delivery/BSGEventUploader.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,11 @@ @interface BSGEventUploader () <BSGEventUploadOperationDelegate>

@implementation BSGEventUploader

@synthesize apiClient = _apiClient;
@synthesize configuration = _configuration;
@synthesize notifier = _notifier;

- (instancetype)initWithConfiguration:(BugsnagConfiguration *)configuration notifier:(BugsnagNotifier *)notifier {
if ((self = [super init])) {
_apiClient = [[BugsnagApiClient alloc] initWithSession:configuration.session];
_configuration = configuration;
_eventsDirectory = [BSGFileLocations current].events;
_kscrashReportsDirectory = [BSGFileLocations current].kscrashReports;
Expand Down
40 changes: 22 additions & 18 deletions Bugsnag/Delivery/BSGSessionUploader.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

@interface BSGSessionUploader ()
@property (nonatomic) NSMutableSet *activeIds;
@property (nonatomic) BugsnagApiClient *apiClient;
@property(nonatomic) BugsnagConfiguration *config;
@end

Expand All @@ -40,25 +39,24 @@ @implementation BSGSessionUploader
- (instancetype)initWithConfig:(BugsnagConfiguration *)config notifier:(BugsnagNotifier *)notifier {
if ((self = [super init])) {
_activeIds = [NSMutableSet new];
_apiClient = [[BugsnagApiClient alloc] initWithSession:config.session];
_config = config;
_notifier = notifier;
}
return self;
}

- (void)uploadSession:(BugsnagSession *)session {
[self sendSession:session completionHandler:^(BugsnagApiClientDeliveryStatus status) {
[self sendSession:session completionHandler:^(BSGDeliveryStatus status) {
switch (status) {
case BugsnagApiClientDeliveryStatusDelivered:
case BSGDeliveryStatusDelivered:
[self processStoredSessions];
break;

case BugsnagApiClientDeliveryStatusFailed:
case BSGDeliveryStatusFailed:
[self storeSession:session]; // Retry later
break;

case BugsnagApiClientDeliveryStatusUndeliverable:
case BSGDeliveryStatusUndeliverable:
break;
}
}];
Expand Down Expand Up @@ -108,8 +106,8 @@ - (void)processStoredSessions {
[self.activeIds addObject:file];
}

[self sendSession:session completionHandler:^(BugsnagApiClientDeliveryStatus status) {
if (status != BugsnagApiClientDeliveryStatusFailed) {
[self sendSession:session completionHandler:^(BSGDeliveryStatus status) {
if (status != BSGDeliveryStatusFailed) {
[fileManager removeItemAtPath:file error:nil];
}
@synchronized (self.activeIds) {
Expand All @@ -135,25 +133,24 @@ - (void)pruneFiles {
//
// https://bugsnagsessiontrackingapi.docs.apiary.io/#reference/0/session/report-a-session-starting
//
- (void)sendSession:(BugsnagSession *)session completionHandler:(nonnull void (^)(BugsnagApiClientDeliveryStatus status))completionHandler {
- (void)sendSession:(BugsnagSession *)session completionHandler:(nonnull void (^)(BSGDeliveryStatus status))completionHandler {
NSString *apiKey = [self.config.apiKey copy];
if (!apiKey) {
bsg_log_err(@"Cannot send session because no apiKey is configured.");
completionHandler(BugsnagApiClientDeliveryStatusUndeliverable);
completionHandler(BSGDeliveryStatusUndeliverable);
return;
}

NSURL *url = self.config.sessionURL;
if (!url) {
bsg_log_err(@"Cannot send session because no endpoint is configured.");
completionHandler(BugsnagApiClientDeliveryStatusUndeliverable);
completionHandler(BSGDeliveryStatusUndeliverable);
return;
}

NSDictionary *headers = @{
BugsnagHTTPHeaderNameApiKey: apiKey,
BugsnagHTTPHeaderNamePayloadVersion: @"1.0",
BugsnagHTTPHeaderNameSentAt: [BSG_RFC3339DateTool stringFromDate:[NSDate date]] ?: [NSNull null]
BugsnagHTTPHeaderNamePayloadVersion: @"1.0"
};

NSDictionary *payload = @{
Expand All @@ -167,20 +164,27 @@ - (void)sendSession:(BugsnagSession *)session completionHandler:(nonnull void (^
}]
};

[self.apiClient sendJSONPayload:payload headers:headers toURL:url completionHandler:^(BugsnagApiClientDeliveryStatus status, NSError *error) {
NSData *data = BSGJSONDataFromDictionary(payload, NULL);
if (!data) {
bsg_log_err(@"Failed to encode session %@", session.id);
completionHandler(BSGDeliveryStatusUndeliverable);
return;
}

BSGPostJSONData(self.config.session, data, headers, url, ^(BSGDeliveryStatus status, NSError *error) {
switch (status) {
case BugsnagApiClientDeliveryStatusDelivered:
case BSGDeliveryStatusDelivered:
bsg_log_info(@"Sent session %@", session.id);
break;
case BugsnagApiClientDeliveryStatusFailed:
case BSGDeliveryStatusFailed:
bsg_log_warn(@"Failed to send sessions: %@", error);
break;
case BugsnagApiClientDeliveryStatusUndeliverable:
case BSGDeliveryStatusUndeliverable:
bsg_log_warn(@"Failed to send sessions: %@", error);
break;
}
completionHandler(status);
}];
});
}

@end
Expand Down
25 changes: 10 additions & 15 deletions Bugsnag/Delivery/BugsnagApiClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,21 @@ static BugsnagHTTPHeaderName const BugsnagHTTPHeaderNamePayloadVersion = @"B
static BugsnagHTTPHeaderName const BugsnagHTTPHeaderNameSentAt = @"Bugsnag-Sent-At";
static BugsnagHTTPHeaderName const BugsnagHTTPHeaderNameStacktraceTypes = @"Bugsnag-Stacktrace-Types";

typedef NS_ENUM(NSInteger, BugsnagApiClientDeliveryStatus) {
typedef NS_ENUM(NSInteger, BSGDeliveryStatus) {
/// The payload was delivered successfully and can be deleted.
BugsnagApiClientDeliveryStatusDelivered,
BSGDeliveryStatusDelivered,
/// The payload was not delivered but can be retried, e.g. when there was a loss of connectivity.
BugsnagApiClientDeliveryStatusFailed,
BSGDeliveryStatusFailed,
/// The payload cannot be delivered and should be deleted without attempting to retry.
BugsnagApiClientDeliveryStatusUndeliverable,
BSGDeliveryStatusUndeliverable,
};

@interface BugsnagApiClient : NSObject
void BSGPostJSONData(NSURLSession *URLSession,
NSData *data,
NSDictionary<BugsnagHTTPHeaderName, NSString *> *headers,
NSURL *url,
void (^ completionHandler)(BSGDeliveryStatus status, NSError *_Nullable error));

- (instancetype)initWithSession:(nullable NSURLSession *)session;

- (nullable NSData *)sendJSONPayload:(NSDictionary *)payload
headers:(NSDictionary<BugsnagHTTPHeaderName, NSString *> *)headers
toURL:(NSURL *)url
completionHandler:(void (^)(BugsnagApiClientDeliveryStatus status, NSError *_Nullable error))completionHandler;

+ (NSString *)SHA1HashStringWithData:(NSData *)data;

@end
NSString *_Nullable BSGIntegrityHeaderValue(NSData *_Nullable data);

NS_ASSUME_NONNULL_END
78 changes: 22 additions & 56 deletions Bugsnag/Delivery/BugsnagApiClient.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import "BSGJSONSerialization.h"
#import "BSGKeys.h"
#import "BSG_RFC3339DateTool.h"
#import "Bugsnag.h"
#import "BugsnagConfiguration.h"
#import "BugsnagLogger.h"
Expand All @@ -30,45 +31,28 @@ typedef NS_ENUM(NSInteger, HTTPStatusCode) {
HTTPStatusCodeTooManyRequests = 429,
};

@interface BugsnagApiClient()
@property (nonatomic, strong) NSURLSession *session;
@end

@implementation BugsnagApiClient

- (instancetype)initWithSession:(nullable NSURLSession *)session {
if ((self = [super init])) {
_session = session ?: [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]];
}
return self;
}

#pragma mark - Delivery

- (NSData *)sendJSONPayload:(NSDictionary *)payload
headers:(NSDictionary<BugsnagHTTPHeaderName, NSString *> *)headers
toURL:(NSURL *)url
completionHandler:(void (^)(BugsnagApiClientDeliveryStatus status, NSError *_Nullable error))completionHandler {
void BSGPostJSONData(NSURLSession *URLSession,
NSData *data,
NSDictionary<BugsnagHTTPHeaderName, NSString *> *headers,
NSURL *url,
void (^ completionHandler)(BSGDeliveryStatus status, NSError *_Nullable error)) {

NSError *error = nil;
NSData *data = BSGJSONDataFromDictionary(payload, &error);
if (!data) {
bsg_log_err(@"Error: Could not encode JSON payload passed to %s", __PRETTY_FUNCTION__);
completionHandler(BugsnagApiClientDeliveryStatusUndeliverable, error);
return nil;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:BSGIntegrityHeaderValue(data) forHTTPHeaderField:BugsnagHTTPHeaderNameIntegrity];
[request setValue:[BSG_RFC3339DateTool stringFromDate:[NSDate date]] forHTTPHeaderField:BugsnagHTTPHeaderNameSentAt];

NSMutableDictionary<BugsnagHTTPHeaderName, NSString *> *mutableHeaders = [headers mutableCopy];
mutableHeaders[BugsnagHTTPHeaderNameIntegrity] = [NSString stringWithFormat:@"sha1 %@", [BugsnagApiClient SHA1HashStringWithData:data]];
for (BugsnagHTTPHeaderName name in headers) {
[request setValue:headers[name] forHTTPHeaderField:name];
}

NSMutableURLRequest *request = [self prepareRequest:url headers:mutableHeaders];
bsg_log_debug(@"Sending %lu byte payload to %@", (unsigned long)data.length, url);

[[self.session uploadTaskWithRequest:request fromData:data completionHandler:^(__unused NSData *responseData,
NSURLResponse *response, NSError *connectionError) {
[[URLSession uploadTaskWithRequest:request fromData:data completionHandler:^(__unused NSData *responseData, NSURLResponse *response, NSError *error) {
if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
bsg_log_debug(@"Request to %@ completed with error %@", url, error);
completionHandler(BugsnagApiClientDeliveryStatusFailed, connectionError ?:
completionHandler(BSGDeliveryStatusFailed, error ?:
[NSError errorWithDomain:@"BugsnagApiClientErrorDomain" code:0 userInfo:@{
NSLocalizedDescriptionKey: @"Request failed: no response was received",
NSURLErrorFailingURLErrorKey: url }]);
Expand All @@ -79,11 +63,11 @@ - (NSData *)sendJSONPayload:(NSDictionary *)payload
bsg_log_debug(@"Request to %@ completed with status code %ld", url, (long)statusCode);

if (statusCode / 100 == 2) {
completionHandler(BugsnagApiClientDeliveryStatusDelivered, nil);
completionHandler(BSGDeliveryStatusDelivered, nil);
return;
}

connectionError = [NSError errorWithDomain:@"BugsnagApiClientErrorDomain" code:1 userInfo:@{
error = [NSError errorWithDomain:@"BugsnagApiClientErrorDomain" code:1 userInfo:@{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Request failed: unacceptable status code %ld (%@)",
(long)statusCode, [NSHTTPURLResponse localizedStringForStatusCode:statusCode]],
NSURLErrorFailingURLErrorKey: url }];
Expand All @@ -96,41 +80,23 @@ - (NSData *)sendJSONPayload:(NSDictionary *)payload
statusCode != HTTPStatusCodeProxyAuthenticationRequired &&
statusCode != HTTPStatusCodeClientTimeout &&
statusCode != HTTPStatusCodeTooManyRequests) {
completionHandler(BugsnagApiClientDeliveryStatusUndeliverable, connectionError);
completionHandler(BSGDeliveryStatusUndeliverable, error);
return;
}

completionHandler(BugsnagApiClientDeliveryStatusFailed, connectionError);
completionHandler(BSGDeliveryStatusFailed, error);
}] resume];
return data;
}

- (NSMutableURLRequest *)prepareRequest:(NSURL *)url
headers:(NSDictionary *)headers {
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:15];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

for (NSString *key in [headers allKeys]) {
[request setValue:headers[key] forHTTPHeaderField:key];
}
return request;
}

+ (NSString *)SHA1HashStringWithData:(NSData *)data {
NSString * BSGIntegrityHeaderValue(NSData *data) {
if (!data) {
return nil;
}
unsigned char md[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(data.bytes, (CC_LONG)data.length, md);
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
return [NSString stringWithFormat:@"sha1 %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
md[0], md[1], md[2], md[3], md[4],
md[5], md[6], md[7], md[8], md[9],
md[10], md[11], md[12], md[13], md[14],
md[15], md[16], md[17], md[18], md[19]];
}

@end
Loading

0 comments on commit 287d1e9

Please sign in to comment.