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

Diff ABT Experiments for real-time RC #12236

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f853b61
Add activated experiment metadata and payload keys to experiments table
qdpham13 May 9, 2023
9f1cea0
Call updateActivatedExperiments in activation method
qdpham13 May 9, 2023
ceae355
Update key name for experiments table
qdpham13 May 11, 2023
e7b92b1
Update test to include activation call
qdpham13 May 11, 2023
db6aff1
Update DB manager for Active experiments payload and metadata
qdpham13 May 12, 2023
5867edb
Fix metadata naming
qdpham13 May 12, 2023
c2b6097
add comment
qdpham13 May 12, 2023
d1b2349
Merge branch 'master'
qdpham13 May 17, 2023
937630d
Don't save experiments metadata b/c it isn't required to diff experim…
qdpham13 May 17, 2023
d49bffb
Create diff method
qdpham13 May 17, 2023
0067548
Adding diffing logic between fetched and activated experiments
qdpham13 May 17, 2023
d02b9e5
formatting
qdpham13 May 17, 2023
c9acee2
Refactor more of main diffing method
qdpham13 May 17, 2023
8337c60
use ABT diffing in ConfigUpdate logic
qdpham13 May 17, 2023
12be9c7
Add extra assertion for test
qdpham13 May 17, 2023
69d4061
Merge branch 'abt-exp-meta'
qdpham13 May 17, 2023
92efb59
Add tests
qdpham13 May 18, 2023
bac29da
Add in commenting
qdpham13 May 18, 2023
829d953
Address PR comments
qdpham13 May 24, 2023
9a0f4e6
update naming
qdpham13 May 26, 2023
bc78a5e
Merge branch 'abt-exp-meta'
qdpham13 May 26, 2023
707f5f1
Merge branch 'master'
qdpham13 May 26, 2023
0ce47b9
formatting
qdpham13 May 26, 2023
5166676
Update diffing logic
qdpham13 Jun 5, 2023
bf27372
Merge branch 'master'
qdpham13 Jun 5, 2023
756df3b
format and add comments
qdpham13 Jun 5, 2023
a1a01e6
Fix grammar
qdpham13 Jun 5, 2023
00d8284
Update comment
qdpham13 Jun 5, 2023
693a2de
Remove unused constant
qdpham13 Jun 5, 2023
9275933
Move diffing logic to ConfigContainer
qdpham13 Jun 6, 2023
b68a3df
remove space
qdpham13 Jun 6, 2023
6dbc9c6
Delete TestABTPayload2.txt
qdpham13 Jun 6, 2023
4e4f4d9
Merge branch 'master'
qdpham13 Jun 6, 2023
e365cce
Merge remote-tracking branch 'refs/remotes/origin/abt-exp-diff-2'
qdpham13 Jun 6, 2023
6e12a8c
Make it easier to test
qdpham13 Jun 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 104 additions & 2 deletions FirebaseRemoteConfig/Sources/RCNConfigContent.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

#import "FirebaseCore/Extension/FirebaseCoreInternal.h"

static NSString *const kAffectedParameterKeys = @"affectedParameterKeys";

@implementation RCNConfigContent {
/// Active config data that is currently used.
NSMutableDictionary *_activeConfig;
Expand Down Expand Up @@ -398,13 +400,108 @@ - (BOOL)checkAndWaitForInitialDatabaseLoad {
return true;
}

/// Load active and fetched experiment payloads and return them in a map.
- (NSDictionary<NSString *, NSMutableArray<NSData *> *> *)loadExperimentsPayloads {
__block NSMutableArray *activeExperimentPayloads = [[NSMutableArray alloc] init];
__block NSMutableArray *experimentPayloads = [[NSMutableArray alloc] init];

/// Load experiments from DB.
RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary<NSString *, id> *result) {
if (result[@RCNExperimentTableKeyPayload]) {
experimentPayloads = [result[@RCNExperimentTableKeyPayload] mutableCopy];
}
if (result[@RCNExperimentTableKeyActivePayload]) {
activeExperimentPayloads = [result[@RCNExperimentTableKeyActivePayload] mutableCopy];
}
};
[_DBManager loadExperimentWithCompletionHandler:completionHandler];

return @{
@RCNExperimentTableKeyPayload : experimentPayloads,
@RCNExperimentTableKeyActivePayload : activeExperimentPayloads
};
}

/// Creates a map where the key is the config key and the value if the experiment description.
- (NSMutableDictionary *)createExperimentsMap:(NSMutableArray<NSData *> *)experiments {
NSMutableDictionary<NSString *, NSMutableDictionary *> *experimentsMap =
[[NSMutableDictionary alloc] init];

/// Iterate through all the experiments and check if they contain `affectedParameterKeys`.
for (NSData *experiment in experiments) {
NSError *error;
NSDictionary *experimentJSON =
[NSJSONSerialization JSONObjectWithData:experiment
options:NSJSONReadingMutableContainers
error:&error];
if (!error && experimentJSON) {
if ([experimentJSON objectForKey:kAffectedParameterKeys]) {
NSMutableArray *configKeys =
(NSMutableArray *)[experimentJSON objectForKey:kAffectedParameterKeys];
NSMutableDictionary *experimentCopy = [experimentJSON mutableCopy];
/// Remove `affectedParameterKeys` because the values come out of order and could affect the
/// diffing.
[experimentCopy removeObjectForKey:kAffectedParameterKeys];

/// Map experiments to config keys.
for (NSString *key in configKeys) {
[experimentsMap setObject:experimentCopy forKey:key];
}
}
}
}

return experimentsMap;
}

/// Returns keys that were affected by experiment changes.
- (NSMutableSet<NSString *> *)
getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads {
NSMutableSet<NSString *> *changedKeys = [[NSMutableSet alloc] init];

/// Create config keys to experiments map.
NSMutableDictionary *activeExperimentsMap = [self createExperimentsMap:activeExperimentPayloads];
NSMutableDictionary *fetchedExperimentsMap = [self createExperimentsMap:experimentPayloads];

/// Iterate through active experiment's keys and compare them to fetched experiment's keys.
for (NSString *key in [activeExperimentsMap allKeys]) {
if (![fetchedExperimentsMap objectForKey:key]) {
[changedKeys addObject:key];
} else {
if (![[activeExperimentsMap objectForKey:key]
isEqualToDictionary:[fetchedExperimentsMap objectForKey:key]]) {
[changedKeys addObject:key];
}
}
}

/// Iterate through fetched experiment's keys and compare them to active experiment's keys.
for (NSString *key in [fetchedExperimentsMap allKeys]) {
if (![activeExperimentsMap objectForKey:key]) {
[changedKeys addObject:key];
} else {
if (![[fetchedExperimentsMap objectForKey:key]
isEqualToDictionary:[activeExperimentsMap objectForKey:key]]) {
[changedKeys addObject:key];
}
}
}

return changedKeys;
}

// Compare fetched config with active config and output what has changed
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace {
// TODO: handle diff in experiment metadata

FIRRemoteConfigUpdate *configUpdate;
NSMutableSet<NSString *> *updatedKeys = [[NSMutableSet alloc] init];

NSDictionary *experiments = [self loadExperimentsPayloads];
NSMutableSet *changedExperimentKeys = [self
getKeysAffectedByChangedExperiments:[experiments
objectForKey:@RCNExperimentTableKeyActivePayload]
fetchedExperimentPayloads:[experiments objectForKey:@RCNExperimentTableKeyPayload]];

NSDictionary *fetchedConfig =
_fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init];
NSDictionary *activeConfig =
Expand Down Expand Up @@ -439,6 +536,11 @@ - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace
}
}

// Add params affected by changed experiments.
for (NSString *key in changedExperimentKeys) {
[updatedKeys addObject:key];
}

configUpdate = [[FIRRemoteConfigUpdate alloc] initWithUpdatedKeys:updatedKeys];
return configUpdate;
}
Expand Down
151 changes: 151 additions & 0 deletions FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"

@interface RCNConfigContent (Testing)
- (NSMutableSet<NSString *> *)
getKeysAffectedByChangedExperiments:(NSMutableArray *)activeExperimentPayloads
fetchedExperimentPayloads:(NSMutableArray *)experimentPayloads;
- (BOOL)checkAndWaitForInitialDatabaseLoad;
@end

Expand Down Expand Up @@ -349,6 +352,129 @@ - (void)testConfigUpdate_noChange_emptyResponse {
XCTAssertTrue([update updatedKeys].count == 0);
}

- (void)testConfigUpdate_noParamChange_butExperimentChange {
NSString *namespace = @"test_namespace";
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *experimentKeys = [[NSMutableSet alloc] init];
[experimentKeys addObject:@"key_2"];
id configMock = OCMPartialMock(configContent);
OCMStub([configMock getKeysAffectedByChangedExperiments:OCMOCK_ANY
fetchedExperimentPayloads:OCMOCK_ANY])
.andReturn(experimentKeys);

// populate fetched config
NSMutableDictionary *fetchResponse =
[self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil];
[configMock updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

// active config is the same as fetched config
FIRRemoteConfigValue *value =
[[FIRRemoteConfigValue alloc] initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding]
source:FIRRemoteConfigSourceRemote];
NSDictionary *namespaceToConfig = @{namespace : @{@"key1" : value}};
[configMock copyFromDictionary:namespaceToConfig
toSource:RCNDBSourceActive
forNamespace:namespace];

FIRRemoteConfigUpdate *update = [configMock getConfigUpdateForNamespace:namespace];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:@"key_2"]);
}

- (void)testExperimentDiff_addedExperiment {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@"exp_2" forKey:@"experimentId"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentMetadata {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@"var_2" forKey:@"variantId"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentKeys {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];

NSError *dataError;
NSMutableDictionary *payload =
[NSJSONSerialization JSONObjectWithData:payloadData1
options:NSJSONReadingMutableContainers
error:&dataError];
[payload setValue:@[ @"test_key_1", @"test_key_2" ] forKey:@"affectedParameterKeys"];
NSError *jsonError;
NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload
options:kNilOptions
error:&jsonError];
NSMutableArray *experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_2"]);
}

- (void)testExperimentDiff_deletedExperiment {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
NSMutableArray *experimentPayloads = [@[] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_noChange {
NSData *payloadData1 = [[self class] payloadDataFromTestFile];
NSMutableArray *activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
NSMutableArray *experimentPayloads = [@[ payloadData1 ] mutableCopy];

RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:nil];
NSMutableSet<NSString *> *changedKeys =
[configContent getKeysAffectedByChangedExperiments:activeExperimentPayloads
fetchedExperimentPayloads:experimentPayloads];
XCTAssertTrue([changedKeys count] == 0);
}

- (void)testConfigUpdate_paramAdded_returnsNewKey {
NSString *namespace = @"test_namespace";
NSString *newParam = @"key2";
Expand Down Expand Up @@ -501,4 +627,29 @@ - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)co
return fetchResponse;
}

+ (NSData *)payloadDataFromTestFile {
#if SWIFT_PACKAGE
NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
#else
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
#endif
NSString *testJsonDataFilePath = [bundle pathForResource:@"TestABTPayload" ofType:@"txt"];
NSError *readTextError = nil;
NSString *fileText = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath
encoding:NSUTF8StringEncoding
error:&readTextError];

NSData *fileData = [fileText dataUsingEncoding:kCFStringEncodingUTF8];

NSError *jsonDictionaryError = nil;
NSMutableDictionary *jsonDictionary =
[[NSJSONSerialization JSONObjectWithData:fileData
options:kNilOptions
error:&jsonDictionaryError] mutableCopy];
NSError *jsonDataError = nil;
return [NSJSONSerialization dataWithJSONObject:jsonDictionary
options:kNilOptions
error:&jsonDataError];
}

@end
3 changes: 2 additions & 1 deletion FirebaseRemoteConfig/Tests/Unit/TestABTPayload.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
{
"experimentId": "exp_1"
}
]
],
"affectedParameterKeys": ["test_key_1"]
}