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 #11398

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 29 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion FirebaseRemoteConfig/Sources/RCNConfigContent.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) {
- (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace;

/// Returns the updated parameters between fetched and active config.
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace;
- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace
withExperimentChanges:
(NSMutableSet<NSString *> *)changedExperimentKeys;

@end
7 changes: 4 additions & 3 deletions FirebaseRemoteConfig/Sources/RCNConfigContent.m
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,12 @@ - (BOOL)checkAndWaitForInitialDatabaseLoad {
}

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

- (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace
withExperimentChanges:
(NSMutableSet<NSString *> *)changedExperimentKeys {
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of doing the diffing elsewhere and passing in the changed keys, could we not read the experiment metadata directly from the dbManager here and do the diffing in this method?

feels unnecessary to have to explicitly pass in the experiment-related keys to getConfigUpdateForNamespace, when the diffing logic can be self-contained within RCNConfigContent

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thinking is that all experiment logic lives in RCNConfigExperiments including in-memory copies of active and fetched experiments so why not do the diffing where all of the experiments already are so that we don't need to load all of the data elsewhere to do the same thing. Plus where it's called in Fetch allows us to easily pass in the keys after getting the diffs from experiments

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But I could go either way

Copy link
Contributor

Choose a reason for hiding this comment

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

I get that it works as-is, but you are having the Fetch class partially compute the results, then pass it to the Config class, just to have it passed back. Fetch doesn't/shouldn't need to know the details of how the ConfigUpdate is computed, otherwise in the future if we decide different metadata is needed (which will likely happen soon), fetch (or anywhere else we call this method) may need to be updated

FIRRemoteConfigUpdate *configUpdate;
NSMutableSet<NSString *> *updatedKeys = [[NSMutableSet alloc] init];
updatedKeys = [[updatedKeys setByAddingObjectsFromSet:changedExperimentKeys] mutableCopy];

NSDictionary *fetchedConfig =
_fetchedConfig[FIRNamespace] ? _fetchedConfig[FIRNamespace] : [[NSDictionary alloc] init];
Expand Down
3 changes: 3 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNConfigExperiment.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@

/// Update experiments to Firebase Analytics when `activateWithCompletion:` happens.
- (void)updateExperimentsWithHandler:(nullable void (^)(NSError *_Nullable error))handler;

/// Return config keys from experiments that have changed.
- (NSMutableSet<NSString *> *_Nonnull)getKeysAffectedByChangedExperiments;
@end
68 changes: 68 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNConfigExperiment.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
static NSString *const kMethodNameLatestStartTime =
@"latestExperimentStartTimestampBetweenTimestamp:andPayloads:";

static NSString *const kAffectedParameterKeys = @"affectedParameterKeys";

@interface RCNConfigExperiment ()
@property(nonatomic, strong)
NSMutableArray<NSData *> *experimentPayloads; ///< Experiment payloads.
Expand Down Expand Up @@ -197,4 +199,70 @@ - (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)exist
latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime
andPayloads:_experimentPayloads];
}

/// 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 {
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;
}
@end
15 changes: 9 additions & 6 deletions FirebaseRemoteConfig/Sources/RCNConfigFetch.m
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ - (void)realtimeFetchConfigWithNoExpirationDuration:(NSInteger)fetchAttemptNumbe
if (strongSelf->_settings.lastFetchTimeInterval > 0) {
FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052",
@"A fetch is already in progress. Using previous fetch results.");
FIRRemoteConfigUpdate *update =
[self->_content getConfigUpdateForNamespace:self->_FIRNamespace];
FIRRemoteConfigUpdate *update = [self->_content
getConfigUpdateForNamespace:self->_FIRNamespace
withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]];
return [strongSelf reportCompletionWithStatus:strongSelf->_settings.lastFetchStatus
withUpdate:update
withError:nil
Expand Down Expand Up @@ -537,8 +538,9 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties
if (!data) {
FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000043", @"RCN Fetch: No data in fetch response");
// There may still be a difference between fetched and active config
FIRRemoteConfigUpdate *update =
[strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace];
FIRRemoteConfigUpdate *update = [strongSelf->_content
getConfigUpdateForNamespace:strongSelf->_FIRNamespace
withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]];
return [strongSelf reportCompletionWithStatus:FIRRemoteConfigFetchStatusSuccess
withUpdate:update
withError:nil
Expand Down Expand Up @@ -615,8 +617,9 @@ - (void)fetchWithUserProperties:(NSDictionary *)userProperties
strongSelf->_settings.lastETag = latestETag;
}
// Compute config update after successful fetch
FIRRemoteConfigUpdate *update =
[strongSelf->_content getConfigUpdateForNamespace:strongSelf->_FIRNamespace];
FIRRemoteConfigUpdate *update = [strongSelf->_content
getConfigUpdateForNamespace:strongSelf->_FIRNamespace
withExperimentChanges:[self->_experiment getKeysAffectedByChangedExperiments]];

[strongSelf->_settings
updateMetadataWithFetchSuccessStatus:YES
Expand Down
50 changes: 44 additions & 6 deletions FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,39 @@ - (void)testConfigUpdate_noChange_emptyResponse {
toSource:RCNDBSourceActive
forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 0);
}

- (void)testConfigUpdate_noParamChange_butExperimentChange {
NSString *namespace = @"test_namespace";

// populate fetched config
NSMutableDictionary *fetchResponse =
[self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil];
[_configContent 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}};
[_configContent copyFromDictionary:namespaceToConfig
toSource:RCNDBSourceActive
forNamespace:namespace];

NSMutableSet *experimentKeys = [[NSMutableSet alloc] init];
[experimentKeys addObject:@"key_2"];
FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:experimentKeys];

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

- (void)testConfigUpdate_paramAdded_returnsNewKey {
NSString *namespace = @"test_namespace";
NSString *newParam = @"key2";
Expand All @@ -368,7 +396,9 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey {
p13nMetadata:nil];
[_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:newParam]);
Expand All @@ -394,7 +424,9 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey {
[self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil];
[_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
Expand All @@ -420,7 +452,9 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey {
[self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil];
[_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 2);
XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); // deleted
Expand Down Expand Up @@ -455,7 +489,9 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey {
forKey:RCNFetchResponseKeyPersonalizationMetadata];
[_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
Expand All @@ -480,7 +516,9 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey {
[self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil];
[_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace];

FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace];
FIRRemoteConfigUpdate *update =
[_configContent getConfigUpdateForNamespace:namespace
withExperimentChanges:[[NSMutableSet alloc] init]];

XCTAssertTrue([update updatedKeys].count == 1);
XCTAssertTrue([[update updatedKeys] containsObject:existingParam]);
Expand Down
114 changes: 114 additions & 0 deletions FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ @interface RCNConfigExperiment ()
- (NSTimeInterval)updateExperimentStartTime;
- (void)loadExperimentFromTable;
- (void)updateActiveExperimentsInDB;
- (NSMutableSet<NSString *> *_Nonnull)getKeysAffectedByChangedExperiments;
@end

@interface RCNConfigExperimentTest : XCTestCase {
Expand Down Expand Up @@ -239,6 +240,119 @@ - (void)testUpdateExperiments {
}];
}

- (void)testExperimentDiff_addedExperiment {
FIRExperimentController *experimentController =
[[FIRExperimentController alloc] initWithAnalytics:nil];
id mockExperimentController = OCMPartialMock(experimentController);
RCNConfigExperiment *experiment =
[[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock
experimentController:mockExperimentController];

NSData *payloadData1 = [[self class] payloadDataFromTestFile];
experiment.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];
experiment.experimentPayloads = [@[ payloadData1, payloadData2 ] mutableCopy];

NSMutableSet<NSString *> *changedKeys = [experiment getKeysAffectedByChangedExperiments];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentMetadata {
FIRExperimentController *experimentController =
[[FIRExperimentController alloc] initWithAnalytics:nil];
id mockExperimentController = OCMPartialMock(experimentController);
RCNConfigExperiment *experiment =
[[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock
experimentController:mockExperimentController];

NSData *payloadData1 = [[self class] payloadDataFromTestFile];
experiment.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];
experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy];

NSMutableSet<NSString *> *changedKeys = [experiment getKeysAffectedByChangedExperiments];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_changedExperimentKeys {
FIRExperimentController *experimentController =
[[FIRExperimentController alloc] initWithAnalytics:nil];
id mockExperimentController = OCMPartialMock(experimentController);
RCNConfigExperiment *experiment =
[[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock
experimentController:mockExperimentController];

NSData *payloadData1 = [[self class] payloadDataFromTestFile];
experiment.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];
experiment.experimentPayloads = [@[ payloadData2 ] mutableCopy];

NSMutableSet<NSString *> *changedKeys = [experiment getKeysAffectedByChangedExperiments];
XCTAssertTrue([changedKeys containsObject:@"test_key_2"]);
}

- (void)testExperimentDiff_deletedExperiment {
FIRExperimentController *experimentController =
[[FIRExperimentController alloc] initWithAnalytics:nil];
id mockExperimentController = OCMPartialMock(experimentController);
RCNConfigExperiment *experiment =
[[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock
experimentController:mockExperimentController];

NSData *payloadData1 = [[self class] payloadDataFromTestFile];
experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
experiment.experimentPayloads = [@[] mutableCopy];

NSMutableSet<NSString *> *changedKeys = [experiment getKeysAffectedByChangedExperiments];
XCTAssertTrue([changedKeys containsObject:@"test_key_1"]);
}

- (void)testExperimentDiff_noChange {
FIRExperimentController *experimentController =
[[FIRExperimentController alloc] initWithAnalytics:nil];
id mockExperimentController = OCMPartialMock(experimentController);
RCNConfigExperiment *experiment =
[[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock
experimentController:mockExperimentController];

NSData *payloadData1 = [[self class] payloadDataFromTestFile];
experiment.activeExperimentPayloads = [@[ payloadData1 ] mutableCopy];
experiment.experimentPayloads = [@[ payloadData1 ] mutableCopy];

NSMutableSet<NSString *> *changedKeys = [experiment getKeysAffectedByChangedExperiments];
XCTAssertTrue([changedKeys count] == 0);
}

#pragma mark Helpers.

- (ABTExperimentPayload *)deserializeABTData:(NSData *)payload {
Expand Down
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"]
}
Loading