diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 4035d558707..098bde00a9c 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -329,6 +329,12 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi // New config has been activated at this point FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); [strongSelf->_configContent activatePersonalization]; + // Update activeRolloutMetadata + [strongSelf->_configContent activateRolloutMetadata]; + // Update last active template version number in setting and userDefaults. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [strongSelf->_settings updateLastActiveTemplateVersion]; + }); // Update experiments only for 3p namespace NSString *namespace = [strongSelf->_FIRNamespace substringToIndex:[strongSelf->_FIRNamespace rangeOfString:@":"].location]; diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h index 987f3a98225..36fb8e7435f 100644 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -79,8 +79,10 @@ @property(nonatomic, readwrite, assign) NSString *lastETag; /// The timestamp of the last eTag update. @property(nonatomic, readwrite, assign) NSTimeInterval lastETagUpdateTime; -// Last fetched template version. -@property(nonatomic, readwrite, assign) NSString *lastTemplateVersion; +/// Last fetched template version. +@property(nonatomic, readwrite, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; #pragma mark Throttling properties @@ -134,6 +136,9 @@ /// indicates a server issue. - (void)updateRealtimeExponentialBackoffTime; +/// Update last active template version from last fetched template version. +- (void)updateLastActiveTemplateVersion; + /// Returns the difference between the Realtime backoff end time and the current time in a /// NSTimeInterval format. - (NSTimeInterval)getRealtimeBackoffInterval; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index e3cd99088a9..e6ff7dfe033 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -60,5 +60,7 @@ static NSString *const RCNFetchResponseKeyStateNoTemplate = @"NO_TEMPLATE"; static NSString *const RCNFetchResponseKeyStateNoChange = @"NO_CHANGE"; /// Template found, but evaluates to empty (e.g. all keys omitted). static NSString *const RCNFetchResponseKeyStateEmptyConfig = @"EMPTY_CONFIG"; -/// Template Version key +/// Fetched Template Version key static NSString *const RCNFetchResponseKeyTemplateVersion = @"templateVersion"; +/// Active Template Version key +static NSString *const RCNActiveKeyTemplateVersion = @"activeTemplateVersion"; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 34d0895243a..b14c2a8aceb 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -65,6 +65,9 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Gets the active config and Personalization metadata. - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; +/// Sets the fetched rollout metadata to active and return the active rollout metadata. +- (NSArray *)activateRolloutMetadata; + /// Returns the updated parameters between fetched and active config. - (FIRRemoteConfigUpdate *)getConfigUpdateForNamespace:(NSString *)FIRNamespace; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index f28ee662b34..0d4da6d7d30 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -280,6 +280,7 @@ - (void)updateConfigContentWithResponse:(NSDictionary *)response [self handleUpdateStateForConfigNamespace:currentNamespace withEntries:response[RCNFetchResponseKeyEntries]]; [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]]; + [self handleUpdateRolloutFetchedMetadata:response[RCNFetchResponseKeyRolloutMetadata]]; return; } } @@ -290,6 +291,14 @@ - (void)activatePersonalization { fromSource:RCNDBSourceActive]; } +- (NSArray *)activateRolloutMetadata { + _activeRolloutMetadata = _fetchedRolloutMetadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyActiveMetadata + value:_activeRolloutMetadata + completionHandler:nil]; + return _activeRolloutMetadata; +} + #pragma mark State handling - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { if (!_fetchedConfig[currentNamespace]) { @@ -353,6 +362,16 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; } +- (void)handleUpdateRolloutFetchedMetadata:(NSArray *)metadata { + if (!metadata) { + return; + } + _fetchedRolloutMetadata = metadata; + [_DBManager insertOrUpdateRolloutTableWithKey:@RCNRolloutTableKeyFetchedMetadata + value:metadata + completionHandler:nil]; +} + #pragma mark - getter/setter - (NSDictionary *)fetchedConfig { /// If this is the first time reading the fetchedConfig, we might still be reading it from the diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index c3a0f16ddd8..d535738f91d 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -105,7 +105,7 @@ - (instancetype)initWithContent:(RCNConfigContent *)content _content = content; _fetchSession = [self newFetchSession]; _options = options; - _templateVersionNumber = [self->_settings lastTemplateVersion]; + _templateVersionNumber = [self->_settings lastFetchedTemplateVersion]; } return self; } diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m index 0b3e3ad1164..5672351a7ee 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -110,7 +110,8 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager } _isFetchInProgress = NO; - _lastTemplateVersion = [_userDefaultsManager lastTemplateVersion]; + _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; + _lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion]; _realtimeExponentialBackoffRetryInterval = [_userDefaultsManager currentRealtimeThrottlingRetryIntervalSeconds]; _realtimeExponentialBackoffThrottleEndTime = [_userDefaultsManager realtimeThrottleEndTime]; @@ -292,7 +293,7 @@ - (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; // Note: We expect the googleAppID to always be available. _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); - [_userDefaultsManager setLastTemplateVersion:templateVersion]; + [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; } [self updateMetadataTable]; @@ -377,6 +378,11 @@ - (void)updateMetadataTable { [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil]; } +- (void)updateLastActiveTemplateVersion { + _lastActiveTemplateVersion = _lastFetchedTemplateVersion; + [_userDefaultsManager setLastActiveTemplateVersion:_lastActiveTemplateVersion]; +} + #pragma mark - fetch request /// Returns a fetch request with the latest device and config change. diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h index acbcd5842f4..b235f217d81 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -44,7 +44,9 @@ NS_ASSUME_NONNULL_BEGIN /// Realtime retry count. @property(nonatomic, assign) int realtimeRetryCount; /// Last fetched template version. -@property(nonatomic, assign) NSString *lastTemplateVersion; +@property(nonatomic, assign) NSString *lastFetchedTemplateVersion; +/// Last active template version. +@property(nonatomic, assign) NSString *lastActiveTemplateVersion; /// Designated initializer. - (instancetype)initWithAppName:(NSString *)appName diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m index 29ec2e87a06..880a2157fe1 100644 --- a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -111,7 +111,7 @@ - (void)setLastETag:(NSString *)lastETag { } } -- (NSString *)lastTemplateVersion { +- (NSString *)lastFetchedTemplateVersion { NSDictionary *userDefaults = [self instanceUserDefaults]; if ([userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]) { return [userDefaults objectForKey:RCNFetchResponseKeyTemplateVersion]; @@ -120,12 +120,27 @@ - (NSString *)lastTemplateVersion { return @"0"; } -- (void)setLastTemplateVersion:(NSString *)templateVersion { +- (void)setLastFetchedTemplateVersion:(NSString *)templateVersion { if (templateVersion) { [self setInstanceUserDefaultsValue:templateVersion forKey:RCNFetchResponseKeyTemplateVersion]; } } +- (NSString *)lastActiveTemplateVersion { + NSDictionary *userDefaults = [self instanceUserDefaults]; + if ([userDefaults objectForKey:RCNActiveKeyTemplateVersion]) { + return [userDefaults objectForKey:RCNActiveKeyTemplateVersion]; + } + + return @"0"; +} + +- (void)setLastActiveTemplateVersion:(NSString *)templateVersion { + if (templateVersion) { + [self setInstanceUserDefaultsValue:templateVersion forKey:RCNActiveKeyTemplateVersion]; + } +} + - (NSTimeInterval)lastETagUpdateTime { NSNumber *lastETagUpdateTime = [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime]; diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index 7c7290e7551..edf1dd2ae8b 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -332,7 +332,9 @@ - (void)testConfigUpdate_noChange_emptyResponse { // populate fetched config NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1"} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // active config is the same as fetched config @@ -365,7 +367,8 @@ - (void)testConfigUpdate_paramAdded_returnsNewKey { // fetch response has new param NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{@"key1" : @"value1", newParam : @"value2"} - p13nMetadata:nil]; + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -391,7 +394,9 @@ - (void)testConfigUpdate_paramValueChanged_returnsUpdatedKey { // fetch response contains updated value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : updatedValue} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -417,7 +422,9 @@ - (void)testConfigUpdate_paramDeleted_returnsDeletedKey { // fetch response does not contain existing param NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{newParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{newParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -437,7 +444,8 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { // popuate fetched config NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{existingParam : value1} - p13nMetadata:@{existingParam : oldMetadata}]; + p13nMetadata:@{existingParam : oldMetadata} + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; // populate active config with the same content @@ -461,6 +469,51 @@ - (void)testConfigUpdate_p13nMetadataUpdated_returnsKey { XCTAssertTrue([[update updatedKeys] containsObject:existingParam]); } +- (void)testConfigUpdate_rolloutMetadataUpdated_returnsKey { + NSString *namespace = @"test_namespace"; + NSString *key = @"key1"; + NSString *value1 = @"value1"; + NSString *value2 = @"value2"; + NSString *rolloutId1 = @"1"; + NSString *variantId1 = @"A"; + NSString *variantId2 = @"B"; + NSArray *rolloutMetadata = @[ + @{@"rollout_id" : rolloutId1, @"variant_id" : variantId1, @"affected_parameter_keys" : @[ key ]} + ]; + // variant_id changed + NSArray *updatedRolloutMetadata = @[ + @{@"rollout_id" : rolloutId1, @"variant_id" : variantId2, @"affected_parameter_keys" : @[ key ]} + ]; + + // Populate fetched config + NSMutableDictionary *fetchResponse = [self createFetchResponseWithConfigEntries:@{key : value1} + p13nMetadata:nil + rolloutMetadata:rolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; + // populate active config with the same content + NSArray *result = [_configContent activateRolloutMetadata]; + XCTAssertEqualObjects(rolloutMetadata, result); + FIRRemoteConfigValue *rcValue1 = + [[FIRRemoteConfigValue alloc] initWithData:[value1 dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + + NSDictionary *namespaceToConfig = @{namespace : @{key : rcValue1}}; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:namespace]; + // New fetch response has updated rollout metadata + NSMutableDictionary *fetchResponse2 = + [self createFetchResponseWithConfigEntries:@{key : value2} + p13nMetadata:nil + rolloutMetadata:updatedRolloutMetadata]; + [_configContent updateConfigContentWithResponse:fetchResponse2 forNamespace:namespace]; + + FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; + + XCTAssertTrue([update updatedKeys].count == 1); + XCTAssertTrue([[update updatedKeys] containsObject:key]); +} + - (void)testConfigUpdate_valueSourceChanged_returnsKey { NSString *namespace = @"test_namespace"; NSString *existingParam = @"key1"; @@ -477,7 +530,9 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { // fetch response contains same key->value NSMutableDictionary *fetchResponse = - [self createFetchResponseWithConfigEntries:@{existingParam : value1} p13nMetadata:nil]; + [self createFetchResponseWithConfigEntries:@{existingParam : value1} + p13nMetadata:nil + rolloutMetadata:nil]; [_configContent updateConfigContentWithResponse:fetchResponse forNamespace:namespace]; FIRRemoteConfigUpdate *update = [_configContent getConfigUpdateForNamespace:namespace]; @@ -489,14 +544,18 @@ - (void)testConfigUpdate_valueSourceChanged_returnsKey { #pragma mark - Test Helpers - (NSMutableDictionary *)createFetchResponseWithConfigEntries:(NSDictionary *)config - p13nMetadata:(NSDictionary *)metadata { + p13nMetadata:(NSDictionary *)p13nMetadata + rolloutMetadata:(NSArray *)rolloutMetadata { NSMutableDictionary *fetchResponse = [[NSMutableDictionary alloc] initWithObjectsAndKeys:RCNFetchResponseKeyStateUpdate, RCNFetchResponseKeyState, nil]; if (config) { [fetchResponse setValue:config forKey:RCNFetchResponseKeyEntries]; } - if (metadata) { - [fetchResponse setValue:metadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + if (p13nMetadata) { + [fetchResponse setValue:p13nMetadata forKey:RCNFetchResponseKeyPersonalizationMetadata]; + } + if (rolloutMetadata) { + [fetchResponse setValue:rolloutMetadata forKey:RCNFetchResponseKeyRolloutMetadata]; } return fetchResponse; } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m index 0c3135e2edd..5f915d73632 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -129,8 +129,17 @@ - (void)testUserDefaultsTemplateVersionWriteAndRead { [[RCNUserDefaultsManager alloc] initWithAppName:AppName bundleID:[NSBundle mainBundle].bundleIdentifier namespace:FQNamespace1]; - [manager setLastTemplateVersion:@"1"]; - XCTAssertEqual([manager lastTemplateVersion], @"1"); + [manager setLastFetchedTemplateVersion:@"1"]; + XCTAssertEqual([manager lastFetchedTemplateVersion], @"1"); +} + +- (void)testUserDefaultsActiveTemplateVersionWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:AppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:FQNamespace1]; + [manager setLastActiveTemplateVersion:@"1"]; + XCTAssertEqual([manager lastActiveTemplateVersion], @"1"); } - (void)testUserDefaultsRealtimeThrottleEndTimeWriteAndRead { @@ -229,10 +238,16 @@ - (void)testUserDefaultsForMultipleNamespaces { XCTAssertEqual([manager2 realtimeRetryCount], 2); /// Fetch template version. - [manager1 setLastTemplateVersion:@"1"]; - [manager2 setLastTemplateVersion:@"2"]; - XCTAssertEqualObjects([manager1 lastTemplateVersion], @"1"); - XCTAssertEqualObjects([manager2 lastTemplateVersion], @"2"); + [manager1 setLastFetchedTemplateVersion:@"1"]; + [manager2 setLastFetchedTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastFetchedTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastFetchedTemplateVersion], @"2"); + + /// Active template version. + [manager1 setLastActiveTemplateVersion:@"1"]; + [manager2 setLastActiveTemplateVersion:@"2"]; + XCTAssertEqualObjects([manager1 lastActiveTemplateVersion], @"1"); + XCTAssertEqualObjects([manager2 lastActiveTemplateVersion], @"2"); } - (void)testUserDefaultsReset {