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

Add custom signals support in Remote Config. #13976

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2455ff8
[Config] Add custom signal setter API
ncooke3 Oct 7, 2024
372538c
Make API test clearer
ncooke3 Oct 7, 2024
2d840c4
Rename to CustomSignal to CustomSignalValue
ncooke3 Oct 7, 2024
4a25d7b
Work for interpolated strings cc: @andrewheard
ncooke3 Oct 7, 2024
4c2bc89
Add support for deleting key
ncooke3 Oct 8, 2024
a8e3617
Complete implementation for setting/updating custom signals with tests
tusharkhandelwal8 Oct 24, 2024
b047554
Fix lint errors and code formatting.
tusharkhandelwal8 Oct 27, 2024
8d0f566
Set minimum iOS version to 13 for setCustomSignals
tusharkhandelwal8 Oct 30, 2024
aebbcf5
Add limits, input validation, and unit tests for custom signals
tusharkhandelwal8 Nov 7, 2024
b2de614
Move last completionHandler block outside if condition
tusharkhandelwal8 Nov 8, 2024
548eefe
Add limits as constants and dispatch all completion blocks to global …
tusharkhandelwal8 Nov 15, 2024
6df9e32
Store Custom Signals value as String instead of Object in iOS SDK
tusharkhandelwal8 Nov 18, 2024
de73b8f
Extend CustomSignalValue to include Double type
tusharkhandelwal8 Nov 21, 2024
d024203
Add debug logs to print custom signals during updates and fetches.
tusharkhandelwal8 Nov 28, 2024
cff4d27
Fix Lint
tusharkhandelwal8 Nov 28, 2024
7fa9ed0
Dispatch completion blocks to main queue and update enum values.
tusharkhandelwal8 Dec 4, 2024
9f25a01
Refactor API to enforce non-null dictionary and improve naming
tusharkhandelwal8 Dec 5, 2024
a7ab377
Add Swift tests and immutable signal map
tusharkhandelwal8 Dec 9, 2024
1d99846
Use descriptionWithLocale instead of description to convert NSNumber …
tusharkhandelwal8 Dec 9, 2024
ec58080
Use stringValue to convert NSNumber to NSString
tusharkhandelwal8 Dec 9, 2024
c2d859d
Improve documentation for setCustomSignals.
tusharkhandelwal8 Dec 16, 2024
b51e459
Log only keys of custom signals.
tusharkhandelwal8 Dec 19, 2024
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
96 changes: 96 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 Down Expand Up @@ -237,6 +240,99 @@ - (void)callListeners:(NSString *)key config:(NSDictionary *)config {
}
}

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

if (key.length > 250 ||
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
([value isKindOfClass:[NSString class]] && [(NSString *)value length] > 500)) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = [NSError errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
userInfo:@{
NSLocalizedDescriptionKey :
@"Custom signal keys and string values must be "
@"250 and 500 characters or less respectively."
}];
completionHandler(error);
});
}
return;
}
}

// Merge new signals with existing ones, overwriting existing keys.
// Also, remove entries where the new value is null.
NSMutableDictionary<NSString *, NSObject *> *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
[newCustomSignals setObject:value forKey:key];
} else {
[newCustomSignals removeObjectForKey:key];
}
}

// Check the size limit.
if (newCustomSignals.count > 100) {
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = [NSError
errorWithDomain:FIRRemoteConfigCustomSignalsErrorDomain
code:FIRRemoteConfigCustomSignalsErrorLimitExceeded
userInfo:@{
NSLocalizedDescriptionKey : @"Custom signals count exceeds the limit of 100."
}];
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_main_queue(), ^{
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 *, NSObject *> *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,
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
/// 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
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
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 *, NSObject *> *)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 *, NSObject *> *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 *, NSObject *> *)customSignals {
NSDictionary *userDefaults = [self instanceUserDefaults];
if ([userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals]) {
return [userDefaults objectForKey:kRCNUserDefaultsKeyCustomSignals];
}

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

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

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

// TODO: Document.
public struct CustomSignalValue {
private enum Kind {
case string(String)
case integer(Int)
}

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))
}

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

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

extension CustomSignalValue: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self = .integer(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 {
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
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()
}
}
}
}
}
tusharkhandelwal8 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,17 @@ 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": nil, // Used to delete the custom signal for a given key.
]
try await config.setCustomSignals(signals)
}
}
}
Loading
Loading