From 4a274039d3735d6910b20f61bb7910f3eb68e0b9 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Wed, 7 Oct 2020 16:58:46 -0700 Subject: [PATCH 1/8] Pull Personalization changes into Remote Config. --- .../Sources/FIRRemoteConfig.m | 31 +++++ .../FirebaseRemoteConfig/FIRRemoteConfig.h | 9 ++ .../Sources/RCNConfigConstants.h | 2 + .../Sources/RCNConfigContent.h | 5 + .../Sources/RCNConfigContent.m | 39 +++++- .../Sources/RCNConfigDBManager.h | 7 + .../Sources/RCNConfigDBManager.m | 122 +++++++++++++++++- .../Sources/RCNPersonalization.h | 44 +++++++ .../Sources/RCNPersonalization.m | 44 +++++++ 9 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 FirebaseRemoteConfig/Sources/RCNPersonalization.h create mode 100644 FirebaseRemoteConfig/Sources/RCNPersonalization.m diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index e1a8a20f531..b68cf7103cb 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -28,6 +28,7 @@ #import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Sources/RCNDevice.h" +#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" /// Remote Config Error Domain. /// TODO: Rename according to obj-c style for constants. @@ -68,6 +69,7 @@ @implementation FIRRemoteConfig { RCNConfigExperiment *_configExperiment; dispatch_queue_t _queue; NSString *_appName; + NSMutableArray *_listeners; } static NSMutableDictionary *> @@ -161,6 +163,14 @@ - (instancetype)initWithAppName:(NSString *)appName options:options]; [_settings loadConfigFromMetadataTable]; + + if (analytics) { + _listeners = [[NSMutableArray alloc] init]; + [RCNPersonalization setAnalytics:analytics]; + [self addListener:^(NSString *value, NSDictionary *metadata) { + [RCNPersonalization logArmActive:value metadata:metadata]; + }]; + } } return self; } @@ -192,6 +202,22 @@ - (void)ensureInitializedWithCompletionHandler: }); } +- (void)addListener:(nonnull FIRRemoteConfigListener)listener { + @synchronized(_listeners) { + [_listeners addObject:listener]; + } +} + +- (void)callListeners:(NSString *)value metadata:(NSDictionary *)metadata { + @synchronized(_listeners) { + for (FIRRemoteConfigListener listener in _listeners) { + dispatch_async(_queue, ^{ + listener(value, metadata); + }); + } + } +} + #pragma mark - fetch - (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { @@ -284,6 +310,7 @@ - (void)activateWithCompletion:(FIRRemoteConfigActivateChangeCompletion)completi [strongSelf->_configContent copyFromDictionary:self->_configContent.fetchedConfig toSource:RCNDBSourceActive forNamespace:self->_FIRNamespace]; + [strongSelf->_configContent activatePersonalization]; [strongSelf updateExperiments]; strongSelf->_settings.lastApplyTimeInterval = [[NSDate date] timeIntervalSince1970]; FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); @@ -331,6 +358,10 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { @"Key %@ should come from source:%zd instead coming from source: %zd.", key, (long)FIRRemoteConfigSourceRemote, (long)value.source); } + if ([self->_configContent.activePersonalization count] > 0) { + [self callListeners:value.stringValue + metadata:self->_configContent.activePersonalization[key]]; + } return; } value = self->_configContent.defaultConfig[FQNamespace][key]; diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index cbf692bce9d..a572b584a80 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -101,6 +101,10 @@ typedef void (^FIRRemoteConfigFetchAndActivateCompletion)( FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error) NS_SWIFT_NAME(RemoteConfigFetchAndActivateCompletion); +/// Listener for the get methods. +typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull) + NS_SWIFT_NAME(FIRRemoteConfigListener); + #pragma mark - FIRRemoteConfigValue /// This class provides a wrapper for Remote Config parameter values, with methods to get parameter /// values as different data types. @@ -178,6 +182,11 @@ NS_SWIFT_NAME(RemoteConfig) /// @param completionHandler Initialization complete callback with error parameter. - (void)ensureInitializedWithCompletionHandler: (void (^_Nonnull)(NSError *_Nullable initializationError))completionHandler; + +/// Adds a listener that will be called whenever one of the get methods is called. +/// @param listener Function that takes in the parameter key and the config. +- (void)addListener:(void (^_Nonnull)(NSString *_Nonnull, NSDictionary *_Nonnull))listener; + #pragma mark - Fetch /// Fetches Remote Config data with a callback. Call activateFetched to make fetched data available /// to your app. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h index 5a29aabba3f..cd062b59e74 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -35,6 +35,8 @@ static const char *RCNRemoteConfigQueueLabel = "com.google.GoogleConfigService.F static NSString *const RCNFetchResponseKeyEntries = @"entries"; /// Key that includes data for experiment descriptions in ABT. static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions"; +/// Key that includes data for Personalization metadata. +static NSString *const RCNFetchResponseKeyPersonalizationMetadata = @"personalizationMetadata"; /// Error key. static NSString *const RCNFetchResponseKeyError = @"error"; /// Error code. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index cc83c2ebb19..6c8b5e44540 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -37,6 +37,8 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { @property(nonatomic, readonly, copy) NSDictionary *activeConfig; /// Local default config that is provided by external users; @property(nonatomic, readonly, copy) NSDictionary *defaultConfig; +/// Active Personalization metadata. +@property(nonatomic, readonly, copy) NSDictionary *activePersonalization; - (instancetype)init NS_UNAVAILABLE; @@ -57,4 +59,7 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { toSource:(RCNDBSource)source forNamespace:(NSString *)FIRNamespace; +/// Sets the fetched Personalization metadata to active. +- (void)activatePersonalization; + @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 37dececcb1f..e935314c250 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -32,6 +32,11 @@ @implementation RCNConfigContent { NSMutableDictionary *_fetchedConfig; /// Default config provided by user. NSMutableDictionary *_defaultConfig; + /// Active Personalization metadata that is currently used. + NSDictionary *_activePersonalization; + /// Pending Personalization metadata that is latest data from server that might or might not be + /// applied. + NSDictionary *_fetchedPersonalization; /// DBManager RCNConfigDBManager *_DBManager; /// Current bundle identifier; @@ -72,6 +77,8 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _activeConfig = [[NSMutableDictionary alloc] init]; _fetchedConfig = [[NSMutableDictionary alloc] init]; _defaultConfig = [[NSMutableDictionary alloc] init]; + _activePersonalization = [[NSDictionary alloc] init]; + _fetchedPersonalization = [[NSDictionary alloc] init]; _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; if (!_bundleIdentifier) { FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", @@ -79,7 +86,7 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _bundleIdentifier = @""; } _DBManager = DBManager; - _configLoadFromDBSemaphore = dispatch_semaphore_create(0); + _configLoadFromDBSemaphore = dispatch_semaphore_create(1); [self loadConfigFromMainTable]; } return self; @@ -204,10 +211,17 @@ - (void)updateConfigContentWithResponse:(NSDictionary *)response if ([state isEqualToString:RCNFetchResponseKeyStateUpdate]) { [self handleUpdateStateForConfigNamespace:currentNamespace withEntries:response[RCNFetchResponseKeyEntries]]; + [self handleUpdatePersonalization:response[RCNFetchResponseKeyPersonalizationMetadata]]; return; } } +- (void)activatePersonalization { + _activePersonalization = _fetchedPersonalization; + [_DBManager insertOrUpdatePersonalizationConfig:_activePersonalization + fromSource:RCNDBSourceActive]; +} + #pragma mark State handling - (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { if (!_fetchedConfig[currentNamespace]) { @@ -263,6 +277,14 @@ - (void)handleUpdateStateForConfigNamespace:(NSString *)currentNamespace } } +- (void)handleUpdatePersonalization:(NSDictionary *)metadata { + if (!metadata) { + return; + } + _fetchedPersonalization = metadata; + [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; +} + #pragma mark - database /// This method is only meant to be called at init time. The underlying logic will need to be @@ -284,6 +306,14 @@ - (void)loadConfigFromMainTable { self->_defaultConfig = [defaultConfig mutableCopy]; dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); }]; + + [_DBManager loadPersonalizationWithCompletionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, + NSDictionary *defaultConfig) { + self->_fetchedPersonalization = [fetchedConfig copy]; + self->_activePersonalization = [activeConfig copy]; + dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); + }]; } /// Update the current config result to main table. @@ -314,6 +344,13 @@ - (NSDictionary *)defaultConfig { return _defaultConfig; } +- (NSDictionary *)activePersonalization { + /// If this is the first time reading the activePersonalization, we might still be reading it from + /// the database. + [self checkAndWaitForInitialDatabaseLoad]; + return _activePersonalization; +} + /// We load the database async at init time. Block all further calls to active/fetched/default /// configs until load is done. /// @return Database load completion status. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 540a2b781e1..5f8c6ee6cf1 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -73,6 +73,9 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, /// Load experiment from experiment table. /// @param handler The callback when reading from DB is complete. - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler; +/// Load Personalization from table. +/// @param handler The callback when reading from DB is complete. +- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler; /// Insert a record in metadata table. /// @param columnNameToValue The column name and its value to be inserted in metadata table. @@ -100,6 +103,10 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, - (void)updateMetadataWithOption:(RCNUpdateOption)option values:(NSArray *)values completionHandler:(RCNDBCompletion)handler; + +/// Insert or update the data in Personalizatoin config. +- (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source; + /// Clear the record of given namespace and package name /// before updating the table. - (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index b3c704de723..e313b98b2c4 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -29,6 +29,7 @@ #define RCNTableNameMetadata "fetch_metadata" #define RCNTableNameInternalMetadata "internal_metadata" #define RCNTableNameExperiment "experiment" +#define RCNTableNamePersonalization "personalization" static BOOL gIsNewDatabase; /// SQLite file name in versions 0, 1 and 2. @@ -274,11 +275,15 @@ - (BOOL)createTableSchema { static const char *createTableExperiment = "create TABLE IF NOT EXISTS " RCNTableNameExperiment " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + static const char *createTablePersonalization = + "create TABLE IF NOT EXISTS " RCNTableNamePersonalization + " (_id INTEGER PRIMARY KEY, key INTEGER, value BLOB)"; return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && [self executeQuery:createTableInternalMetadata] && - [self executeQuery:createTableExperiment]; + [self executeQuery:createTableExperiment] && + [self executeQuery:createTablePersonalization]; } - (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { @@ -565,6 +570,48 @@ - (BOOL)updateExperimentMetadata:(NSData *)dataValue { return YES; } +- (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)dataValue + fromSource:(RCNDBSource)source { + RCN_MUST_NOT_BE_MAIN_THREAD(); + + NSError *error; + NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:dataValue + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (!JSONPayload || error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000075", + @"Invalid Personalization payload to be serialized."); + } + + const char *SQL = "INSERT OR REPLACE INTO " RCNTableNamePersonalization + " (_id, key, value) values ((SELECT _id from " RCNTableNamePersonalization + " WHERE key = ?), ?, ?)"; + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + if (sqlite3_bind_int(statement, 1, (int)source) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_bind_int(statement, 2, (int)source) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + if (sqlite3_bind_blob(statement, 3, JSONPayload.bytes, (int)JSONPayload.length, NULL) != + SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + #pragma mark - update - (void)updateMetadataWithOption:(RCNUpdateOption)option @@ -798,6 +845,79 @@ - (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { return results; } +- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + NSDictionary *activePersonalization; + NSData *personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceActive]; + // There should be only one entry for Personalization metadata. + if (personalizationResult) { + NSError *error; + activePersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult + options:0 + error:&error]; + } + if (!activePersonalization) { + activePersonalization = [[NSMutableDictionary alloc] init]; + } + + NSDictionary *fetchedPersonalization; + personalizationResult = [strongSelf loadPersonalizationTableFromKey:RCNDBSourceFetched]; + // There should be only one entry for Personalization metadata. + if (personalizationResult) { + NSError *error; + fetchedPersonalization = [NSJSONSerialization JSONObjectWithData:personalizationResult + options:0 + error:&error]; + } + if (!fetchedPersonalization) { + fetchedPersonalization = [[NSMutableDictionary alloc] init]; + } + + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(YES, activePersonalization, fetchedPersonalization, nil); + }); + } + }); +} + +- (NSData *)loadPersonalizationTableFromKey:(int)key { + RCN_MUST_NOT_BE_MAIN_THREAD(); + + NSMutableArray *results = [[NSMutableArray alloc] init]; + const char *SQL = "SELECT value FROM " RCNTableNamePersonalization " WHERE key = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + + if (sqlite3_bind_int(statement, 1, key) != SQLITE_OK) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return nil; + } + NSData *personalizationData; + while (sqlite3_step(statement) == SQLITE_ROW) { + personalizationData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) + length:sqlite3_column_bytes(statement, 0)]; + if (personalizationData) { + [results addObject:personalizationData]; + } + } + + sqlite3_finalize(statement); + // There should be only one entry in this table. + if (results.count != 1) { + return nil; + } + return results[0]; +} + - (NSDictionary *)loadInternalMetadataTable { __block NSMutableDictionary *internalMetadataTableResult; __weak RCNConfigDBManager *weakSelf = self; diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.h b/FirebaseRemoteConfig/Sources/RCNPersonalization.h new file mode 100644 index 00000000000..8be0ad2893f --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "Interop/Analytics/Public/FIRAnalyticsInterop.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kAnalyticsOriginPersonalization = @"fp"; +static NSString *const kAnalyticsPullEvent = @"_fpc"; +static NSString *const kArmKey = @"_fpid"; +static NSString *const kArmValue = @"_fpct"; +static NSString *const kPersonalizationId = @"personalizationId"; + +@interface RCNPersonalization : NSObject + +/// Analytics connector +@property(nonatomic, strong) id _Nullable analytics; + +/// Returns the RCNPersonalization singleton. ++ (instancetype)sharedInstance; + +/// Sets analytics for underlying implementation of RCNPersonalization, if defined. ++ (void)setAnalytics:(id _Nullable)analytics; + +/// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to +/// Google in another thread. ++ (void)logArmActive:(NSString *)value metadata:(NSDictionary *)metadata; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.m b/FirebaseRemoteConfig/Sources/RCNPersonalization.m new file mode 100644 index 00000000000..a5e0ffa28ea --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.m @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" + +@implementation RCNPersonalization + ++ (instancetype)sharedInstance { + static RCNPersonalization *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[RCNPersonalization alloc] init]; + }); + return sharedInstance; +} + ++ (void)setAnalytics:(id _Nullable)analytics { + RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; + personalization->_analytics = analytics; +} + ++ (void)logArmActive:(NSString *)value metadata:(NSDictionary *)metadata { + RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; + [personalization->_analytics + logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:@{kArmKey : metadata[kPersonalizationId], kArmValue : value}]; +} + +@end From 550212dfe7fc6dfa4df070d74122b333f8c41ce3 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Wed, 7 Oct 2020 23:26:44 -0700 Subject: [PATCH 2/8] Adding unit tests. --- FirebaseRemoteConfig.podspec | 1 + .../Sources/RCNPersonalization.m | 4 + .../Tests/Unit/RCNPersonalizationTest.m | 97 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 9b1c9582c9d..c648a91347f 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -54,6 +54,7 @@ app update. # 'FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m', 'FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m', 'FirebaseRemoteConfig/Tests/Unit/RCNConfigValueTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m', # 'FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfig+FIRAppTest.m', 'FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m', # 'FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m', diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.m b/FirebaseRemoteConfig/Sources/RCNPersonalization.m index a5e0ffa28ea..3cd71417079 100644 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.m +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.m @@ -34,6 +34,10 @@ + (void)setAnalytics:(id _Nullable)analytics { } + (void)logArmActive:(NSString *)value metadata:(NSDictionary *)metadata { + if (metadata[kPersonalizationId] == nil) { + return; + } + RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; [personalization->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m new file mode 100644 index 00000000000..d5c33c58e07 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "OCMock.h" + +#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" +#import "Interop/Analytics/Public/FIRAnalyticsInterop.h" + +@interface RCNPersonalizationTest : XCTestCase { + NSMutableArray *_fakeLogs; + id _analyticsMock; +} +@end + +@implementation RCNPersonalizationTest +- (void)setUp { + [super setUp]; + + _fakeLogs = [[NSMutableArray alloc] init]; + _analyticsMock = OCMProtocolMock(@protocol(FIRAnalyticsInterop)); + OCMStub([_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:[OCMArg isKindOfClass:[NSDictionary class]]]) + .andDo(^(NSInvocation *invocation) { + __unsafe_unretained NSDictionary *bundle; + [invocation getArgument:&bundle atIndex:4]; + [self->_fakeLogs addObject:bundle]; + }); + + [RCNPersonalization setAnalytics:_analyticsMock]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testNonPersonalizationKey { + [_fakeLogs removeAllObjects]; + + [RCNPersonalization logArmActive:@"value3" metadata:[[NSDictionary alloc] init]]; + + OCMVerify(never(), + [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:[OCMArg isKindOfClass:[NSDictionary class]]]); + XCTAssertEqual([_fakeLogs count], 0); +} + +- (void)testSinglePersonalizationKey { + [_fakeLogs removeAllObjects]; + + [RCNPersonalization logArmActive:@"value1" metadata:@{kPersonalizationId : @"id1"}]; + + OCMVerify(times(1), + [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:[OCMArg isKindOfClass:[NSDictionary class]]]); + XCTAssertEqual([_fakeLogs count], 1); + + NSDictionary *params = @{kArmKey : @"id1", kArmValue : @"value1"}; + XCTAssertEqualObjects(_fakeLogs[0], params); +} + +- (void)testMultiplePersonalizationKeys { + [_fakeLogs removeAllObjects]; + + [RCNPersonalization logArmActive:@"value1" metadata:@{kPersonalizationId : @"id1"}]; + [RCNPersonalization logArmActive:@"value2" metadata:@{kPersonalizationId : @"id2"}]; + + OCMVerify(times(2), + [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:[OCMArg isKindOfClass:[NSDictionary class]]]); + XCTAssertEqual([_fakeLogs count], 2); + + NSDictionary *params1 = @{kArmKey : @"id1", kArmValue : @"value1"}; + XCTAssertEqualObjects(_fakeLogs[0], params1); + + NSDictionary *params2 = @{kArmKey : @"id2", kArmValue : @"value2"}; + XCTAssertEqualObjects(_fakeLogs[1], params2); +} + +@end From b8fac8490888df22404c13eaee3e58d975dd7345 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Thu, 8 Oct 2020 15:31:38 -0700 Subject: [PATCH 3/8] Pass the whole config to listeners, instead of just the Personalization metadata. --- .../Sources/FIRRemoteConfig.m | 19 +++++++------ .../FirebaseRemoteConfig/FIRRemoteConfig.h | 9 ------- .../Sources/RCNConfigContent.h | 5 ++-- .../Sources/RCNConfigContent.m | 21 ++++++++------- .../Sources/RCNConfigDBManager.m | 2 +- .../Sources/RCNPersonalization.h | 2 +- .../Sources/RCNPersonalization.m | 23 +++++++++++----- .../Tests/Unit/RCNPersonalizationTest.m | 27 ++++++++++++++++--- 8 files changed, 68 insertions(+), 40 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index b68cf7103cb..9714f79d68e 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -42,6 +42,10 @@ /// Timeout value for waiting on a fetch response. static NSString *const kRemoteConfigFetchTimeoutKey = @"_rcn_fetch_timeout"; +/// Listener for the get methods. +typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull) + NS_SWIFT_NAME(FIRRemoteConfigListener); + @interface FIRRemoteConfigSettings () { BOOL _developerModeEnabled; } @@ -167,8 +171,8 @@ - (instancetype)initWithAppName:(NSString *)appName if (analytics) { _listeners = [[NSMutableArray alloc] init]; [RCNPersonalization setAnalytics:analytics]; - [self addListener:^(NSString *value, NSDictionary *metadata) { - [RCNPersonalization logArmActive:value metadata:metadata]; + [self addListener:^(NSString *key, NSDictionary *config) { + [RCNPersonalization logArmActive:key config:config]; }]; } } @@ -202,17 +206,19 @@ - (void)ensureInitializedWithCompletionHandler: }); } +/// Adds a listener that will be called whenever one of the get methods is called. +/// @param listener Function that takes in the parameter key and the config. - (void)addListener:(nonnull FIRRemoteConfigListener)listener { @synchronized(_listeners) { [_listeners addObject:listener]; } } -- (void)callListeners:(NSString *)value metadata:(NSDictionary *)metadata { +- (void)callListeners:(NSString *)key config:(NSDictionary *)config { @synchronized(_listeners) { for (FIRRemoteConfigListener listener in _listeners) { dispatch_async(_queue, ^{ - listener(value, metadata); + listener(key, config); }); } } @@ -358,10 +364,7 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { @"Key %@ should come from source:%zd instead coming from source: %zd.", key, (long)FIRRemoteConfigSourceRemote, (long)value.source); } - if ([self->_configContent.activePersonalization count] > 0) { - [self callListeners:value.stringValue - metadata:self->_configContent.activePersonalization[key]]; - } + [self callListeners:key config:[self->_configContent getMetadataForNamespace:FQNamespace]]; return; } value = self->_configContent.defaultConfig[FQNamespace][key]; diff --git a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h index a572b584a80..cbf692bce9d 100644 --- a/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h @@ -101,10 +101,6 @@ typedef void (^FIRRemoteConfigFetchAndActivateCompletion)( FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error) NS_SWIFT_NAME(RemoteConfigFetchAndActivateCompletion); -/// Listener for the get methods. -typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull) - NS_SWIFT_NAME(FIRRemoteConfigListener); - #pragma mark - FIRRemoteConfigValue /// This class provides a wrapper for Remote Config parameter values, with methods to get parameter /// values as different data types. @@ -182,11 +178,6 @@ NS_SWIFT_NAME(RemoteConfig) /// @param completionHandler Initialization complete callback with error parameter. - (void)ensureInitializedWithCompletionHandler: (void (^_Nonnull)(NSError *_Nullable initializationError))completionHandler; - -/// Adds a listener that will be called whenever one of the get methods is called. -/// @param listener Function that takes in the parameter key and the config. -- (void)addListener:(void (^_Nonnull)(NSString *_Nonnull, NSDictionary *_Nonnull))listener; - #pragma mark - Fetch /// Fetches Remote Config data with a callback. Call activateFetched to make fetched data available /// to your app. diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 6c8b5e44540..1cc2c809cdc 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -37,8 +37,6 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { @property(nonatomic, readonly, copy) NSDictionary *activeConfig; /// Local default config that is provided by external users; @property(nonatomic, readonly, copy) NSDictionary *defaultConfig; -/// Active Personalization metadata. -@property(nonatomic, readonly, copy) NSDictionary *activePersonalization; - (instancetype)init NS_UNAVAILABLE; @@ -62,4 +60,7 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { /// Sets the fetched Personalization metadata to active. - (void)activatePersonalization; +/// Gets the active config and Personalization metadata. +- (NSDictionary *)getMetadataForNamespace:(NSString *)FIRNamespace; + @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index e935314c250..c8ad7c77a23 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -307,11 +307,11 @@ - (void)loadConfigFromMainTable { dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); }]; - [_DBManager loadPersonalizationWithCompletionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, - NSDictionary *defaultConfig) { - self->_fetchedPersonalization = [fetchedConfig copy]; - self->_activePersonalization = [activeConfig copy]; + [_DBManager loadPersonalizationWithCompletionHandler:^( + BOOL success, NSDictionary *fetchedPersonalization, + NSDictionary *activePersonalization, NSDictionary *defaultConfig) { + self->_fetchedPersonalization = [fetchedPersonalization copy]; + self->_activePersonalization = [activePersonalization copy]; dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); }]; } @@ -344,11 +344,14 @@ - (NSDictionary *)defaultConfig { return _defaultConfig; } -- (NSDictionary *)activePersonalization { - /// If this is the first time reading the activePersonalization, we might still be reading it from - /// the database. +- (NSDictionary *)getMetadataForNamespace:(NSString *)FIRNamespace { + /// If this is the first time reading the active metadata, we might still be reading it from the + /// database. [self checkAndWaitForInitialDatabaseLoad]; - return _activePersonalization; + return @{ + RCNFetchResponseKeyEntries : _activeConfig[FIRNamespace], + RCNFetchResponseKeyPersonalizationMetadata : _activePersonalization + }; } /// We load the database async at init time. Block all further calls to active/fetched/default diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m index e313b98b2c4..f91ead9c93f 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -881,7 +881,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler { if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ - handler(YES, activePersonalization, fetchedPersonalization, nil); + handler(YES, fetchedPersonalization, activePersonalization, nil); }); } }); diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.h b/FirebaseRemoteConfig/Sources/RCNPersonalization.h index 8be0ad2893f..6a833fa624d 100644 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.h +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.h @@ -37,7 +37,7 @@ static NSString *const kPersonalizationId = @"personalizationId"; /// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to /// Google in another thread. -+ (void)logArmActive:(NSString *)value metadata:(NSDictionary *)metadata; ++ (void)logArmActive:(NSString *)key config:(NSDictionary *)config; @end diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.m b/FirebaseRemoteConfig/Sources/RCNPersonalization.m index 3cd71417079..5960c9df2f8 100644 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.m +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.m @@ -15,7 +15,9 @@ */ #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" + #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" @implementation RCNPersonalization @@ -33,16 +35,25 @@ + (void)setAnalytics:(id _Nullable)analytics { personalization->_analytics = analytics; } -+ (void)logArmActive:(NSString *)value metadata:(NSDictionary *)metadata { - if (metadata[kPersonalizationId] == nil) { ++ (void)logArmActive:(NSString *)key config:(NSDictionary *)config { + NSDictionary *ids = config[RCNFetchResponseKeyPersonalizationMetadata]; + NSDictionary *values = config[RCNFetchResponseKeyEntries]; + if (ids.count < 1 || values.count < 1 || !values[key]) { + return; + } + + NSDictionary *metadata = ids[key]; + if (!metadata || metadata[kPersonalizationId] == nil) { return; } RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; - [personalization->_analytics - logEventWithOrigin:kAnalyticsOriginPersonalization - name:kAnalyticsPullEvent - parameters:@{kArmKey : metadata[kPersonalizationId], kArmValue : value}]; + [personalization->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:@{ + kArmKey : metadata[kPersonalizationId], + kArmValue : values[key].stringValue + }]; } @end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m index d5c33c58e07..52f37742ce4 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -17,10 +17,13 @@ #import #import "OCMock.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" @interface RCNPersonalizationTest : XCTestCase { + NSDictionary *_configContainer; NSMutableArray *_fakeLogs; id _analyticsMock; } @@ -30,6 +33,22 @@ @implementation RCNPersonalizationTest - (void)setUp { [super setUp]; + _configContainer = @{ + RCNFetchResponseKeyEntries : @{ + @"key1" : [[FIRRemoteConfigValue alloc] + initWithData:[@"value1" dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote], + @"key2" : [[FIRRemoteConfigValue alloc] + initWithData:[@"value2" dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote], + @"key3" : [[FIRRemoteConfigValue alloc] + initWithData:[@"value3" dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote] + }, + RCNFetchResponseKeyPersonalizationMetadata : + @{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}} + }; + _fakeLogs = [[NSMutableArray alloc] init]; _analyticsMock = OCMProtocolMock(@protocol(FIRAnalyticsInterop)); OCMStub([_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -51,7 +70,7 @@ - (void)tearDown { - (void)testNonPersonalizationKey { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"value3" metadata:[[NSDictionary alloc] init]]; + [RCNPersonalization logArmActive:@"key3" config:_configContainer]; OCMVerify(never(), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -63,7 +82,7 @@ - (void)testNonPersonalizationKey { - (void)testSinglePersonalizationKey { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"value1" metadata:@{kPersonalizationId : @"id1"}]; + [RCNPersonalization logArmActive:@"key1" config:_configContainer]; OCMVerify(times(1), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -78,8 +97,8 @@ - (void)testSinglePersonalizationKey { - (void)testMultiplePersonalizationKeys { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"value1" metadata:@{kPersonalizationId : @"id1"}]; - [RCNPersonalization logArmActive:@"value2" metadata:@{kPersonalizationId : @"id2"}]; + [RCNPersonalization logArmActive:@"key1" config:_configContainer]; + [RCNPersonalization logArmActive:@"key2" config:_configContainer]; OCMVerify(times(2), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization From 9a5c7f990ab7db66b87de63970d9d5efa18cbdbc Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Mon, 12 Oct 2020 14:39:16 -0700 Subject: [PATCH 4/8] Fixing some errors. --- FirebaseRemoteConfig/Sources/FIRRemoteConfig.m | 3 +-- FirebaseRemoteConfig/Sources/RCNConfigDBManager.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index d0960a02605..3da9722201d 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -43,8 +43,7 @@ static NSString *const kRemoteConfigFetchTimeoutKey = @"_rcn_fetch_timeout"; /// Listener for the get methods. -typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull) - NS_SWIFT_NAME(FIRRemoteConfigListener); +typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); @interface FIRRemoteConfigSettings () { BOOL _developerModeEnabled; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h index 5f8c6ee6cf1..556ffb9c9fa 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -104,7 +104,7 @@ typedef void (^RCNDBLoadCompletion)(BOOL success, values:(NSArray *)values completionHandler:(RCNDBCompletion)handler; -/// Insert or update the data in Personalizatoin config. +/// Insert or update the data in Personalization config. - (BOOL)insertOrUpdatePersonalizationConfig:(NSDictionary *)metadata fromSource:(RCNDBSource)source; /// Clear the record of given namespace and package name From 7bcef17167a64a2d64b3ed6e8b8e0877a7bcb882 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Mon, 12 Oct 2020 16:04:16 -0700 Subject: [PATCH 5/8] Update CHANGELOG. --- FirebaseRemoteConfig/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index 3fa0730647a..01c0b926753 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -7,7 +7,7 @@ - [fixed] Fixed database creation on tvOS. (#6612) - [changed] Updated public API documentation to no longer reference removed APIs. (#6641) - [fixed] Updated `activateWithCompletion:` to use completion handler for experiment updates. (#3687) -- [changed] Log metadata for Remote Config parameter get calls. (#6692) +- [changed] Added preliminary infrastructure to support future features. (#6692) # v4.9.1 - [fixed] Fix an `attempt to insert nil object` crash in `fetchWithExpirationDuration:`. (#6522) From c4f83b2de620b541fb46a6e114157569c60f8e13 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Mon, 19 Oct 2020 08:52:48 -0700 Subject: [PATCH 6/8] Make RCNPersonalization non-singleton and add unit test for FIRRemoteConfig integration. --- .../Sources/FIRRemoteConfig.m | 13 +-- .../Sources/RCNConfigContent.h | 2 +- .../Sources/RCNConfigContent.m | 2 +- .../Sources/RCNPersonalization.h | 10 +- .../Sources/RCNPersonalization.m | 33 +++---- .../Tests/Unit/RCNPersonalizationTest.m | 93 ++++++++++++++++++- 6 files changed, 113 insertions(+), 40 deletions(-) diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 3da9722201d..e6930899c0c 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -45,11 +45,6 @@ /// Listener for the get methods. typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull); -@interface FIRRemoteConfigSettings () { - BOOL _developerModeEnabled; -} -@end - @implementation FIRRemoteConfigSettings - (instancetype)init { @@ -169,9 +164,10 @@ - (instancetype)initWithAppName:(NSString *)appName if (analytics) { _listeners = [[NSMutableArray alloc] init]; - [RCNPersonalization setAnalytics:analytics]; + RCNPersonalization *personalization = + [[RCNPersonalization alloc] initWithAnalytics:analytics]; [self addListener:^(NSString *key, NSDictionary *config) { - [RCNPersonalization logArmActive:key config:config]; + [personalization logArmActive:key config:config]; }]; } } @@ -360,7 +356,8 @@ - (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { @"Key %@ should come from source:%zd instead coming from source: %zd.", key, (long)FIRRemoteConfigSourceRemote, (long)value.source); } - [self callListeners:key config:[self->_configContent getMetadataForNamespace:FQNamespace]]; + [self callListeners:key + config:[self->_configContent getConfigAndMetadataForNamespace:FQNamespace]]; return; } value = self->_configContent.defaultConfig[FQNamespace][key]; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h index 1cc2c809cdc..d212101973a 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.h +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -61,6 +61,6 @@ typedef NS_ENUM(NSInteger, RCNDBSource) { - (void)activatePersonalization; /// Gets the active config and Personalization metadata. -- (NSDictionary *)getMetadataForNamespace:(NSString *)FIRNamespace; +- (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace; @end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index c8ad7c77a23..67877fe7b77 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -344,7 +344,7 @@ - (NSDictionary *)defaultConfig { return _defaultConfig; } -- (NSDictionary *)getMetadataForNamespace:(NSString *)FIRNamespace { +- (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace { /// If this is the first time reading the active metadata, we might still be reading it from the /// database. [self checkAndWaitForInitialDatabaseLoad]; diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.h b/FirebaseRemoteConfig/Sources/RCNPersonalization.h index 6a833fa624d..a9ecb1db7d9 100644 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.h +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.h @@ -29,15 +29,15 @@ static NSString *const kPersonalizationId = @"personalizationId"; /// Analytics connector @property(nonatomic, strong) id _Nullable analytics; -/// Returns the RCNPersonalization singleton. -+ (instancetype)sharedInstance; +- (instancetype)init NS_UNAVAILABLE; -/// Sets analytics for underlying implementation of RCNPersonalization, if defined. -+ (void)setAnalytics:(id _Nullable)analytics; +/// Designated initializer. +- (instancetype)initWithAnalytics:(id _Nullable)analytics + NS_DESIGNATED_INITIALIZER; /// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to /// Google in another thread. -+ (void)logArmActive:(NSString *)key config:(NSDictionary *)config; +- (void)logArmActive:(NSString *)key config:(NSDictionary *)config; @end diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.m b/FirebaseRemoteConfig/Sources/RCNPersonalization.m index 5960c9df2f8..41d2cd4cd51 100644 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.m +++ b/FirebaseRemoteConfig/Sources/RCNPersonalization.m @@ -21,21 +21,15 @@ @implementation RCNPersonalization -+ (instancetype)sharedInstance { - static RCNPersonalization *sharedInstance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[RCNPersonalization alloc] init]; - }); - return sharedInstance; -} - -+ (void)setAnalytics:(id _Nullable)analytics { - RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; - personalization->_analytics = analytics; +- (instancetype)initWithAnalytics:(id _Nullable)analytics { + self = [super init]; + if (self) { + self->_analytics = analytics; + } + return self; } -+ (void)logArmActive:(NSString *)key config:(NSDictionary *)config { +- (void)logArmActive:(NSString *)key config:(NSDictionary *)config { NSDictionary *ids = config[RCNFetchResponseKeyPersonalizationMetadata]; NSDictionary *values = config[RCNFetchResponseKeyEntries]; if (ids.count < 1 || values.count < 1 || !values[key]) { @@ -47,13 +41,12 @@ + (void)logArmActive:(NSString *)key config:(NSDictionary *)config { return; } - RCNPersonalization *personalization = [RCNPersonalization sharedInstance]; - [personalization->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization - name:kAnalyticsPullEvent - parameters:@{ - kArmKey : metadata[kPersonalizationId], - kArmValue : values[key].stringValue - }]; + [self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:@{ + kArmKey : metadata[kPersonalizationId], + kArmValue : values[key].stringValue + }]; } @end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m index 52f37742ce4..2e87ccd13ff 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -17,15 +17,31 @@ #import #import "OCMock.h" +#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" +#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" #import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" +@interface RCNConfigFetch (ForTest) +- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content + completionHandler: + (RCNConfigFetcherCompletion)fetcherCompletion; + +- (void)fetchWithUserProperties:(NSDictionary *)userProperties + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler; +@end + @interface RCNPersonalizationTest : XCTestCase { NSDictionary *_configContainer; NSMutableArray *_fakeLogs; id _analyticsMock; + RCNPersonalization *_personalization; + FIRRemoteConfig *_configInstance; } @end @@ -60,7 +76,51 @@ - (void)setUp { [self->_fakeLogs addObject:bundle]; }); - [RCNPersonalization setAnalytics:_analyticsMock]; + _personalization = [[RCNPersonalization alloc] initWithAnalytics:_analyticsMock]; + + NSString *DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; + id DBMock = OCMClassMock([RCNConfigDBManager class]); + OCMStub([DBMock remoteConfigPathForDatabase]).andReturn(DBPath); + + id configFetch = OCMClassMock([RCNConfigFetch class]); + OCMStub([configFetch fetchConfigWithExpirationDuration:0 completionHandler:OCMOCK_ANY]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation *invocation) { + __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) = nil; + [invocation getArgument:&handler atIndex:3]; + [configFetch fetchWithUserProperties:[[NSDictionary alloc] init] completionHandler:handler]; + }); + + NSDictionary *response = @{ + RCNFetchResponseKeyState : RCNFetchResponseKeyStateUpdate, + RCNFetchResponseKeyEntries : @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"}, + RCNFetchResponseKeyPersonalizationMetadata : + @{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}} + }; + id completionBlock = [OCMArg + invokeBlockWithArgs:[NSJSONSerialization dataWithJSONObject:response options:0 error:nil], + [[NSHTTPURLResponse alloc] + initWithURL:[NSURL URLWithString:@"https://firebase.com"] + statusCode:200 + HTTPVersion:nil + headerFields:@{@"etag" : @"etag1"}], + [NSNull null], nil]; + + OCMExpect([configFetch URLSessionDataTaskWithContent:[OCMArg any] + completionHandler:completionBlock]) + .andReturn(nil); + + RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:DBMock]; + _configInstance = OCMPartialMock([[FIRRemoteConfig alloc] + initWithAppName:@"testApp" + FIROptions:[[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:test" + GCMSenderID:@"testSender"] + namespace:@"namespace" + DBManager:DBMock + configContent:configContent + analytics:_analyticsMock]); + [_configInstance setValue:configFetch forKey:@"_configFetch"]; } - (void)tearDown { @@ -70,7 +130,7 @@ - (void)tearDown { - (void)testNonPersonalizationKey { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"key3" config:_configContainer]; + [_personalization logArmActive:@"key3" config:_configContainer]; OCMVerify(never(), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -82,7 +142,7 @@ - (void)testNonPersonalizationKey { - (void)testSinglePersonalizationKey { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"key1" config:_configContainer]; + [_personalization logArmActive:@"key1" config:_configContainer]; OCMVerify(times(1), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -97,8 +157,8 @@ - (void)testSinglePersonalizationKey { - (void)testMultiplePersonalizationKeys { [_fakeLogs removeAllObjects]; - [RCNPersonalization logArmActive:@"key1" config:_configContainer]; - [RCNPersonalization logArmActive:@"key2" config:_configContainer]; + [_personalization logArmActive:@"key1" config:_configContainer]; + [_personalization logArmActive:@"key2" config:_configContainer]; OCMVerify(times(2), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -113,4 +173,27 @@ - (void)testMultiplePersonalizationKeys { XCTAssertEqualObjects(_fakeLogs[1], params2); } +- (void)testRemoteConfigIntegration { + [_fakeLogs removeAllObjects]; + + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = + ^void(FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + OCMVerify(times(2), [self->_analyticsMock + logEventWithOrigin:kAnalyticsOriginPersonalization + name:kAnalyticsPullEvent + parameters:[OCMArg isKindOfClass:[NSDictionary class]]]); + XCTAssertEqual([self->_fakeLogs count], 2); + + NSDictionary *params1 = @{kArmKey : @"id1", kArmValue : @"value1"}; + XCTAssertEqualObjects(self->_fakeLogs[0], params1); + + NSDictionary *params2 = @{kArmKey : @"id2", kArmValue : @"value2"}; + XCTAssertEqualObjects(self->_fakeLogs[1], params2); + }; + + [_configInstance fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; + [_configInstance configValueForKey:@"key1"]; + [_configInstance configValueForKey:@"key2"]; +} + @end From a6574e4ce0fc5e7ae5f5cfaf49a33d41c28119e1 Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Tue, 20 Oct 2020 14:29:39 -0700 Subject: [PATCH 7/8] Add some more comments. --- FirebaseRemoteConfig/CHANGELOG.md | 2 +- .../Sources/RCNConfigContent.m | 76 ++++++++++--------- .../Tests/Unit/RCNPersonalizationTest.m | 69 +++++++++-------- 3 files changed, 79 insertions(+), 68 deletions(-) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index 01c0b926753..c3ed52b56a4 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -7,7 +7,7 @@ - [fixed] Fixed database creation on tvOS. (#6612) - [changed] Updated public API documentation to no longer reference removed APIs. (#6641) - [fixed] Updated `activateWithCompletion:` to use completion handler for experiment updates. (#3687) -- [changed] Added preliminary infrastructure to support future features. (#6692) +- [changed] Add support for other Firebase products to integrate with Remote Config. (#6692) # v4.9.1 - [fixed] Fix an `attempt to insert nil object` crash in `fetchWithExpirationDuration:`. (#6522) diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m index 67877fe7b77..024cb1b66ab 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigContent.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -86,6 +86,7 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { _bundleIdentifier = @""; } _DBManager = DBManager; + // Waits for both config and Personalization data to load. _configLoadFromDBSemaphore = dispatch_semaphore_create(1); [self loadConfigFromMainTable]; } @@ -100,6 +101,44 @@ - (BOOL)initializationSuccessful { return isDatabaseLoadSuccessful; } +#pragma mark - database + +/// This method is only meant to be called at init time. The underlying logic will need to be +/// revaluated if the assumption changes at a later time. +- (void)loadConfigFromMainTable { + if (!_DBManager) { + return; + } + + NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated"); + _isDatabaseLoadAlreadyInitiated = true; + + [_DBManager + loadMainWithBundleIdentifier:_bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig) { + self->_fetchedConfig = [fetchedConfig mutableCopy]; + self->_activeConfig = [activeConfig mutableCopy]; + self->_defaultConfig = [defaultConfig mutableCopy]; + dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); + }]; + + [_DBManager loadPersonalizationWithCompletionHandler:^( + BOOL success, NSDictionary *fetchedPersonalization, + NSDictionary *activePersonalization, NSDictionary *defaultConfig) { + self->_fetchedPersonalization = [fetchedPersonalization copy]; + self->_activePersonalization = [activePersonalization copy]; + dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); + }]; +} + +/// Update the current config result to main table. +/// @param values Values in a row to write to the table. +/// @param source The source the config data is coming from. It determines which table to write to. +- (void)updateMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source { + [_DBManager insertMainTableWithValues:values fromSource:source completionHandler:nil]; +} + #pragma mark - update /// This function is for copying dictionary when user set up a default config or when user clicks /// activate. For now the DBSource can only be Active or Default. @@ -285,43 +324,6 @@ - (void)handleUpdatePersonalization:(NSDictionary *)metadata { [_DBManager insertOrUpdatePersonalizationConfig:metadata fromSource:RCNDBSourceFetched]; } -#pragma mark - database - -/// This method is only meant to be called at init time. The underlying logic will need to be -/// revaluated if the assumption changes at a later time. -- (void)loadConfigFromMainTable { - if (!_DBManager) { - return; - } - - NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated"); - _isDatabaseLoadAlreadyInitiated = true; - - [_DBManager - loadMainWithBundleIdentifier:_bundleIdentifier - completionHandler:^(BOOL success, NSDictionary *fetchedConfig, - NSDictionary *activeConfig, NSDictionary *defaultConfig) { - self->_fetchedConfig = [fetchedConfig mutableCopy]; - self->_activeConfig = [activeConfig mutableCopy]; - self->_defaultConfig = [defaultConfig mutableCopy]; - dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); - }]; - - [_DBManager loadPersonalizationWithCompletionHandler:^( - BOOL success, NSDictionary *fetchedPersonalization, - NSDictionary *activePersonalization, NSDictionary *defaultConfig) { - self->_fetchedPersonalization = [fetchedPersonalization copy]; - self->_activePersonalization = [activePersonalization copy]; - dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); - }]; -} - -/// Update the current config result to main table. -/// @param values Values in a row to write to the table. -/// @param source The source the config data is coming from. It determines which table to write to. -- (void)updateMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source { - [_DBManager insertMainTableWithValues:values fromSource:source 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/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m index 2e87ccd13ff..1044cc0cf0c 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -78,40 +78,14 @@ - (void)setUp { _personalization = [[RCNPersonalization alloc] initWithAnalytics:_analyticsMock]; + // Always remove the database at the start of testing. NSString *DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; id DBMock = OCMClassMock([RCNConfigDBManager class]); OCMStub([DBMock remoteConfigPathForDatabase]).andReturn(DBPath); - id configFetch = OCMClassMock([RCNConfigFetch class]); - OCMStub([configFetch fetchConfigWithExpirationDuration:0 completionHandler:OCMOCK_ANY]) - .ignoringNonObjectArgs() - .andDo(^(NSInvocation *invocation) { - __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, - NSError *_Nullable error) = nil; - [invocation getArgument:&handler atIndex:3]; - [configFetch fetchWithUserProperties:[[NSDictionary alloc] init] completionHandler:handler]; - }); - - NSDictionary *response = @{ - RCNFetchResponseKeyState : RCNFetchResponseKeyStateUpdate, - RCNFetchResponseKeyEntries : @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"}, - RCNFetchResponseKeyPersonalizationMetadata : - @{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}} - }; - id completionBlock = [OCMArg - invokeBlockWithArgs:[NSJSONSerialization dataWithJSONObject:response options:0 error:nil], - [[NSHTTPURLResponse alloc] - initWithURL:[NSURL URLWithString:@"https://firebase.com"] - statusCode:200 - HTTPVersion:nil - headerFields:@{@"etag" : @"etag1"}], - [NSNull null], nil]; - - OCMExpect([configFetch URLSessionDataTaskWithContent:[OCMArg any] - completionHandler:completionBlock]) - .andReturn(nil); - RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:DBMock]; + + // Create a mock FIRRemoteConfig instance. _configInstance = OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:@"testApp" FIROptions:[[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:test" @@ -120,7 +94,7 @@ __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, DBManager:DBMock configContent:configContent analytics:_analyticsMock]); - [_configInstance setValue:configFetch forKey:@"_configFetch"]; + [_configInstance setValue:[RCNPersonalizationTest mockFetchRequest] forKey:@"_configFetch"]; } - (void)tearDown { @@ -196,4 +170,39 @@ - (void)testRemoteConfigIntegration { [_configInstance configValueForKey:@"key2"]; } ++ (id)mockFetchRequest { + id configFetch = OCMClassMock([RCNConfigFetch class]); + OCMStub([configFetch fetchConfigWithExpirationDuration:0 completionHandler:OCMOCK_ANY]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation *invocation) { + __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) = nil; + [invocation getArgument:&handler atIndex:3]; + [configFetch fetchWithUserProperties:[[NSDictionary alloc] init] completionHandler:handler]; + }); + OCMExpect([configFetch + URLSessionDataTaskWithContent:[OCMArg any] + completionHandler:[RCNPersonalizationTest mockResponseHandler]]) + .andReturn(nil); + return configFetch; +} + ++ (id)mockResponseHandler { + NSDictionary *response = @{ + RCNFetchResponseKeyState : RCNFetchResponseKeyStateUpdate, + RCNFetchResponseKeyEntries : @{@"key1" : @"value1", @"key2" : @"value2", @"key3" : @"value3"}, + RCNFetchResponseKeyPersonalizationMetadata : + @{@"key1" : @{kPersonalizationId : @"id1"}, @"key2" : @{kPersonalizationId : @"id2"}} + }; + return [OCMArg invokeBlockWithArgs:[NSJSONSerialization dataWithJSONObject:response + options:0 + error:nil], + [[NSHTTPURLResponse alloc] + initWithURL:[NSURL URLWithString:@"https://firebase.com"] + statusCode:200 + HTTPVersion:nil + headerFields:@{@"etag" : @"etag1"}], + [NSNull null], nil]; +} + @end From 9724b710cad959852f3e66430bcc74f541c589fa Mon Sep 17 00:00:00 2001 From: Victor Lum Date: Wed, 21 Oct 2020 09:27:46 -0700 Subject: [PATCH 8/8] Update CHANGELOG. --- FirebaseRemoteConfig/CHANGELOG.md | 4 +++- FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index c3ed52b56a4..83fedb1cf60 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,6 @@ +# v7.1.0 +- [changed] Add support for other Firebase products to integrate with Remote Config. (#6692) + # v7.0.0 - [changed] Updated `lastFetchTime` field to readonly. (#6567) - [changed] Functionally neutral change to stop using a deprecated method in the AB Testing API. (#6543) @@ -7,7 +10,6 @@ - [fixed] Fixed database creation on tvOS. (#6612) - [changed] Updated public API documentation to no longer reference removed APIs. (#6641) - [fixed] Updated `activateWithCompletion:` to use completion handler for experiment updates. (#3687) -- [changed] Add support for other Firebase products to integrate with Remote Config. (#6692) # v4.9.1 - [fixed] Fix an `attempt to insert nil object` crash in `fetchWithExpirationDuration:`. (#6522) diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m index 1044cc0cf0c..9ea07ed759b 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -175,8 +175,7 @@ + (id)mockFetchRequest { OCMStub([configFetch fetchConfigWithExpirationDuration:0 completionHandler:OCMOCK_ANY]) .ignoringNonObjectArgs() .andDo(^(NSInvocation *invocation) { - __unsafe_unretained void (^handler)(FIRRemoteConfigFetchStatus status, - NSError *_Nullable error) = nil; + __unsafe_unretained FIRRemoteConfigFetchCompletion handler; [invocation getArgument:&handler atIndex:3]; [configFetch fetchWithUserProperties:[[NSDictionary alloc] init] completionHandler:handler]; });