Skip to content

Commit

Permalink
Merge pull request #46 from launchdarkly/reportEnhancement
Browse files Browse the repository at this point in the history
add REPORT flag request
  • Loading branch information
arun251 authored Oct 4, 2017
2 parents ec7f1b3 + 6148f92 commit 27ba6bc
Show file tree
Hide file tree
Showing 13 changed files with 579 additions and 158 deletions.
6 changes: 6 additions & 0 deletions Darkly.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
833D08D21F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */ = {isa = PBXBuildFile; fileRef = 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */; };
8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */; };
8349F5211F195BCF00B1F3DB /* LDUserModel+Equatable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8349F5201F195BCF00B1F3DB /* LDUserModel+Equatable.m */; };
8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */; };
8369475C1F1FED400047697C /* boolConfigIsABool-false.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947591F1FED400047697C /* boolConfigIsABool-false.json */; };
8369475D1F1FED400047697C /* boolConfigIsABool-true.json in Resources */ = {isa = PBXBuildFile; fileRef = 8369475A1F1FED400047697C /* boolConfigIsABool-true.json */; };
8369475E1F1FED400047697C /* boolConfigIsABool2-true.json in Resources */ = {isa = PBXBuildFile; fileRef = 8369475B1F1FED400047697C /* boolConfigIsABool2-true.json */; };
Expand Down Expand Up @@ -273,6 +274,8 @@
8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+StringKey_Matchable.m"; sourceTree = "<group>"; };
8349F51F1F195BCF00B1F3DB /* LDUserModel+Equatable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDUserModel+Equatable.h"; sourceTree = "<group>"; };
8349F5201F195BCF00B1F3DB /* LDUserModel+Equatable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDUserModel+Equatable.m"; sourceTree = "<group>"; };
8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDConfig+Testable.h"; sourceTree = "<group>"; };
8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDConfig+Testable.m"; sourceTree = "<group>"; };
836947591F1FED400047697C /* boolConfigIsABool-false.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool-false.json"; sourceTree = "<group>"; };
8369475A1F1FED400047697C /* boolConfigIsABool-true.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool-true.json"; sourceTree = "<group>"; };
8369475B1F1FED400047697C /* boolConfigIsABool2-true.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool2-true.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -501,6 +504,8 @@
83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */,
83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */,
83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */,
8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */,
8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */,
);
path = Categories;
sourceTree = "<group>";
Expand Down Expand Up @@ -1082,6 +1087,7 @@
690347331E689B9F00E45133 /* NSArray+UnitTests.m in Sources */,
6903472C1E689B9F00E45133 /* LDClientTest.m in Sources */,
832C78901F2A8DF600E334A2 /* LDUserModel+JsonDecodeable.m in Sources */,
8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
3 changes: 3 additions & 0 deletions Darkly/DarklyConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ extern int const kMinimumPollingInterval;
extern int const kDefaultBackgroundFetchInterval;
extern int const kMinimumBackgroundFetchInterval;
extern int const kMillisInSecs;
extern NSInteger const kHTTPStatusCodeBadRequest;
extern NSInteger const kHTTPStatusCodeMethodNotAllowed;
extern NSInteger const kHTTPStatusCodeNotImplemented;
3 changes: 3 additions & 0 deletions Darkly/DarklyConstants.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@
int const kDefaultBackgroundFetchInterval = 3600;
int const kMinimumBackgroundFetchInterval = 900;
int const kMillisInSecs = 1000;
NSInteger const kHTTPStatusCodeBadRequest = 400;
NSInteger const kHTTPStatusCodeMethodNotAllowed = 405;
NSInteger const kHTTPStatusCodeNotImplemented = 501;
26 changes: 9 additions & 17 deletions Darkly/LDClientManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -158,25 +158,17 @@ -(void)syncWithServerForEvents {
}

-(void)syncWithServerForConfig {
if (!offlineEnabled) {
DEBUG_LOGX(@"ClientManager syncing config with server");
LDClient *client = [LDClient sharedInstance];
LDUserModel *currentUser = client.ldUser;

if (currentUser) {
NSString *jsonString = [currentUser convertToJson];
if (jsonString) {
NSString *encodedUser = [LDUtil base64UrlEncodeString:jsonString];
[[LDRequestManager sharedInstance] performFeatureFlagRequest:encodedUser];
} else {
DEBUG_LOGX(@"ClientManager is not able to convert user to json");
}
} else {
DEBUG_LOGX(@"ClientManager has no user so won't sync config with server");
}
} else {
if (offlineEnabled) {
DEBUG_LOGX(@"ClientManager is in offline mode so won't sync config with server");
return;
}

if (![LDClient sharedInstance].ldUser) {
DEBUG_LOGX(@"ClientManager has no user so won't sync config with server");
return;
}

[[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser];
}

- (void)flushEvents {
Expand Down
8 changes: 8 additions & 0 deletions Darkly/LDConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
*/
@property (nonatomic) BOOL streaming;

/**
Flag that enables REPORT HTTP method for feature flag requests. When useReport is false,
feature flag requests use the GET HTTP method. The default is NO.
Do not use unless advised by LaunchDarkly.
*/
@property (nonatomic, assign) BOOL useReport;

/**
Flag that enables debug mode to allow things such as logging.
*/
Expand All @@ -80,6 +87,7 @@
@return An instance of LDConfig object.
*/
- (instancetype _Nonnull)initWithMobileKey:(nonnull NSString *)mobileKey NS_DESIGNATED_INITIALIZER;
- (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode;

- (instancetype _Nonnull )init NS_UNAVAILABLE;

Expand Down
10 changes: 10 additions & 0 deletions Darkly/LDConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

@interface LDConfig()
@property (nonatomic, copy, nonnull) NSString* mobileKey;
@property (nonatomic, strong, nonnull) NSArray<NSNumber*> *flagRetryStatusCodes;
@end

@implementation LDConfig
Expand All @@ -26,6 +27,11 @@ - (instancetype)initWithMobileKey:(NSString *)mobileKey {
self.baseUrl = kBaseUrl;
self.eventsUrl = kEventsUrl;
self.streamUrl = kStreamUrl;
// self.flagRetryStatusCodes = @[@(kHTTPStatusCodeMethodNotAllowed),
// @(kHTTPStatusCodeBadRequest),
// @(kHTTPStatusCodeNotImplemented)];
self.flagRetryStatusCodes = @[]; //Temporarily, leave these codes empty to disable the REPORT fallback using GET capability
self.useReport = NO;

return self;
}
Expand Down Expand Up @@ -115,6 +121,10 @@ - (void)setDebugEnabled:(BOOL)debugEnabled {
DEBUG_LOG(@"Set LDConfig debug enabled: %d", debugEnabled);
}

- (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode {
return [self.flagRetryStatusCodes containsObject:@(statusCode)];
}

@end


Expand Down
9 changes: 3 additions & 6 deletions Darkly/LDRequestManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// Copyright © 2015 Catamorphic Co. All rights reserved.
//


#import "LDFlagConfigModel.h"
#import "LDUserModel.h"

@protocol RequestManagerDelegate <NSObject>

Expand All @@ -12,9 +11,7 @@

@end

@interface LDRequestManager : NSObject {

}
@interface LDRequestManager : NSObject

extern NSString * const kHeaderMobileKey;
@property (nonatomic) NSString* mobileKey;
Expand All @@ -25,7 +22,7 @@ extern NSString * const kHeaderMobileKey;

+(LDRequestManager *)sharedInstance;

-(void)performFeatureFlagRequest:(NSString *)encodedUser;
-(void)performFeatureFlagRequest:(LDUserModel *)user;

-(void)performEventRequest:(NSArray *)jsonEventArray;

Expand Down
173 changes: 123 additions & 50 deletions Darkly/LDRequestManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
#import "LDClientManager.h"
#import "LDConfig.h"

static NSString * const kFeatureFlagUrl = @"/msdk/eval/users/";
static NSString * const kFeatureFlagGetUrl = @"/msdk/eval/users/";
static NSString * const kFeatureFlagReportUrl = @"/msdk/eval/user";
static NSString * const kEventUrl = @"/mobile/events/bulk";
NSString * const kHeaderMobileKey = @"api_key ";
static NSString * const kConfigRequestCompletedNotification = @"config_request_completed_notification";
Expand All @@ -17,8 +18,7 @@ @implementation LDRequestManager

@synthesize mobileKey, baseUrl, eventsUrl, connectionTimeout, delegate;

+(LDRequestManager *)sharedInstance
{
+(LDRequestManager *)sharedInstance {
static LDRequestManager *sharedApiManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Expand All @@ -34,63 +34,91 @@ +(LDRequestManager *)sharedInstance
return sharedApiManager;
}

- (void)dealloc
{
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

-(void)performFeatureFlagRequest:(NSString *)encodedUser
-(void)performFeatureFlagRequest:(LDUserModel *)user
{
DEBUG_LOGX(@"RequestManager syncing config to server");
if (!mobileKey) {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no mobileKey");
return;
}

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
if (!user) {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no user");
return;
}

if (mobileKey) {
if (encodedUser) {
NSURLSession *defaultSession = [NSURLSession sharedSession];
NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagUrl];

requestUrl = [requestUrl stringByAppendingString:encodedUser];
if ([LDClient sharedInstance].ldConfig.useReport) {
DEBUG_LOGX(@"RequestManager syncing config to server via REPORT");
NSURLRequest *flagRequestUsingReportMethod = [self flagRequestUsingReportMethodForUser:user];
[self performFlagRequest:flagRequestUsingReportMethod completionHandler:^(NSData * _Nullable originalData, NSURLResponse * _Nullable originalResponse, NSError * _Nullable originalError) {

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]];
[request setTimeoutInterval:self.connectionTimeout];

[self addFeatureRequestHeaders:request];

NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// This comes in on a background thread, there's no reason to do any additional processing of
// the data on the main thread -- just the notification of the delegate on the main thread.
BOOL configProcessed = NO;
NSDictionary *responseDict = nil;
if (!error) {
NSError *jsonError;
NSDictionary * responseObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
if (responseObject) {
configProcessed = YES;
responseDict = responseObject;
}
if ([self shouldTryFlagGetRequestForFlagResponse:originalResponse]) {
NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user];
if (flagRequestUsingGetMethod) {
DEBUG_LOGX(@"RequestManager syncing config to server via GET");

[self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable retriedData, NSURLResponse * _Nullable retriedResponse, NSError * _Nullable retriedError) {
[self processFlagResponseWithData:retriedData error:retriedError];
}];
return;
}
dispatch_semaphore_signal(semaphore);
dispatch_async(dispatch_get_main_queue(), ^{
[delegate processedConfig:configProcessed jsonConfigDictionary:responseDict];
});

}];

[dataTask resume];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

} else {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no encodedUser");
}
}
[self processFlagResponseWithData:originalData error:originalError];
}];
} else {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no mobileKey");
DEBUG_LOGX(@"RequestManager syncing config to server via GET");

NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user];
[self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[self processFlagResponseWithData:data error:error];
}];
}
}

-(void)performEventRequest:(NSArray *)jsonEventArray
{
-(BOOL)shouldTryFlagGetRequestForFlagResponse:(NSURLResponse*)flagResponse {
if (!flagResponse) { return NO; }
if (![flagResponse isKindOfClass:[NSHTTPURLResponse class]]) { return NO; }
NSHTTPURLResponse *httpFlagResponse = (NSHTTPURLResponse*)flagResponse;
return [LDClient sharedInstance].ldConfig.useReport && [[LDClient sharedInstance].ldConfig isFlagRetryStatusCode:httpFlagResponse.statusCode];
}

-(void)processFlagResponseWithData:(NSData*)data error:(NSError*)error {
BOOL configProcessed = NO;
NSDictionary *featureFlags;
if (!error) {
NSError *jsonError;
featureFlags = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
configProcessed = featureFlags != nil;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate processedConfig:configProcessed jsonConfigDictionary:featureFlags];
});
}

-(void)performFlagRequest:(NSURLRequest*)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler {
if (!request) { return; }

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (completionHandler) {
completionHandler(data, response, error);
}
dispatch_semaphore_signal(semaphore);
}];

[dataTask resume];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

-(void)performEventRequest:(NSArray *)jsonEventArray {
DEBUG_LOGX(@"RequestManager syncing events to server");

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
Expand Down Expand Up @@ -131,14 +159,59 @@ -(void)performEventRequest:(NSArray *)jsonEventArray
}

#pragma mark - requests
-(void)addFeatureRequestHeaders:(NSMutableURLRequest *)request{
-(NSURLRequest*)flagRequestUsingReportMethodForUser:(LDUserModel*)user {
if (!user) {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no user");
return nil;
}
NSString *userJson = [user convertToJson];
if (!userJson) {
DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server");
return nil;
}

NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagReportUrl];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]];
request.HTTPMethod = @"REPORT";
request.HTTPBody = [userJson dataUsingEncoding:NSUTF8StringEncoding];
[request setTimeoutInterval:self.connectionTimeout];
[self addFeatureRequestHeaders:request];

return request;
}

-(NSURLRequest*)flagRequestUsingGetMethodForUser:(LDUserModel*)user {
if (!user) {
DEBUG_LOGX(@"RequestManager unable to sync config to server since no user");
return nil;
}
NSString *userJson = [user convertToJson];
if (!userJson) {
DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server");
return nil;
}
NSString *encodedUser = [LDUtil base64UrlEncodeString:userJson];
if (!encodedUser) {
DEBUG_LOGX(@"RequestManager could not base64Url encode user, aborting sync config to server");
return nil;
}
NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagGetUrl];
requestUrl = [requestUrl stringByAppendingString:encodedUser];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]];
[request setTimeoutInterval:self.connectionTimeout];
[self addFeatureRequestHeaders:request];

return request;
}

-(void)addFeatureRequestHeaders:(NSMutableURLRequest *)request {
NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey];

[request addValue:authKey forHTTPHeaderField:@"Authorization"];
[request addValue:[@"iOS/" stringByAppendingString:kClientVersion] forHTTPHeaderField:@"User-Agent"];
}

-(void)addEventRequestHeaders: (NSMutableURLRequest *)request {
-(void)addEventRequestHeaders: (NSMutableURLRequest *)request {
NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey];

[request addValue:authKey forHTTPHeaderField:@"Authorization"];
Expand Down
13 changes: 13 additions & 0 deletions DarklyTests/Categories/LDConfig+Testable.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// LDConfig+Testable.h
// Darkly
//
// Created by Mark Pokorny on 8/14/17. +JMJ
// Copyright © 2017 LaunchDarkly. All rights reserved.
//

#import <Darkly/Darkly.h>

@interface LDConfig (Testable)
@property (nonatomic, strong, nonnull, readonly) NSArray<NSNumber*> *flagRetryStatusCodes;
@end
Loading

0 comments on commit 27ba6bc

Please sign in to comment.