Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor BugsnagApiClient #1450

Merged
merged 4 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳


@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