// Copyright 2020 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 "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags.h" #import "FirebasePerformance/Sources/Configurations/FPRConfigurations+Private.h" #import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h" #import "FirebasePerformance/Sources/Configurations/FPRRemoteConfigFlags+Private.h" #import "FirebasePerformance/Sources/FPRConsoleLogger.h" #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h" #define ONE_DAY_SECONDS 24 * 60 * 60 typedef NS_ENUM(NSInteger, FPRConfigValueType) { // Config value type String. FPRConfigValueTypeString, // Config value type Bool. FPRConfigValueTypeBool, // Config value type Integer. FPRConfigValueTypeInteger, // Config value type Float. FPRConfigValueTypeFloat, }; @interface FPRRemoteConfigFlags () /** @brief Represents if a fetch is currently in progress. */ @property(atomic) BOOL fetchInProgress; /** @brief Dictionary of different config keys and value types. */ @property(nonatomic) NSDictionary *configKeys; /** @brief Last time the configs were cached. */ @property(nonatomic) NSDate *lastCachedTime; /** @brief Status of the last remote config fetch. */ @property(nonatomic) FIRRemoteConfigFetchStatus lastFetchStatus; @end @implementation FPRRemoteConfigFlags + (nullable instancetype)sharedInstance { static FPRRemoteConfigFlags *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfigWithFIRNamespace:@"fireperf" app:[FIRApp defaultApp]]; instance = [[FPRRemoteConfigFlags alloc] initWithRemoteConfig:rc]; }); return instance; } - (instancetype)initWithRemoteConfig:(FIRRemoteConfig *)config { self = [super init]; if (self) { _fprRemoteConfig = config; _userDefaults = [FPRConfigurations sharedInstance].userDefaults; self.fetchInProgress = NO; NSMutableDictionary *keysToCache = [[NSMutableDictionary alloc] init]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_log_source"]; [keysToCache setObject:@(FPRConfigValueTypeBool) forKey:@"fpr_enabled"]; [keysToCache setObject:@(FPRConfigValueTypeString) forKey:@"fpr_disabled_ios_versions"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_rl_time_limit_sec"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_rl_trace_event_count_fg"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_rl_trace_event_count_bg"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_rl_network_request_event_count_fg"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_rl_network_request_event_count_bg"]; [keysToCache setObject:@(FPRConfigValueTypeFloat) forKey:@"fpr_vc_trace_sampling_rate"]; [keysToCache setObject:@(FPRConfigValueTypeFloat) forKey:@"fpr_vc_network_request_sampling_rate"]; [keysToCache setObject:@(FPRConfigValueTypeFloat) forKey:@"fpr_vc_session_sampling_rate"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_gauge_cpu_capture_frequency_fg_ms"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_gauge_cpu_capture_frequency_bg_ms"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_gauge_memory_capture_frequency_fg_ms"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_gauge_memory_capture_frequency_bg_ms"]; [keysToCache setObject:@(FPRConfigValueTypeInteger) forKey:@"fpr_session_max_duration_min"]; [keysToCache setObject:@(FPRConfigValueTypeFloat) forKey:@"fpr_log_transport_ios_percent"]; self.configKeys = [keysToCache copy]; [self update]; } return self; } - (void)update { // If a fetch is already happening, do not attempt a fetch. if (self.fetchInProgress) { return; } NSTimeInterval timeIntervalSinceLastFetch = [self.fprRemoteConfig.lastFetchTime timeIntervalSinceNow]; if (!self.fprRemoteConfig.lastFetchTime || ABS(timeIntervalSinceLastFetch) > kFPRConfigFetchIntervalInSeconds) { self.fetchInProgress = YES; [self.fprRemoteConfig fetchAndActivateWithCompletionHandler:^(FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error) { self.lastFetchStatus = self.fprRemoteConfig.lastFetchStatus; if (status == FIRRemoteConfigFetchAndActivateStatusError) { FPRLogError(kFPRConfigurationFetchFailure, @"Unable to fetch configurations."); } else { self.lastFetchedTime = self.fprRemoteConfig.lastFetchTime; // If a fetch was successful, // 1. Clear the old cache [self resetCache]; // 2. Cache the new config values [self cacheConfigValues]; } self.fetchInProgress = NO; }]; } else if (self.fprRemoteConfig.lastFetchTime) { // Update the last fetched time to know that remote config fetch has happened in the past. self.lastFetchedTime = self.fprRemoteConfig.lastFetchTime; } } - (BOOL)containsRemoteConfigFlags { // Ideally this should not be tied to any specific flag but since "fpr_enabled" is and should // always be available we simply check for its existence to validate that the RC flags exists // in the cache or not. id cachedValueObject = [self cachedValueForConfigFlag:@"fpr_enabled"]; if (cachedValueObject) { return true; } return false; } #pragma mark - Util methods. - (void)resetCache { [self.configKeys enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *valueType, BOOL *stop) { NSString *cacheKey = [NSString stringWithFormat:@"%@.%@", kFPRConfigPrefix, key]; [self.userDefaults removeObjectForKey:cacheKey]; }]; } - (void)cacheConfigValues { [self.configKeys enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSNumber *valueType, BOOL *stop) { FIRRemoteConfigValue *rcValue = [self.fprRemoteConfig configValueForKey:key]; // Cache only values that comes from remote. if (rcValue != nil && rcValue.source == FIRRemoteConfigSourceRemote) { FPRConfigValueType configValueType = [valueType integerValue]; NSString *cacheKey = [NSString stringWithFormat:@"%@.%@", kFPRConfigPrefix, key]; if (configValueType == FPRConfigValueTypeInteger) { NSInteger integerValue = [[rcValue numberValue] integerValue]; [self.userDefaults setInteger:integerValue forKey:cacheKey]; } else if (configValueType == FPRConfigValueTypeFloat) { float floatValue = [[rcValue numberValue] floatValue]; [self.userDefaults setFloat:floatValue forKey:cacheKey]; } else if (configValueType == FPRConfigValueTypeBool) { BOOL boolValue = [rcValue boolValue]; [self.userDefaults setBool:boolValue forKey:cacheKey]; } else if (configValueType == FPRConfigValueTypeString) { NSString *stringValue = [rcValue stringValue]; [self.userDefaults setObject:stringValue forKey:cacheKey]; } self.lastCachedTime = [NSDate date]; } }]; } - (id)cachedValueForConfigFlag:(NSString *)configFlag { // If the cached value is too old, return nil. if (ABS([self.lastFetchedTime timeIntervalSinceNow]) > 7 * ONE_DAY_SECONDS) { return nil; } NSString *cacheKey = [NSString stringWithFormat:@"%@.%@", kFPRConfigPrefix, configFlag]; id cachedValueObject = [self.userDefaults objectForKey:cacheKey]; return cachedValueObject; } #pragma mark - Config value fetch methods. - (NSString *)getStringValueForFlag:(NSString *)flagName defaultValue:(NSString *)defaultValue { id cachedValueObject = [self cachedValueForConfigFlag:flagName]; if ([cachedValueObject isKindOfClass:[NSString class]]) { return (NSString *)cachedValueObject; } return defaultValue; } - (int)getIntValueForFlag:(NSString *)flagName defaultValue:(int)defaultValue { id cachedValueObject = [self cachedValueForConfigFlag:flagName]; if (cachedValueObject) { return [cachedValueObject intValue]; } return defaultValue; } - (float)getFloatValueForFlag:(NSString *)flagName defaultValue:(float)defaultValue { id cachedValueObject = [self cachedValueForConfigFlag:flagName]; if (cachedValueObject) { return [cachedValueObject floatValue]; } return defaultValue; } - (BOOL)getBoolValueForFlag:(NSString *)flagName defaultValue:(BOOL)defaultValue { id cachedValueObject = [self cachedValueForConfigFlag:flagName]; if (cachedValueObject) { return [cachedValueObject boolValue]; } return defaultValue; } #pragma mark - Configuration methods. - (int)logSourceWithDefaultValue:(int)logSource { return [self getIntValueForFlag:@"fpr_log_source" defaultValue:logSource]; } - (BOOL)performanceSDKEnabledWithDefaultValue:(BOOL)sdkEnabled { /* Order of preference: * 1. If remote config fetch was a failure, return NO. * 2. If the fetch was successful, but RC does not have the value (not a remote value), * return YES. * 3. Else, use the value from RC. */ if (self.lastFetchStatus == FIRRemoteConfigFetchStatusFailure) { return NO; } return [self getBoolValueForFlag:@"fpr_enabled" defaultValue:sdkEnabled]; } - (NSSet *)sdkDisabledVersionsWithDefaultValue:(NSSet *)sdkVersions { NSMutableSet *disabledVersions = [[NSMutableSet alloc] init]; NSString *sdkVersionsString = [[self getStringValueForFlag:@"fpr_disabled_ios_versions" defaultValue:@""] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (sdkVersionsString.length > 0) { NSArray *sdkVersionStrings = [sdkVersionsString componentsSeparatedByString:@";"]; for (NSString *sdkVersionString in sdkVersionStrings) { NSString *trimmedString = [sdkVersionString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (trimmedString.length > 0) { [disabledVersions addObject:trimmedString]; } } } else { return sdkVersions; } return [disabledVersions copy]; } #pragma mark - Rate limiting flags - (int)rateLimitTimeDurationWithDefaultValue:(int)durationInSeconds { return [self getIntValueForFlag:@"fpr_rl_time_limit_sec" defaultValue:durationInSeconds]; } - (int)rateLimitTraceCountInForegroundWithDefaultValue:(int)eventCount { return [self getIntValueForFlag:@"fpr_rl_trace_event_count_fg" defaultValue:eventCount]; } - (int)rateLimitTraceCountInBackgroundWithDefaultValue:(int)eventCount { return [self getIntValueForFlag:@"fpr_rl_trace_event_count_bg" defaultValue:eventCount]; } - (int)rateLimitNetworkRequestCountInForegroundWithDefaultValue:(int)eventCount { return [self getIntValueForFlag:@"fpr_rl_network_request_event_count_fg" defaultValue:eventCount]; } - (int)rateLimitNetworkRequestCountInBackgroundWithDefaultValue:(int)eventCount { return [self getIntValueForFlag:@"fpr_rl_network_request_event_count_bg" defaultValue:eventCount]; } #pragma mark - Sampling flags - (float)traceSamplingRateWithDefaultValue:(float)samplingRate { return [self getFloatValueForFlag:@"fpr_vc_trace_sampling_rate" defaultValue:samplingRate]; } - (float)networkRequestSamplingRateWithDefaultValue:(float)samplingRate { return [self getFloatValueForFlag:@"fpr_vc_network_request_sampling_rate" defaultValue:samplingRate]; } #pragma mark - Session flags - (float)sessionSamplingRateWithDefaultValue:(float)samplingRate { return [self getFloatValueForFlag:@"fpr_vc_session_sampling_rate" defaultValue:samplingRate]; } - (int)sessionGaugeCPUCaptureFrequencyInForegroundWithDefaultValue:(int)defaultFrequency { return [self getIntValueForFlag:@"fpr_session_gauge_cpu_capture_frequency_fg_ms" defaultValue:defaultFrequency]; } - (int)sessionGaugeCPUCaptureFrequencyInBackgroundWithDefaultValue:(int)defaultFrequency { return [self getIntValueForFlag:@"fpr_session_gauge_cpu_capture_frequency_bg_ms" defaultValue:defaultFrequency]; } - (int)sessionGaugeMemoryCaptureFrequencyInForegroundWithDefaultValue:(int)defaultFrequency { return [self getIntValueForFlag:@"fpr_session_gauge_memory_capture_frequency_fg_ms" defaultValue:defaultFrequency]; } - (int)sessionGaugeMemoryCaptureFrequencyInBackgroundWithDefaultValue:(int)defaultFrequency { return [self getIntValueForFlag:@"fpr_session_gauge_memory_capture_frequency_bg_ms" defaultValue:defaultFrequency]; } - (int)sessionMaxDurationWithDefaultValue:(int)maxDurationInMinutes { return [self getIntValueForFlag:@"fpr_session_max_duration_min" defaultValue:maxDurationInMinutes]; } #pragma mark - Google Data Transport related methods - (float)fllTransportPercentageWithDefaultValue:(float)percentage { return [self getFloatValueForFlag:@"fpr_log_transport_ios_percent" defaultValue:percentage]; } @end