From e7d2cd099d10b83b176642cca203b4ca12ff609e Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:07:03 -0500 Subject: [PATCH] [Config] Port 'RCNConfigSettings' (#14262) --- .../Sources/FIRRemoteConfig.m | 1 - .../Sources/Private/FIRRemoteConfig_Private.h | 1 - .../Sources/Private/RCNConfigSettings.h | 157 ----- FirebaseRemoteConfig/Sources/RCNConfigFetch.m | 1 - .../Sources/RCNConfigRealtime.m | 1 - .../Sources/RCNConfigSettings.m | 555 ------------------ .../SwiftNew/ConfigSettings.swift | 546 +++++++++++++++++ FirebaseRemoteConfig/SwiftNew/Device.swift | 4 +- .../SwiftNew/RemoteConfigComponent.swift | 22 +- .../Tests/Unit/RCNConfigContentTest.m | 1 - .../Tests/Unit/RCNConfigDBManagerTest.m | 5 +- .../Tests/Unit/RCNConfigExperimentTest.m | 1 - .../Tests/Unit/RCNTestUtilities.h | 2 +- 13 files changed, 569 insertions(+), 728 deletions(-) delete mode 100644 FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h delete mode 100644 FirebaseRemoteConfig/Sources/RCNConfigSettings.m create mode 100644 FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 29b3ebc7135..7950f0938fd 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -20,7 +20,6 @@ #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h" #import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h index 0dab0b4839d..4bc864fe302 100644 --- a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -15,7 +15,6 @@ */ #import -#import "RCNConfigSettings.h" // This import is needed to expose settings for the Swift API tests. @class FIROptions; @class RCNConfigContent; diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h deleted file mode 100644 index 108a04472a4..00000000000 --- a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 - -@class RCNConfigDBManager; - -/// This internal class contains a set of variables that are unique among all the config instances. -/// It also handles all metadata. This class is not thread safe and does not -/// inherently allow for synchronized access. Callers are responsible for synchronization -/// (currently using serial dispatch queues). -@interface RCNConfigSettings : NSObject - -/// The time interval that config data stays fresh. -@property(nonatomic, readwrite, assign) NSTimeInterval minimumFetchInterval; - -/// The timeout to set for outgoing fetch requests. -@property(nonatomic, readwrite, assign) NSTimeInterval fetchTimeout; -// The Google App ID of the configured FIRApp. -@property(nonatomic, readwrite, copy) NSString *googleAppID; -#pragma mark - Data required by config request. -/// Device authentication ID required by config request. -@property(nonatomic, copy) NSString *deviceAuthID; -/// Secret Token required by config request. -@property(nonatomic, copy) NSString *secretToken; -/// Device data version of checkin information. -@property(nonatomic, copy) NSString *deviceDataVersion; -/// InstallationsID. -/// @note The property is atomic because it is accessed across multiple threads. -@property(atomic, copy) NSString *configInstallationsIdentifier; -/// Installations token. -/// @note The property is atomic because it is accessed across multiple threads. -@property(atomic, copy) NSString *configInstallationsToken; - -/// A list of successful fetch timestamps in milliseconds. -/// TODO Not used anymore. Safe to remove. -@property(nonatomic, readonly, copy) NSArray *successFetchTimes; -/// A list of failed fetch timestamps in milliseconds. -@property(nonatomic, readonly, copy) NSArray *failureFetchTimes; -/// Custom variable (aka App context digest). This is the pending custom variables request before -/// fetching. -@property(nonatomic, copy) NSDictionary *customVariables; -/// Device conditions since last successful fetch from the backend. Device conditions including -/// app -/// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for -/// determing whether to throttle. -@property(nonatomic, readonly, copy) NSDictionary *deviceContext; -/// Bundle Identifier -@property(nonatomic, readonly, copy) NSString *bundleIdentifier; -/// The time of last successful config fetch. -@property(nonatomic, readonly, assign) NSTimeInterval lastFetchTimeInterval; -/// Last fetch status. -@property(nonatomic, readwrite, assign) FIRRemoteConfigFetchStatus lastFetchStatus; -/// The reason that last fetch failed. -@property(nonatomic, readwrite, assign) FIRRemoteConfigError lastFetchError; -/// The time of last apply timestamp. -@property(nonatomic, readwrite, assign) NSTimeInterval lastApplyTimeInterval; -/// The time of last setDefaults timestamp. -@property(nonatomic, readwrite, assign) NSTimeInterval lastSetDefaultsTimeInterval; -/// The latest eTag value stored from the last successful response. -@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 *lastFetchedTemplateVersion; -/// Last active template version. -@property(nonatomic, readwrite, assign) NSString *lastActiveTemplateVersion; - -#pragma mark - Custom Signals - -/// A dictionary to hold custom signals that are set by the developer. -@property(nonatomic, readwrite, strong) NSDictionary *customSignals; - -#pragma mark Throttling properties - -/// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff -/// Returns true if client has fetched config and has not got back from server. This is used to -/// determine whether there is another config task infight when fetching. -@property(atomic, readwrite, assign) BOOL isFetchInProgress; -/// Returns the current retry interval in seconds set for exponential backoff. -@property(nonatomic, readwrite, assign) double exponentialBackoffRetryInterval; -/// Returns the time in seconds until the next request is allowed while in exponential backoff mode. -@property(nonatomic, readonly, assign) NSTimeInterval exponentialBackoffThrottleEndTime; -/// Returns the current retry interval in seconds set for exponential backoff for the Realtime -/// service. -@property(nonatomic, readwrite, assign) double realtimeExponentialBackoffRetryInterval; -/// Returns the time in seconds until the next request is allowed while in exponential backoff mode -/// for the Realtime service. -@property(nonatomic, readonly, assign) NSTimeInterval realtimeExponentialBackoffThrottleEndTime; -/// Realtime connection attempts. -@property(nonatomic, readwrite, assign) NSInteger realtimeRetryCount; - -#pragma mark Throttling Methods - -/// Designated initializer. -- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager - namespace:(NSString *)FIRNamespace - firebaseAppName:(NSString *)appName - googleAppID:(NSString *)googleAppID; - -- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager - namespace:(NSString *)FIRNamespace - firebaseAppName:(NSString *)appName - googleAppID:(NSString *)googleAppID - userDefaults:(NSUserDefaults *)userDefaults; - -/// Returns a fetch request with the latest device and config change. -/// Whenever user issues a fetch api call, collect the latest request. -/// @param userProperties User properties to set to config request. -/// @return Config fetch request string -- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties; - -/// Returns metadata from metadata table. -- (void)loadConfigFromMetadataTable; - -/// Updates the metadata table with the current fetch status. -/// @param fetchSuccess True if fetch was successful. -- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess - templateVersion:(NSString *)templateVersion; - -/// Increases the throttling time. Should only be called if the fetch error indicates a server -/// issue. -- (void)updateExponentialBackoffTime; - -/// Increases the throttling time for Realtime. Should only be called if the Realtime error -/// 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; - -/// Returns true if we are in exponential backoff mode and it is not yet the next request time. -- (BOOL)shouldThrottle; - -/// Returns true if the last fetch is outside the minimum fetch interval supplied. -- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval; - -@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m index 47b100703c8..d4a23fe0ce8 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigFetch.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.m @@ -20,7 +20,6 @@ #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" #import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" diff --git a/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m b/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m index f54a555258d..4d1229fa513 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m @@ -21,7 +21,6 @@ #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" #import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" /// URL params diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m deleted file mode 100644 index 5b9a241665c..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m +++ /dev/null @@ -1,555 +0,0 @@ -/* - * 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/Private/RCNConfigSettings.h" - -#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" - -#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" - -#import -#import "FirebaseCore/Extension/FirebaseCoreInternal.h" - -// Temps from old RCNConfigDBManager.h -static NSString *const RCNKeyBundleIdentifier = @"bundle_identifier"; -static NSString *const RCNKeyNamespace = @"namespace"; -static NSString *const RCNKeyFetchTime = @"fetch_time"; -static NSString *const RCNKeyDigestPerNamespace = @"digest_per_ns"; -static NSString *const RCNKeyDeviceContext = @"device_context"; -static NSString *const RCNKeyAppContext = @"app_context"; -static NSString *const RCNKeySuccessFetchTime = @"success_fetch_time"; -static NSString *const RCNKeyFailureFetchTime = @"failure_fetch_time"; -static NSString *const RCNKeyLastFetchStatus = @"last_fetch_status"; -static NSString *const RCNKeyLastFetchError = @"last_fetch_error"; -static NSString *const RCNKeyLastApplyTime = @"last_apply_time"; -static NSString *const RCNKeyLastSetDefaultsTime = @"last_set_defaults_time"; - -static NSString *const kRCNGroupPrefix = @"frc.group."; -static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag"; -static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime"; -static NSString *const kRCNAnalyticsFirstOpenTimePropertyName = @"_fot"; -static const int kRCNExponentialBackoffMinimumInterval = 60 * 2; // 2 mins. -static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4; // 4 hours. - -@interface RCNConfigSettings () { - /// A list of successful fetch timestamps in seconds. - NSMutableArray *_successFetchTimes; - /// A list of failed fetch timestamps in seconds. - NSMutableArray *_failureFetchTimes; - /// Device conditions since last successful fetch from the backend. Device conditions including - /// app - /// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for - /// determing whether to throttle. - NSMutableDictionary *_deviceContext; - /// Custom variables (aka App context digest). This is the pending custom variables request before - /// fetching. - NSMutableDictionary *_customVariables; - /// Last fetch status. - FIRRemoteConfigFetchStatus _lastFetchStatus; - /// Last fetch Error. - FIRRemoteConfigError _lastFetchError; - /// The time of last apply timestamp. - NSTimeInterval _lastApplyTimeInterval; - /// The time of last setDefaults timestamp. - NSTimeInterval _lastSetDefaultsTimeInterval; - /// The database manager. - RCNConfigDBManager *_DBManager; - // The namespace for this instance. - NSString *_FIRNamespace; - // The Google App ID of the configured FIRApp. - NSString *_googleAppID; - /// The user defaults manager scoped to this RC instance of FIRApp and namespace. - RCNUserDefaultsManager *_userDefaultsManager; -} -@end - -@implementation RCNConfigSettings - -- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager - namespace:(NSString *)FIRNamespace - firebaseAppName:(NSString *)appName - googleAppID:(NSString *)googleAppID - userDefaults:(NSUserDefaults *)userDefaults { - self = [super init]; - if (self) { - _FIRNamespace = FIRNamespace; - _googleAppID = googleAppID; - _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; - if (!_bundleIdentifier) { - FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", - @"Main bundle identifier is missing. Remote Config might not work properly."); - _bundleIdentifier = @""; - } - _minimumFetchInterval = RCNDefaultMinimumFetchInterval; - _deviceContext = [[NSMutableDictionary alloc] init]; - _customVariables = [[NSMutableDictionary alloc] init]; - _successFetchTimes = [[NSMutableArray alloc] init]; - _failureFetchTimes = [[NSMutableArray alloc] init]; - _DBManager = manager; - - _userDefaultsManager = [[RCNUserDefaultsManager alloc] initWithAppName:appName - bundleID:_bundleIdentifier - namespace:_FIRNamespace - userDefaults:userDefaults]; - - // Check if the config database is new. If so, clear the configs saved in userDefaults. - if ([_DBManager isNewDatabase]) { - FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000072", - @"New config database created. Resetting user defaults."); - [_userDefaultsManager resetUserDefaults]; - } - - _isFetchInProgress = NO; - _lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion]; - _lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion]; - _realtimeExponentialBackoffRetryInterval = - [_userDefaultsManager currentRealtimeThrottlingRetryIntervalSeconds]; - _realtimeExponentialBackoffThrottleEndTime = [_userDefaultsManager realtimeThrottleEndTime]; - _realtimeRetryCount = [_userDefaultsManager realtimeRetryCount]; - } - return self; -} - -- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager - namespace:(NSString *)FIRNamespace - firebaseAppName:(NSString *)appName - googleAppID:(NSString *)googleAppID { - return [self initWithDatabaseManager:manager - namespace:FIRNamespace - firebaseAppName:appName - googleAppID:googleAppID - userDefaults:nil]; -} - -#pragma mark - read from / update userDefaults -- (NSString *)lastETag { - return [_userDefaultsManager lastETag]; -} - -- (void)setLastETag:(NSString *)lastETag { - [self setLastETagUpdateTime:[[NSDate date] timeIntervalSince1970]]; - [_userDefaultsManager setLastETag:lastETag]; -} - -- (void)setLastETagUpdateTime:(NSTimeInterval)lastETagUpdateTime { - [_userDefaultsManager setLastETagUpdateTime:lastETagUpdateTime]; -} - -- (NSTimeInterval)lastFetchTimeInterval { - return _userDefaultsManager.lastFetchTime; -} - -- (NSTimeInterval)lastETagUpdateTime { - return _userDefaultsManager.lastETagUpdateTime; -} - -// TODO: Update logic for app extensions as required. -- (void)updateLastFetchTimeInterval:(NSTimeInterval)lastFetchTimeInterval { - _userDefaultsManager.lastFetchTime = lastFetchTimeInterval; -} - -#pragma mark - load from DB -- (void)loadConfigFromMetadataTable { - [_DBManager - loadMetadataWithBundleIdentifier:_bundleIdentifier - namespace:_FIRNamespace - completionHandler:^(NSDictionary *_Nonnull metadata) { - if (metadata) { - // TODO: Remove (all metadata in general) once ready to - // migrate to user defaults completely. - if (metadata[RCNKeyDeviceContext]) { - self->_deviceContext = [metadata[RCNKeyDeviceContext] mutableCopy]; - } - if (metadata[RCNKeyAppContext]) { - self->_customVariables = [metadata[RCNKeyAppContext] mutableCopy]; - } - if (metadata[RCNKeySuccessFetchTime]) { - self->_successFetchTimes = - [metadata[RCNKeySuccessFetchTime] mutableCopy]; - } - if (metadata[RCNKeyFailureFetchTime]) { - self->_failureFetchTimes = - [metadata[RCNKeyFailureFetchTime] mutableCopy]; - } - if (metadata[RCNKeyLastFetchStatus]) { - self->_lastFetchStatus = (FIRRemoteConfigFetchStatus) - [metadata[RCNKeyLastFetchStatus] intValue]; - } - if (metadata[RCNKeyLastFetchError]) { - self->_lastFetchError = - (FIRRemoteConfigError)[metadata[RCNKeyLastFetchError] intValue]; - } - if (metadata[RCNKeyLastApplyTime]) { - self->_lastApplyTimeInterval = - [metadata[RCNKeyLastApplyTime] doubleValue]; - } - if (metadata[RCNKeyLastFetchStatus]) { - self->_lastSetDefaultsTimeInterval = - [metadata[RCNKeyLastSetDefaultsTime] doubleValue]; - } - } - }]; -} - -#pragma mark - update DB/cached -/// If the last fetch was not successful, update the (exponential backoff) period that we wait until -/// fetching again. Any subsequent fetch requests will be checked and allowed only if past this -/// throttle end time. -- (void)updateExponentialBackoffTime { - // If not in exponential backoff mode, reset the retry interval. - if (_lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057", - @"Throttling: Entering exponential backoff mode."); - _exponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval; - } else { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057", - @"Throttling: Updating throttling interval."); - // Double the retry interval until we hit the truncated exponential backoff. More info here: - // https://cloud.google.com/storage/docs/exponential-backoff - _exponentialBackoffRetryInterval = - ((_exponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval) - ? _exponentialBackoffRetryInterval * 2 - : _exponentialBackoffRetryInterval; - } - - // Randomize the next retry interval. - int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1; - NSTimeInterval randomizedRetryInterval = - _exponentialBackoffRetryInterval + - (0.5 * _exponentialBackoffRetryInterval * randomPlusMinusInterval); - _exponentialBackoffThrottleEndTime = - [[NSDate date] timeIntervalSince1970] + randomizedRetryInterval; -} - -/// If the last Realtime stream attempt was not successful, update the (exponential backoff) period -/// that we wait until trying again. Any subsequent Realtime requests will be checked and allowed -/// only if past this throttle end time. -- (void)updateRealtimeExponentialBackoffTime { - // If there was only one stream attempt before, reset the retry interval. - if (_realtimeRetryCount == 0) { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058", - @"Throttling: Entering exponential Realtime backoff mode."); - _realtimeExponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval; - } else { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058", - @"Throttling: Updating Realtime throttling interval."); - // Double the retry interval until we hit the truncated exponential backoff. More info here: - // https://cloud.google.com/storage/docs/exponential-backoff - _realtimeExponentialBackoffRetryInterval = - ((_realtimeExponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval) - ? _realtimeExponentialBackoffRetryInterval * 2 - : _realtimeExponentialBackoffRetryInterval; - } - - // Randomize the next retry interval. - int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1; - NSTimeInterval randomizedRetryInterval = - _realtimeExponentialBackoffRetryInterval + - (0.5 * _realtimeExponentialBackoffRetryInterval * randomPlusMinusInterval); - _realtimeExponentialBackoffThrottleEndTime = - [[NSDate date] timeIntervalSince1970] + randomizedRetryInterval; - - [_userDefaultsManager setRealtimeThrottleEndTime:_realtimeExponentialBackoffThrottleEndTime]; - [_userDefaultsManager - setCurrentRealtimeThrottlingRetryIntervalSeconds:_realtimeExponentialBackoffRetryInterval]; -} - -- (void)setRealtimeRetryCount:(NSInteger)realtimeRetryCount { - _realtimeRetryCount = realtimeRetryCount; - [_userDefaultsManager setRealtimeRetryCount:_realtimeRetryCount]; -} - -- (NSTimeInterval)getRealtimeBackoffInterval { - NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; - return _realtimeExponentialBackoffThrottleEndTime - now; -} - -- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess - templateVersion:(NSString *)templateVersion { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000056", @"Updating metadata with fetch result."); - [self updateFetchTimeWithSuccessFetch:fetchSuccess]; - _lastFetchStatus = - fetchSuccess ? FIRRemoteConfigFetchStatusSuccess : FIRRemoteConfigFetchStatusFailure; - _lastFetchError = fetchSuccess ? FIRRemoteConfigErrorUnknown : FIRRemoteConfigErrorInternalError; - if (fetchSuccess) { - [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; - // Note: We expect the googleAppID to always be available. - _deviceContext = [[Device remoteConfigDeviceContextWith:_googleAppID] mutableCopy]; - _lastFetchedTemplateVersion = templateVersion; - [_userDefaultsManager setLastFetchedTemplateVersion:templateVersion]; - } - - [self updateMetadataTable]; -} - -- (void)updateFetchTimeWithSuccessFetch:(BOOL)isSuccessfulFetch { - NSTimeInterval epochTimeInterval = [[NSDate date] timeIntervalSince1970]; - if (isSuccessfulFetch) { - [_successFetchTimes addObject:@(epochTimeInterval)]; - } else { - [_failureFetchTimes addObject:@(epochTimeInterval)]; - } -} - -- (void)updateMetadataTable { - [_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier namespace:_FIRNamespace]; - NSError *error; - // Objects to be serialized cannot be invalid. - if (!_bundleIdentifier) { - return; - } - if (![NSJSONSerialization isValidJSONObject:_customVariables]) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028", - @"Invalid custom variables to be serialized."); - return; - } - if (![NSJSONSerialization isValidJSONObject:_deviceContext]) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000029", - @"Invalid device context to be serialized."); - return; - } - - if (![NSJSONSerialization isValidJSONObject:_successFetchTimes]) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000031", - @"Invalid success fetch times to be serialized."); - return; - } - if (![NSJSONSerialization isValidJSONObject:_failureFetchTimes]) { - FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000032", - @"Invalid failure fetch times to be serialized."); - return; - } - NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:_customVariables - options:NSJSONWritingPrettyPrinted - error:&error]; - NSData *serializedDeviceContext = - [NSJSONSerialization dataWithJSONObject:_deviceContext - options:NSJSONWritingPrettyPrinted - error:&error]; - // The digestPerNamespace is not used and only meant for backwards DB compatibility. - NSData *serializedDigestPerNamespace = - [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error]; - NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:_successFetchTimes - options:NSJSONWritingPrettyPrinted - error:&error]; - NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:_failureFetchTimes - options:NSJSONWritingPrettyPrinted - error:&error]; - - if (!serializedDigestPerNamespace || !serializedDeviceContext || !serializedAppContext || - !serializedSuccessTime || !serializedFailureTime) { - return; - } - - NSDictionary *columnNameToValue = @{ - RCNKeyBundleIdentifier : _bundleIdentifier, - RCNKeyNamespace : _FIRNamespace, - RCNKeyFetchTime : @(self.lastFetchTimeInterval), - RCNKeyDigestPerNamespace : serializedDigestPerNamespace, - RCNKeyDeviceContext : serializedDeviceContext, - RCNKeyAppContext : serializedAppContext, - RCNKeySuccessFetchTime : serializedSuccessTime, - RCNKeyFailureFetchTime : serializedFailureTime, - RCNKeyLastFetchStatus : [NSString stringWithFormat:@"%ld", (long)_lastFetchStatus], - RCNKeyLastFetchError : [NSString stringWithFormat:@"%ld", (long)_lastFetchError], - RCNKeyLastApplyTime : @(_lastApplyTimeInterval), - RCNKeyLastSetDefaultsTime : @(_lastSetDefaultsTimeInterval) - }; - - [_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. -/// Whenever user issues a fetch api call, collect the latest request. -- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties { - // Note: We only set user properties as mentioned in the new REST API Design doc - NSString *ret = [NSString stringWithFormat:@"{"]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@"app_instance_id:'%@'", - _configInstallationsIdentifier]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_instance_id_token:'%@'", - _configInstallationsToken]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_id:'%@'", _googleAppID]]; - - ret = - [ret stringByAppendingString:[NSString stringWithFormat:@", country_code:'%@'", - [Device remoteConfigDeviceCountry]]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", language_code:'%@'", - [Device remoteConfigDeviceLocale]]]; - ret = [ret - stringByAppendingString:[NSString stringWithFormat:@", platform_version:'%@'", - [GULAppEnvironmentUtil systemVersion]]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", time_zone:'%@'", - [Device remoteConfigTimezone]]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", package_name:'%@'", - _bundleIdentifier]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_version:'%@'", - [Device remoteConfigAppVersion]]]; - ret = [ret - stringByAppendingString:[NSString stringWithFormat:@", app_build:'%@'", - [Device remoteConfigAppBuildVersion]]]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", sdk_version:'%@'", - [Device remoteConfigPodVersion]]]; - - if (userProperties && userProperties.count > 0) { - NSError *error; - - // Extract first open time from user properties and send as a separate field - NSNumber *firstOpenTime = userProperties[kRCNAnalyticsFirstOpenTimePropertyName]; - NSMutableDictionary *remainingUserProperties = [userProperties mutableCopy]; - if (firstOpenTime != nil) { - NSDate *date = [NSDate dateWithTimeIntervalSince1970:([firstOpenTime longValue] / 1000)]; - NSISO8601DateFormatter *formatter = [[NSISO8601DateFormatter alloc] init]; - NSString *firstOpenTimeISOString = [formatter stringFromDate:date]; - ret = [ret stringByAppendingString:[NSString stringWithFormat:@", first_open_time:'%@'", - firstOpenTimeISOString]]; - - [remainingUserProperties removeObjectForKey:kRCNAnalyticsFirstOpenTimePropertyName]; - } - if (remainingUserProperties.count > 0) { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:remainingUserProperties - options:0 - error:&error]; - if (!error) { - ret = [ret - stringByAppendingString:[NSString - stringWithFormat:@", analytics_user_properties:%@", - [[NSString alloc] - initWithData:jsonData - encoding:NSUTF8StringEncoding]]]; - } - } - } - - NSDictionary *customSignals = [self customSignals]; - if (customSignals.count > 0) { - NSError *error; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:customSignals - options:0 - error:&error]; - if (!error) { - ret = [ret - stringByAppendingString:[NSString - stringWithFormat:@", custom_signals:%@", - [[NSString alloc] - initWithData:jsonData - encoding:NSUTF8StringEncoding]]]; - // Log the keys of the custom signals sent during fetch. - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000078", - @"Keys of custom signals during fetch: %@", [customSignals allKeys]); - } - } - ret = [ret stringByAppendingString:@"}"]; - return ret; -} - -#pragma mark - getter/setter - -- (void)setLastFetchError:(FIRRemoteConfigError)lastFetchError { - if (_lastFetchError != lastFetchError) { - _lastFetchError = lastFetchError; - [_DBManager updateMetadataWithOption:UpdateOptionFetchStatus - namespace:_FIRNamespace - values:@[ @(_lastFetchStatus), @(_lastFetchError) ] - completionHandler:nil]; - } -} - -- (NSArray *)successFetchTimes { - return [_successFetchTimes copy]; -} - -- (NSArray *)failureFetchTimes { - return [_failureFetchTimes copy]; -} - -- (NSDictionary *)customVariables { - return [_customVariables copy]; -} - -- (NSDictionary *)deviceContext { - return [_deviceContext copy]; -} - -- (void)setCustomVariables:(NSDictionary *)customVariables { - _customVariables = [[NSMutableDictionary alloc] initWithDictionary:customVariables]; - [self updateMetadataTable]; -} - -- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval { - if (minimumFetchInterval < 0) { - _minimumFetchInterval = 0; - } else { - _minimumFetchInterval = minimumFetchInterval; - } -} - -- (void)setFetchTimeout:(NSTimeInterval)fetchTimeout { - if (fetchTimeout <= 0) { - _fetchTimeout = RCNHTTPDefaultConnectionTimeout; - } else { - _fetchTimeout = fetchTimeout; - } -} - -- (void)setLastApplyTimeInterval:(NSTimeInterval)lastApplyTimestamp { - _lastApplyTimeInterval = lastApplyTimestamp; - [_DBManager updateMetadataWithOption:UpdateOptionApplyTime - namespace:_FIRNamespace - values:@[ @(lastApplyTimestamp) ] - completionHandler:nil]; -} - -- (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp { - _lastSetDefaultsTimeInterval = lastSetDefaultsTimestamp; - [_DBManager updateMetadataWithOption:UpdateOptionDefaultTime - namespace:_FIRNamespace - values:@[ @(lastSetDefaultsTimestamp) ] - completionHandler:nil]; -} - -- (NSDictionary *)customSignals { - return [_userDefaultsManager customSignals]; -} - -- (void)setCustomSignals:(NSDictionary *)customSignals { - [_userDefaultsManager setCustomSignals:customSignals]; -} - -#pragma mark Throttling - -- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval { - if (self.lastFetchTimeInterval == 0) return YES; - - // Check if last config fetch is within minimum fetch interval in seconds. - NSTimeInterval diffInSeconds = [[NSDate date] timeIntervalSince1970] - self.lastFetchTimeInterval; - return diffInSeconds > minimumFetchInterval; -} - -- (BOOL)shouldThrottle { - NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; - return ((self.lastFetchTimeInterval > 0) && - (_lastFetchStatus != FIRRemoteConfigFetchStatusSuccess) && - (_exponentialBackoffThrottleEndTime - now > 0)); -} - -@end diff --git a/FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift b/FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift new file mode 100644 index 00000000000..f4ec9bade68 --- /dev/null +++ b/FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift @@ -0,0 +1,546 @@ +// Copyright 2024 Google LLC +// +// 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 Foundation +import GoogleUtilities + +// TODO(ncooke3): Once Obj-C tests are ported, all `public` access modifers can be removed. + +private let kRCNGroupPrefix = "frc.group." +private let kRCNUserDefaultsKeyNamelastETag = "lastETag" +private let kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = "lastSuccessfulFetchTime" +private let kRCNAnalyticsFirstOpenTimePropertyName = "_fot" +private let kRCNExponentialBackoffMinimumInterval = 60 * 2 // 2 mins. +private let kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4 // 4 hours. + +let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60 + +/// This internal class contains a set of variables that are unique among all the config instances. +/// It also handles all metadata. This class is not thread safe and does not +/// inherently allow for synchronized access. Callers are responsible for synchronization +/// (currently using serial dispatch queues). +@objc(RCNConfigSettings) public class ConfigSettings: NSObject { + // MARK: - Private Properties + + /// A list of successful fetch timestamps in seconds. + private var _successFetchTimes: [TimeInterval] = [] + + /// A list of failed fetch timestamps in seconds. + private var _failureFetchTimes: [TimeInterval] = [] + + /// Device conditions since last successful fetch from the backend. Device conditions including + /// app version, iOS version, device locale, language, GMP project ID and Game project ID. + /// Used for determining whether to throttle. + @objc public private(set) var deviceContext: [String: String] = [:] + + /// Custom variables (aka App context digest). This is the pending custom variables + /// request before fetching. + private var _customVariables: [String: Sendable] = [:] + + /// Last fetch status. + @objc public var lastFetchStatus: RemoteConfigFetchStatus = .noFetchYet + + /// Last fetch Error. + private var _lastFetchError: RemoteConfigError + + /// The time of last apply timestamp. + private var _lastApplyTimeInterval: TimeInterval = 0 + + /// The time of last setDefaults timestamp. + private var _lastSetDefaultsTimeInterval: TimeInterval = 0 + + /// The database manager. + private var _DBManager: ConfigDBManager + + /// The namespace for this instance. + private let _FIRNamespace: String + + /// The Google App ID of the configured FIRApp. + private let _googleAppID: String + + /// The user defaults manager scoped to this RC instance of FIRApp and namespace. + private var _userDefaultsManager: UserDefaultsManager + + // MARK: - Data required by config request. + + // TODO(ncooke3): This property was atomic in ObjC. + /// InstallationsID. + /// - Note: The property is atomic because it is accessed across multiple threads. + @objc public var configInstallationsIdentifier: String? + + // TODO(ncooke3): This property was atomic in ObjC. + /// Installations token. + /// - Note: The property is atomic because it is accessed across multiple threads. + @objc public var configInstallationsToken: String? + + /// Bundle Identifier + public let bundleIdentifier: String + + /// Last fetched template version. + @objc public var lastFetchedTemplateVersion: String + + /// Last active template version. + @objc public var lastActiveTemplateVersion: String + + // MARK: - Throttling Properties + + // TODO(ncooke3): This property was atomic in ObjC. + /// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff + /// Returns true if client has fetched config and has not got back from server. This is used to + /// determine whether there is another config task infight when fetching. + @objc public var isFetchInProgress: Bool + + /// Returns the current retry interval in seconds set for exponential backoff. + @objc public var exponentialBackoffRetryInterval: Double + + /// Returns the time in seconds until the next request is allowed while in exponential backoff + /// mode. + @objc public var exponentialBackoffThrottleEndTime: TimeInterval + + /// Returns the current retry interval in seconds set for exponential backoff for the Realtime + /// service. + @objc public var realtimeExponentialBackoffRetryInterval: Double + + /// Returns the time in seconds until the next request is allowed while in + /// exponential backoff mode for the Realtime service. + public var realtimeExponentialBackoffThrottleEndTime: TimeInterval + + /// Realtime connection attempts. + @objc public var realtimeRetryCount: Int + + // MARK: - Initializers + + /// Designated initializer. + @objc public init(databaseManager: ConfigDBManager, + namespace: String, + firebaseAppName: String, + googleAppID: String, + userDefaults: UserDefaults?) { + _FIRNamespace = namespace + _googleAppID = googleAppID + bundleIdentifier = Bundle.main.bundleIdentifier ?? "" + if bundleIdentifier.isEmpty { + RCLog.notice( + "I-RCN000038", + "Main bundle identifier is missing. Remote Config might not work properly." + ) + } + _minimumFetchInterval = ConfigConstants.defaultMinimumFetchInterval + deviceContext = [:] + _customVariables = [:] + _successFetchTimes = [] + _failureFetchTimes = [] + _DBManager = databaseManager + + _userDefaultsManager = UserDefaultsManager( + appName: firebaseAppName, + bundleID: bundleIdentifier, + namespace: _FIRNamespace, + userDefaults: userDefaults + ) + + // Check if the config database is new. If so, clear the configs saved in userDefaults. + if _DBManager.isNewDatabase { + RCLog.notice("I-RCN000072", "New config database created. Resetting user defaults.") + _userDefaultsManager.resetUserDefaults() + } + + isFetchInProgress = false + lastFetchedTemplateVersion = _userDefaultsManager.lastFetchedTemplateVersion + lastActiveTemplateVersion = _userDefaultsManager.lastActiveTemplateVersion + realtimeExponentialBackoffRetryInterval = _userDefaultsManager + .currentRealtimeThrottlingRetryIntervalSeconds + realtimeExponentialBackoffThrottleEndTime = _userDefaultsManager + .currentRealtimeThrottlingRetryIntervalSeconds + realtimeRetryCount = _userDefaultsManager.realtimeRetryCount + + _lastFetchError = RemoteConfigError(.unknown) + exponentialBackoffRetryInterval = 0 + _fetchTimeout = 0 + exponentialBackoffThrottleEndTime = 0 + + super.init() + } + + @objc public convenience init(databaseManager: ConfigDBManager, + namespace: String, + firebaseAppName: String, + googleAppID: String) { + self.init( + databaseManager: databaseManager, + namespace: namespace, + firebaseAppName: firebaseAppName, + googleAppID: googleAppID, + userDefaults: nil + ) + } + + // MARK: - Read / Update User Defaults + + /// The latest eTag value stored from the last successful response. + @objc public var lastETag: String? { + get { _userDefaultsManager.lastETag } + set { + lastETagUpdateTime = Date().timeIntervalSince1970 + _userDefaultsManager.lastETag = newValue + } + } + + /// The time of last successful config fetch. + @objc public var lastFetchTimeInterval: TimeInterval { + _userDefaultsManager.lastFetchTime + } + + /// The timestamp of the last eTag update. + @objc public var lastETagUpdateTime: TimeInterval { + get { _userDefaultsManager.lastETagUpdateTime } + set { _userDefaultsManager.lastETagUpdateTime = newValue } + } + + // TODO: Update logic for app extensions as required. + private func updateLastFetchTimeInterval(_ lastFetchTimeInternal: TimeInterval) { + _userDefaultsManager.lastFetchTime = lastFetchTimeInternal + } + + // MARK: - Load from Database + + /// Returns metadata from metadata table. + @objc public func loadConfigFromMetadataTable() { + _DBManager + .loadMetadata( + withBundleIdentifier: bundleIdentifier, + namespace: _FIRNamespace + ) { metadata in + // TODO: Remove (all metadata in general) once ready to + // migrate to user defaults completely. + if let deviceContext = metadata[RCNKeyDeviceContext] as? [String: String] { + self.deviceContext = deviceContext + } + if let customVariables = metadata[RCNKeyAppContext] as? [String: Sendable] { + self._customVariables = customVariables + } + if let successFetchTimes = metadata[RCNKeySuccessFetchTime] as? [TimeInterval] { + self._successFetchTimes = successFetchTimes + } + if let failureFetchTimes = metadata[RCNKeyFailureFetchTime] as? [TimeInterval] { + self._failureFetchTimes = failureFetchTimes + } + if let lastFetchStatus = metadata[RCNKeyLastFetchStatus] as? RemoteConfigFetchStatus { + self.lastFetchStatus = lastFetchStatus + } + if let lastFetchError = metadata[RCNKeyLastFetchError] as? RemoteConfigError { + self._lastFetchError = lastFetchError + } + if let lastApplyTimeInterval = metadata[RCNKeyLastApplyTime] as? TimeInterval { + self._lastApplyTimeInterval = lastApplyTimeInterval + } + if let lastSetDefaultsTimeInterval = metadata[RCNKeyLastFetchStatus] as? TimeInterval { + self._lastSetDefaultsTimeInterval = lastSetDefaultsTimeInterval + } + } + } + + // MARK: - Update Database/Cache + + /// If the last fetch was not successful, update the (exponential backoff) + /// period that we wait until fetching again. Any subsequent fetch requests + /// will be checked and allowed only if past this throttle end time. + @objc public func updateExponentialBackoffTime() { + if lastFetchStatus == .success { + RCLog.debug("I-RCN000057", "Throttling: Entering exponential backoff mode.") + exponentialBackoffRetryInterval = Double(kRCNExponentialBackoffMinimumInterval) + } else { + RCLog.debug("I-RCN000057", "Throttling: Updating throttling interval.") + // Double the retry interval until we hit the truncated exponential backoff. More info here: + // https://cloud.google.com/storage/docs/exponential-backoff + exponentialBackoffRetryInterval = if exponentialBackoffRetryInterval * 2 < + Double(kRCNExponentialBackoffMaximumInterval) { + exponentialBackoffRetryInterval * 2 + } else { + exponentialBackoffRetryInterval + } + } + + // Randomize the next retry interval. + let randomPlusMinusInterval = Bool.random() ? -0.5 : 0.5 + let randomizedRetryInterval = exponentialBackoffRetryInterval + + (exponentialBackoffRetryInterval * randomPlusMinusInterval) + exponentialBackoffThrottleEndTime = Date().timeIntervalSince1970 + randomizedRetryInterval + } + + /// Increases the throttling time for Realtime. Should only be called if the Realtime error + /// indicates a server issue. + @objc public func updateRealtimeExponentialBackoffTime() { + // If there was only one stream attempt before, reset the retry interval. + if realtimeRetryCount == 0 { + RCLog.debug("I-RCN000058", "Throttling: Entering exponential Realtime backoff mode.") + realtimeExponentialBackoffRetryInterval = Double(kRCNExponentialBackoffMinimumInterval) + } else { + RCLog.debug("I-RCN000058", "Throttling: Updating Realtime throttling interval.") + // Double the retry interval until we hit the truncated exponential backoff. More info here: + // https://cloud.google.com/storage/docs/exponential-backoff + realtimeExponentialBackoffRetryInterval = if (realtimeExponentialBackoffRetryInterval * 2) < + Double(kRCNExponentialBackoffMaximumInterval) { + realtimeExponentialBackoffRetryInterval * 2 + } else { + realtimeExponentialBackoffRetryInterval + } + } + + // Randomize the next retry interval. + let randomPlusMinusInterval = Bool.random() ? -0.5 : 0.5 + let randomizedRetryInterval = realtimeExponentialBackoffRetryInterval + + (realtimeExponentialBackoffRetryInterval * randomPlusMinusInterval) + realtimeExponentialBackoffThrottleEndTime = Date() + .timeIntervalSince1970 + randomizedRetryInterval + + _userDefaultsManager.realtimeThrottleEndTime = realtimeExponentialBackoffThrottleEndTime + _userDefaultsManager + .currentRealtimeThrottlingRetryIntervalSeconds = realtimeExponentialBackoffRetryInterval + } + + func setRealtimeRetryCount(_ retryCount: Int) { + realtimeRetryCount = retryCount + _userDefaultsManager.realtimeRetryCount = realtimeRetryCount + } + + /// Returns the difference between the Realtime backoff end time and the current time in a + /// NSTimeInterval format. + @objc public func getRealtimeBackoffInterval() -> TimeInterval { + let now = Date().timeIntervalSince1970 + return realtimeExponentialBackoffThrottleEndTime - now + } + + /// Updates the metadata table with the current fetch status. + /// @param fetchSuccess True if fetch was successful. + @objc public func updateMetadata(withFetchSuccessStatus fetchSuccess: Bool, + templateVersion: String?) { + RCLog.debug("I-RCN000056", "Updating metadata with fetch result.") + updateFetchTime(success: fetchSuccess) + lastFetchStatus = fetchSuccess ? .success : .failure + _lastFetchError = RemoteConfigError(fetchSuccess ? .unknown : .internalError) + if fetchSuccess, let templateVersion { + updateLastFetchTimeInterval(Date().timeIntervalSince1970) + // Note: We expect the googleAppID to always be available. + deviceContext = Device.remoteConfigDeviceContext(with: _googleAppID) + lastFetchedTemplateVersion = templateVersion + _userDefaultsManager.lastFetchedTemplateVersion = templateVersion + } + + updateMetadataTable() + } + + private func updateFetchTime(success: Bool) { + let epochTimeInterval = Date().timeIntervalSince1970 + if success { + _successFetchTimes.append(epochTimeInterval) + } else { + _failureFetchTimes.append(epochTimeInterval) + } + } + + private func updateMetadataTable() { + _DBManager.deleteRecord(withBundleIdentifier: bundleIdentifier, namespace: _FIRNamespace) + + guard JSONSerialization.isValidJSONObject(_customVariables) else { + RCLog.error("I-RCN000028", "Invalid custom variables to be serialized.") + return + } + guard JSONSerialization.isValidJSONObject(deviceContext) else { + RCLog.error("I-RCN000029", "Invalid device context to be serialized.") + return + } + guard JSONSerialization.isValidJSONObject(_successFetchTimes) else { + RCLog.error("I-RCN000031", "Invalid success fetch times to be serialized.") + return + } + guard JSONSerialization.isValidJSONObject(_failureFetchTimes) else { + RCLog.error("I-RCN000032", "Invalid failure fetch times to be serialized.") + return + } + + let serializedAppContext = try? JSONSerialization.data(withJSONObject: _customVariables, + options: [.prettyPrinted]) + let serializedDeviceContext = try? JSONSerialization.data(withJSONObject: deviceContext, + options: [.prettyPrinted]) + // The digestPerNamespace is not used and only meant for backwards DB compatibility. + let serializedDigestPerNamespace = try? JSONSerialization.data(withJSONObject: [:], + options: [.prettyPrinted]) + let serializedSuccessTime = try? JSONSerialization.data(withJSONObject: _successFetchTimes, + options: [.prettyPrinted]) + let serializedFailureTime = try? JSONSerialization.data(withJSONObject: _failureFetchTimes, + options: [.prettyPrinted]) + + guard let serializedDigestPerNamespace = serializedDigestPerNamespace, + let serializedDeviceContext = serializedDeviceContext, + let serializedAppContext = serializedAppContext, + let serializedSuccessTime = serializedSuccessTime, + let serializedFailureTime = serializedFailureTime else { + return + } + + let columnNameToValue: [String: Any] = [ + RCNKeyBundleIdentifier: bundleIdentifier, + RCNKeyNamespace: _FIRNamespace, + RCNKeyFetchTime: lastFetchTimeInterval, + RCNKeyDigestPerNamespace: serializedDigestPerNamespace, + RCNKeyDeviceContext: serializedDeviceContext, + RCNKeyAppContext: serializedAppContext, + RCNKeySuccessFetchTime: serializedSuccessTime, + RCNKeyFailureFetchTime: serializedFailureTime, + RCNKeyLastFetchStatus: lastFetchStatus.rawValue, + RCNKeyLastFetchError: _lastFetchError.errorCode, + RCNKeyLastApplyTime: _lastApplyTimeInterval, + RCNKeyLastSetDefaultsTime: _lastSetDefaultsTimeInterval, + ] + + _DBManager.insertMetadataTable(withValues: columnNameToValue) + } + + /// Update last active template version from last fetched template version. + @objc public func updateLastActiveTemplateVersion() { + lastActiveTemplateVersion = lastFetchedTemplateVersion + _userDefaultsManager.lastActiveTemplateVersion = lastActiveTemplateVersion + } + + // MARK: - Fetch Request + + /// Returns a fetch request with the latest device and config change. + /// Whenever user issues a fetch api call, collect the latest request. + /// - Parameter userProperties: User properties to set to config request. + /// - Returns: Config fetch request string + @objc public func nextRequest(withUserProperties userProperties: [String: Any]?) -> String { + var request = "{" + request += "app_instance_id:'\(configInstallationsIdentifier ?? "")'" + request += ", app_instance_id_token:'\(configInstallationsToken ?? "")'" + request += ", app_id:'\(_googleAppID)'" + request += ", country_code:'\(Device.remoteConfigDeviceCountry())'" + request += ", language_code:'\(Device.remoteConfigDeviceLocale())'" + request += ", platform_version:'\(GULAppEnvironmentUtil.systemVersion())'" + request += ", time_zone:'\(Device.remoteConfigTimezone())'" + request += ", package_name:'\(bundleIdentifier)'" + request += ", app_version:'\(Device.remoteConfigAppVersion())'" + request += ", app_build:'\(Device.remoteConfigAppBuildVersion())'" + request += ", sdk_version:'\(Device.remoteConfigPodVersion())'" + + if let userProperties, !userProperties.isEmpty { + // Extract first open time from user properties and send as a separate field + var remainingUserProperties = userProperties + if let firstOpenTime = userProperties[kRCNAnalyticsFirstOpenTimePropertyName] as? NSNumber { + let date = Date(timeIntervalSince1970: firstOpenTime.doubleValue / 1000) + let formatter = ISO8601DateFormatter() + let firstOpenTimeISOString = formatter.string(from: date) + request += ", first_open_time:'\(firstOpenTimeISOString)'" + + remainingUserProperties.removeValue(forKey: kRCNAnalyticsFirstOpenTimePropertyName) + } + if !remainingUserProperties.isEmpty { + do { + let jsonData = try JSONSerialization.data( + withJSONObject: remainingUserProperties, + options: [] + ) + if let jsonString = String(data: jsonData, encoding: .utf8) { + request += ", analytics_user_properties:\(jsonString)" + } + } catch { + // Ignore JSON serialization error. + } + } + } + request += "}" + return request + } + + // MARK: - Getter/Setter + + /// The reason that last fetch failed. + @objc public var lastFetchError: RemoteConfigError.Code { + get { _lastFetchError.code } + set { + _lastFetchError = RemoteConfigError(newValue) + _DBManager + .updateMetadata( + withOption: .fetchStatus, + namespace: _FIRNamespace, + values: [lastFetchStatus, _lastFetchError] + ) + } + } + + private var _minimumFetchInterval: TimeInterval + + /// The time interval that config data stays fresh. + @objc public var minimumFetchInterval: TimeInterval { + get { _minimumFetchInterval } + set { _minimumFetchInterval = max(0, newValue) } + } + + private var _fetchTimeout: TimeInterval + + /// The timeout to set for outgoing fetch requests. + @objc public var fetchTimeout: TimeInterval { + get { _fetchTimeout } + set { + if newValue <= 0 { + _fetchTimeout = RCNHTTPDefaultConnectionTimeout + } else { + _fetchTimeout = newValue + } + } + } + + /// The time of last apply timestamp. + @objc public var lastApplyTimeInterval: TimeInterval { + get { _lastApplyTimeInterval } + set { + _lastApplyTimeInterval = newValue + _DBManager + .updateMetadata(withOption: .applyTime, namespace: _FIRNamespace, values: [newValue]) + } + } + + /// The time of last setDefaults timestamp. + @objc public var lastSetDefaultsTimeInterval: TimeInterval { + get { _lastSetDefaultsTimeInterval } + set { + _lastSetDefaultsTimeInterval = newValue + _DBManager.updateMetadata( + withOption: .defaultTime, + namespace: _FIRNamespace, + values: [newValue] + ) + } + } + + // MARK: - Throttling + + /// Returns true if the last fetch is outside the minimum fetch interval supplied. + @objc public func hasMinimumFetchIntervalElapsed(_ minimumFetchInterval: TimeInterval) -> Bool { + if lastFetchTimeInterval == 0 { + return true + } + + // Check if last config fetch is within minimum fetch interval in seconds. + let diffInSeconds = Date().timeIntervalSince1970 - lastFetchTimeInterval + return diffInSeconds > minimumFetchInterval + } + + /// Returns true if we are in exponential backoff mode and it is not yet the next request time. + @objc public func shouldThrottle() -> Bool { + let now = Date().timeIntervalSince1970 + return lastFetchTimeInterval > 0 && lastFetchStatus != .success && + exponentialBackoffThrottleEndTime - now > 0 + } +} diff --git a/FirebaseRemoteConfig/SwiftNew/Device.swift b/FirebaseRemoteConfig/SwiftNew/Device.swift index 0dbc3f959a5..41d8a27fcca 100644 --- a/FirebaseRemoteConfig/SwiftNew/Device.swift +++ b/FirebaseRemoteConfig/SwiftNew/Device.swift @@ -208,8 +208,8 @@ import GoogleUtilities } @objc public static func remoteConfigDeviceContext(with projectIdentifier: String?) - -> [String: Any] { - var deviceContext: [String: Any] = [:] + -> [String: String] { + var deviceContext: [String: String] = [:] deviceContext[RCNDeviceContextKeyVersion] = remoteConfigAppVersion() deviceContext[RCNDeviceContextKeyBuild] = remoteConfigAppBuildVersion() deviceContext[RCNDeviceContextKeyOSVersion] = GULAppEnvironmentUtil.systemVersion() diff --git a/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift b/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift index e10091fc96d..22df46175e1 100644 --- a/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift +++ b/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift @@ -21,7 +21,9 @@ import FirebaseRemoteConfigInterop // TODO(ncooke3): Once Obj-C tests are ported, all `public` access modifers can be removed. // TODO(ncooke3): Move to another pod. -@objc protocol FIRAnalyticsInterop {} +@objc(AnalyticsInterop) public protocol FIRAnalyticsInterop { + func getUserProperties(callback: @escaping ([String: Any]) -> Void) +} /// Provides and creates instances of Remote Config based on the namespace provided. Used in the /// interop registration process to keep track of RC instances for each `FIRApp` instance. @@ -75,9 +77,21 @@ extension RemoteConfigComponent: RemoteConfigProvider { } else { nil as String? } if let errorPropertyName { - fatalError("Firebase Remote Config is missing the required \(errorPropertyName) property from the " + - "configured FirebaseApp and will not be able to function properly. " + - "Please fix this issue to ensure that Firebase is correctly configured.") + // TODO(ncooke): The ObjC unit tests depend on this throwing an exception + // (which can be caught in ObjC but not as easily in Swift). Once unit + // tests are ported, move to fatalError and document behavior change in + // release notes. +// fatalError("Firebase Remote Config is missing the required \(errorPropertyName) property +// from the " + +// "configured FirebaseApp and will not be able to function properly. " + +// "Please fix this issue to ensure that Firebase is correctly configured.") + NSException.raise( + NSExceptionName("com.firebase.config"), + format: "Firebase Remote Config is missing the required %@ property from the " + + "configured FirebaseApp and will not be able to function properly. " + + "Please fix this issue to ensure that Firebase is correctly configured.", + arguments: getVaList([errorPropertyName]) + ) } instancesLock.lock() diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m index ed68786784d..82557002d1d 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -19,7 +19,6 @@ @import FirebaseRemoteConfig; -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m index 8600e614ad0..bd5e38cdbfa 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -19,14 +19,13 @@ #import +@import FirebaseRemoteConfig; + #import "FirebaseCore/Extension/FirebaseCoreInternal.h" -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" -#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" - typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); typedef void (^RCNDBDictCompletion)(NSDictionary *result); diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m index 0c5de6eb45c..63bd82b71f9 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -19,7 +19,6 @@ @import FirebaseRemoteConfig; -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" #import "FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig/FIRRemoteConfig.h" #import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h index e1024182970..dbd9c4bba7f 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h +++ b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import NS_ASSUME_NONNULL_BEGIN