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 10 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
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
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 > 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,
/// 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 *, 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 {
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,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