Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nc/custom signal #13976

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions FirebaseRemoteConfig/Sources/FIRRemoteConfig.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
/// Remote Config Error Domain.
/// TODO: Rename according to obj-c style for constants.
NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain";
// Remote Config Custom Signals Error Domain
NSString *const FIRRemoteConfigCustomSignalsErrorDomain =
@"com.google.remoteconfig.customsignals.ErrorDomain";
// Remote Config Realtime Error Domain
NSString *const FIRRemoteConfigUpdateErrorDomain = @"com.google.remoteconfig.update.ErrorDomain";
/// Remote Config Error Info End Time Seconds;
Expand All @@ -47,6 +50,12 @@
@"FIRRemoteConfigActivateNotification";
static NSNotificationName FIRRolloutsStateDidChangeNotificationName =
@"FIRRolloutsStateDidChangeNotification";
/// Maximum allowed length for a custom signal key (in characters).
static const NSUInteger FIRRemoteConfigCustomSignalsMaxKeyLength = 250;
/// Maximum allowed length for a string value in custom signals (in characters).
static const NSUInteger FIRRemoteConfigCustomSignalsMaxStringValueLength = 500;
/// Maximum number of custom signals allowed.
static const NSUInteger FIRRemoteConfigCustomSignalsMaxCount = 100;

/// Listener for the get methods.
typedef void (^FIRRemoteConfigListener)(NSString *_Nonnull, NSDictionary *_Nonnull);
Expand Down Expand Up @@ -237,6 +246,106 @@ - (void)callListeners:(NSString *)key config:(NSDictionary *)config {
}
}

- (void)setCustomSignals:(nullable NSDictionary<NSString *, NSObject *> *)customSignals
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
WithCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler {
void (^setCustomSignalsBlock)(void) = ^{
if (!customSignals) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completionHandler(nil);
});
}
return;
}

// Validate value type, and key and value length
for (NSString *key in customSignals) {
NSObject *value = customSignals[key];
if (![value isKindOfClass:[NSNull class]] && ![value isKindOfClass:[NSString class]] &&
![value isKindOfClass:[NSNumber class]]) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error =
[NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
code:FIRRemoteConfigCustomSignalsErrorInvalidValueType
userInfo:@{
NSLocalizedDescriptionKey :
@"Invalid value type. Must be NSString, NSNumber or NSNull"
}];
completionHandler(error);
});
}
return;
}

if (key.length > FIRRemoteConfigCustomSignalsMaxKeyLength ||
([value isKindOfClass:[NSString class]] &&
[(NSString *)value length] > FIRRemoteConfigCustomSignalsMaxStringValueLength)) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
userInfo:@{
NSLocalizedDescriptionKey : [NSString
stringWithFormat:@"Custom signal keys and string values must be "
@"%lu and %lu characters or less respectively.",
FIRRemoteConfigCustomSignalsMaxKeyLength,
FIRRemoteConfigCustomSignalsMaxStringValueLength]
}];
completionHandler(error);
});
}
return;
}
}

// Merge new signals with existing ones, overwriting existing keys.
// Also, remove entries where the new value is null.
NSMutableDictionary<NSString *, NSString *> *newCustomSignals =
[[NSMutableDictionary alloc] initWithDictionary:self->_settings.customSignals];

for (NSString *key in customSignals) {
NSObject *value = customSignals[key];
if (![value isKindOfClass:[NSNull class]]) {
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
NSString *stringValue = [value description];
[newCustomSignals setObject:stringValue forKey:key];
} else {
[newCustomSignals removeObjectForKey:key];
}
}

// Check the size limit.
if (newCustomSignals.count > FIRRemoteConfigCustomSignalsMaxCount) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
userInfo:@{
NSLocalizedDescriptionKey : [NSString
stringWithFormat:@"Custom signals count exceeds the limit of %lu.",
FIRRemoteConfigCustomSignalsMaxCount]
}];
completionHandler(error);
});
}
return;
}

// Update only if there are changes.
if (![newCustomSignals isEqualToDictionary:self->_settings.customSignals]) {
self->_settings.customSignals = newCustomSignals;
}
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
completionHandler(nil);
});
}
};
dispatch_async(_queue, setCustomSignalsBlock);
}

#pragma mark - fetch

- (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler {
Expand Down
5 changes: 5 additions & 0 deletions FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@
/// 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) NSMutableDictionary<NSString *, NSString *> *customSignals;

#pragma mark Throttling properties

/// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ typedef NS_ERROR_ENUM(FIRRemoteConfigUpdateErrorDomain, FIRRemoteConfigUpdateErr
FIRRemoteConfigUpdateErrorUnavailable = 8004,
} NS_SWIFT_NAME(RemoteConfigUpdateError);

/// Error domain for custom signals errors.
extern NSString *const _Nonnull FIRRemoteConfigCustomSignalsErrorDomain NS_SWIFT_NAME(RemoteConfigCustomSignalsErrorDomain);

/// Firebase Remote Config custom signals error.
typedef NS_ERROR_ENUM(FIRRemoteConfigCustomSignalsErrorDomain, FIRRemoteConfigCustomSignalsError){
/// Unknown error.
FIRRemoteConfigCustomSignalsErrorUnknown = 8001,
/// Invalid value type in the custom signals dictionary.
FIRRemoteConfigCustomSignalsErrorInvalidValueType = 8002,
/// Limit exceeded for key length, value length, or number of signals.
FIRRemoteConfigCustomSignalsErrorLimitExceeded = 8003,
} NS_SWIFT_NAME(RemoteConfigCustomSignalsError);

/// Enumerated value that indicates the source of Remote Config data. Data can come from
/// the Remote Config service, the DefaultConfig that is available when the app is first installed,
/// or a static initialized value if data is not available from the service or DefaultConfig.
Expand Down Expand Up @@ -357,4 +370,8 @@ typedef void (^FIRRemoteConfigUpdateCompletion)(FIRRemoteConfigUpdate *_Nullable
(FIRRemoteConfigUpdateCompletion _Nonnull)listener
NS_SWIFT_NAME(addOnConfigUpdateListener(remoteConfigUpdateCompletion:));

- (void)setCustomSignals:(nullable NSDictionary<NSString *, NSObject *> *)customSignals
WithCompletion:(void (^_Nullable)(NSError *_Nullable error))completionHandler
NS_REFINED_FOR_SWIFT;

@end
21 changes: 21 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNConfigSettings.m
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ - (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager
[_userDefaultsManager resetUserDefaults];
}

_customSignals = [_userDefaultsManager customSignals];
_isFetchInProgress = NO;
_lastFetchedTemplateVersion = [_userDefaultsManager lastFetchedTemplateVersion];
_lastActiveTemplateVersion = [_userDefaultsManager lastActiveTemplateVersion];
Expand Down Expand Up @@ -444,6 +445,21 @@ - (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties {
}
}
}

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]]];
}
}
ret = [ret stringByAppendingString:@"}"];
return ret;
}
Expand Down Expand Up @@ -517,6 +533,11 @@ - (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp
completionHandler:nil];
}

- (void)setCustomSignals:(NSMutableDictionary<NSString *, NSString *> *)customSignals {
_customSignals = customSignals;
[_userDefaultsManager setCustomSignals:customSignals];
}

#pragma mark Throttling

- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval {
Expand Down
2 changes: 2 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ NS_ASSUME_NONNULL_BEGIN
@property(nonatomic, assign) NSString *lastFetchedTemplateVersion;
/// Last active template version.
@property(nonatomic, assign) NSString *lastActiveTemplateVersion;
/// A dictionary to hold the latest custom signals set by the developer.
@property(nonatomic, readwrite, strong) NSMutableDictionary<NSString *, NSString *> *customSignals;

/// Designated initializer.
- (instancetype)initWithAppName:(NSString *)appName
Expand Down
16 changes: 16 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
static NSString *const kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
@"currentRealtimeThrottlingRetryInterval";
static NSString *const kRCNUserDefaultsKeyNameRealtimeRetryCount = @"realtimeRetryCount";
static NSString *const kRCNUserDefaultsKeyCustomSignals = @"customSignals";

@interface RCNUserDefaultsManager () {
/// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe.
Expand Down Expand Up @@ -141,6 +142,21 @@ - (void)setLastActiveTemplateVersion:(NSString *)templateVersion {
}
}

- (NSMutableDictionary<NSString *, NSString *> *)customSignals {
NSDictionary *userDefaults = [self instanceUserDefaults];
if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) {
return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals];
}

return [[NSMutableDictionary<NSString *, NSString *> alloc] init];
}

- (void)setCustomSignals:(NSMutableDictionary<NSString *, NSString *> *)customSignals {
if (customSignals) {
[self setInstanceUserDefaultsValue:customSignals forKey:kRCNUserDefaultsKeyCustomSignals];
}
}

- (NSTimeInterval)lastETagUpdateTime {
NSNumber *lastETagUpdateTime =
[[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETagUpdateTime];
Expand Down
103 changes: 103 additions & 0 deletions FirebaseRemoteConfig/Swift/CustomSignals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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
#if SWIFT_PACKAGE
@_exported import FirebaseRemoteConfigInternal
#endif // SWIFT_PACKAGE

/// Represents a value associated with a key in a custom signal, restricted to the allowed data
/// types : String, Int, Double.
public struct CustomSignalValue {
private enum Kind {
case string(String)
case integer(Int)
case double(Double)
}

private let kind: Kind

private init(kind: Kind) {
self.kind = kind
}

/// Returns a string backed custom signal.
/// - Parameter string: The given string to back the custom signal with.
/// - Returns: A string backed custom signal.
public static func string(_ string: String) -> Self {
Self(kind: .string(string))
}

/// Returns an integer backed custom signal.
/// - Parameter integer: The given integer to back the custom signal with.
/// - Returns: An integer backed custom signal.
public static func integer(_ integer: Int) -> Self {
Self(kind: .integer(integer))
}

/// Returns an floating-point backed custom signal.
/// - Parameter double: The given floating-point value to back the custom signal with.
/// - Returns: An floating-point backed custom signal
public static func double(_ double: Double) -> Self {
Self(kind: .double(double))
}

fileprivate func toNSObject() -> NSObject {
switch kind {
case let .string(string):
return string as NSString
case let .integer(int):
return int as NSNumber
case let .double(double):
return double as NSNumber
}
}
}

extension CustomSignalValue: ExpressibleByStringInterpolation {
public init(stringLiteral value: String) {
self = .string(value)
}
}

extension CustomSignalValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .integer(value)
}
}

extension CustomSignalValue: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = .double(value)
}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public extension RemoteConfig {
/// Sets custom signals for this Remote Config instance.
/// - Parameter customSignals: A dictionary mapping string keys to custom
/// signals to be set for the app instance.
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
return try await withCheckedThrowingContinuation { continuation in
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
self.__setCustomSignals(customSignals) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,19 @@ final class FirebaseRemoteConfig_APIBuildTests: XCTestCase {

struct MyEncodableValue: Encodable {}
let _: Void = try config.setDefaults(from: MyEncodableValue())

Task {
let signals: [String: CustomSignalValue?] = [
"signal_1": .integer(5),
"signal_2": .string("enable_feature"),
"signal_3": 5,
"signal_4": "enable_feature",
"signal_5": "enable_feature_\("secret")",
"signal_6": .double(3.14),
"signal_7": 3.14159,
"signal_8": nil, // Used to delete the custom signal for a given key.
]
try await config.setCustomSignals(signals)
}
}
}
Loading
Loading