Skip to content

Commit

Permalink
[rc-swift] Custom Signals and Sendable (#14359)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulb777 authored Jan 17, 2025
1 parent 824ffcd commit ccfc844
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 34 deletions.
25 changes: 1 addition & 24 deletions FirebaseRemoteConfig/Swift/CustomSignals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public struct CustomSignalValue {
Self(kind: .double(double))
}

fileprivate func toNSObject() -> NSObject {
func toNSObject() -> NSObject {
switch kind {
case let .string(string):
return string as NSString
Expand Down Expand Up @@ -82,26 +82,3 @@ extension CustomSignalValue: ExpressibleByFloatLiteral {
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.
///
/// When a new key is provided, a new key-value pair is added to the custom signals.
/// If an existing key is provided with a new value, the corresponding signal is updated.
/// If the value for a key is `nil`, the signal associated with that key is removed.
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()
}
}
}
}
}
5 changes: 4 additions & 1 deletion FirebaseRemoteConfig/SwiftNew/ConfigConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ enum ConfigConstants {

/// Remote Config Error Domain.
static let remoteConfigErrorDomain = "com.google.remoteconfig.ErrorDomain"
// Remote Config Realtime Error Domain
/// Remote Config Realtime Error Domain
static let remoteConfigUpdateErrorDomain = "com.google.remoteconfig.update.ErrorDomain"
/// Error domain for custom signals errors.
static let remoteConfigCustomSignalsErrorDomain =
"com.google.remoteconfig.customsignals.ErrorDomain"

// MARK: - Fetch Response Keys

Expand Down
15 changes: 15 additions & 0 deletions FirebaseRemoteConfig/SwiftNew/ConfigSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
// Ignore JSON serialization error.
}
}
if customSignals.count > 0,
let jsonData = try? JSONSerialization.data(withJSONObject: customSignals),
let jsonString = String(data: jsonData, encoding: .utf8) {
request += ", custom_signals:\(jsonString)"
// Log the keys of the custom signals sent during fetch.
RCLog.debug("I-RCN000078", "Keys of custom signals during fetch: \(customSignals.keys)")
}
}
request += "}"
return request
Expand Down Expand Up @@ -524,6 +531,14 @@ let RCNHTTPDefaultConnectionTimeout: TimeInterval = 60
}
}

/// A dictionary to hold custom signals set by the developer.
@objc public var customSignals: [String: String] {
get { _userDefaultsManager.customSignals }
set {
_userDefaultsManager.customSignals = newValue
}
}

// MARK: - Throttling

/// Returns true if the last fetch is outside the minimum fetch interval supplied.
Expand Down
172 changes: 164 additions & 8 deletions FirebaseRemoteConfig/SwiftNew/RemoteConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public class RemoteConfigSettings: NSObject, NSCopying {

/// Indicates whether updated data was successfully fetched.
@objc(FIRRemoteConfigFetchStatus)
public enum RemoteConfigFetchStatus: Int {
public enum RemoteConfigFetchStatus: Int, Sendable {
/// Config has never been fetched.
case noFetchYet
/// Config fetch succeeded.
Expand Down Expand Up @@ -134,6 +134,17 @@ public enum RemoteConfigUpdateError: Int, LocalizedError, CustomNSError {
}
}

/// Firebase Remote Config custom signals error.
@objc(FIRRemoteConfigCustomSignalsError)
public enum RemoteConfigCustomSignalsError: Int, CustomNSError {
/// Unknown error.
case unknown = 8101
/// Invalid value type in the custom signals dictionary.
case invalidValueType = 8102
/// Limit exceeded for key length, value length, or number of signals.
case limitExceeded = 8103
}

/// 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
Expand Down Expand Up @@ -468,7 +479,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// and avoid calling this method again.
///
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
@objc public func fetch(completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
@objc public func fetch(completionHandler: (
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
)? =
nil) {
queue.async {
self.fetch(withExpirationDuration: self.settings.minimumFetchInterval,
completionHandler: completionHandler)
Expand Down Expand Up @@ -515,7 +529,10 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// To stop the periodic sync, call `Installations.delete(completion:)`
/// and avoid calling this method again.
@objc public func fetch(withExpirationDuration expirationDuration: TimeInterval,
completionHandler: ((RemoteConfigFetchStatus, Error?) -> Void)? = nil) {
completionHandler: (
@Sendable (RemoteConfigFetchStatus, Error?) -> Void
)? =
nil) {
configFetch.fetchConfig(withExpirationDuration: expirationDuration,
completionHandler: completionHandler)
}
Expand Down Expand Up @@ -554,8 +571,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
///
/// - Parameter completionHandler Fetch operation callback with status and error parameters.
@objc public func fetchAndActivate(completionHandler:
((RemoteConfigFetchAndActivateStatus, Error?) -> Void)? =
nil) {
(@Sendable (RemoteConfigFetchAndActivateStatus, Error?) -> Void)? = nil) {
fetch { [weak self] fetchStatus, error in
guard let self else { return }
// Fetch completed. We are being called on the main queue.
Expand Down Expand Up @@ -602,7 +618,7 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// Applies Fetched Config data to the Active Config, causing updates to the behavior and
/// appearance of the app to take effect (depending on how config data is used in the app).
/// - Parameter completion Activate operation callback with changed and error parameters.
@objc public func activate(completion: ((Bool, Error?) -> Void)? = nil) {
@objc public func activate(completion: (@Sendable (Bool, Error?) -> Void)? = nil) {
queue.async { [weak self] in
guard let self else {
let error = NSError(
Expand Down Expand Up @@ -882,8 +898,9 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
/// contains a remove method, which can be used to stop receiving updates for the provided
/// listener.
@discardableResult
@objc(addOnConfigUpdateListener:) public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?)
@objc(addOnConfigUpdateListener:)
public func addOnConfigUpdateListener(remoteConfigUpdateCompletion listener: @Sendable @escaping (RemoteConfigUpdate?,
Error?)
-> Void)
-> ConfigUpdateListenerRegistration {
return configRealtime.addConfigUpdateListener(listener)
Expand Down Expand Up @@ -951,6 +968,145 @@ open class RemoteConfig: NSObject, NSFastEnumeration {
}
return rolloutsAssignments
}

let customSignalsMaxKeyLength = 250
let customSignalsMaxStringValueLength = 500
let customSignalsMaxCount = 100

// MARK: - Custom Signals

/// 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.
///
/// When a new key is provided, a new key-value pair is added to the custom signals.
/// If an existing key is provided with a new value, the corresponding signal is updated.
/// If the value for a key is `nil`, the signal associated with that key is removed.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
public
func setCustomSignals(_ customSignals: [String: CustomSignalValue?]) async throws {
return try await withUnsafeThrowingContinuation { continuation in
let customSignals = customSignals.mapValues { $0?.toNSObject() ?? NSNull() }
self.setCustomSignalsImpl(customSignals) { error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume()
}
}
}
}

@available(swift 1000.0) // Objective-C only API
@objc(setCustomSignals:withCompletion:) public func __setCustomSignals(_ customSignals: [
String: Any
]?,
withCompletion completionHandler: (
@Sendable (Error?) -> Void
)?) {
setCustomSignalsImpl(customSignals, withCompletion: completionHandler)
}

private func setCustomSignalsImpl(_ customSignals: [String: Any]?,
withCompletion completionHandler: (
@Sendable (Error?) -> Void
)?) {
queue.async { [weak self] in
guard let self else { return }
guard let customSignals = customSignals else {
if let completionHandler {
DispatchQueue.main.async {
completionHandler(nil)
}
}
return
}

// Validate value type, and key and value length
for (key, value) in customSignals {
if !(value is NSNull || value is NSString || value is NSNumber) {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.invalidValueType.rawValue,
userInfo: [
NSLocalizedDescriptionKey: "Invalid value type. Must be NSString, NSNumber, or NSNull.",
]
)
if let completionHandler {
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}

if key.count > customSignalsMaxKeyLength ||
(value is NSString && (value as! NSString).length > customSignalsMaxStringValueLength) {
if let completionHandler {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
userInfo: [
NSLocalizedDescriptionKey:
"Custom signal keys and string values must be " +
"\(customSignalsMaxKeyLength) and " +
"\(customSignalsMaxStringValueLength) " +
"characters or less respectively.",
]
)
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}
}

// Merge new signals with existing ones, overwriting existing keys.
// Also, remove entries where the new value is null.
var newCustomSignals = self.settings.customSignals

for (key, value) in customSignals {
if !(value is NSNull) {
let stringValue = value is NSNumber ? (value as! NSNumber).stringValue : value as! String
newCustomSignals[key] = stringValue
} else {
newCustomSignals.removeValue(forKey: key)
}
}

// Check the size limit.
if newCustomSignals.count > customSignalsMaxCount {
if let completionHandler {
let error = NSError(
domain: ConfigConstants.remoteConfigCustomSignalsErrorDomain,
code: RemoteConfigCustomSignalsError.limitExceeded.rawValue,
userInfo: [
NSLocalizedDescriptionKey:
"Custom signals count exceeds the limit of \(customSignalsMaxCount).",
]
)
DispatchQueue.main.async {
completionHandler(error)
}
}
return
}

// Update only if there are changes.
if newCustomSignals != self.settings.customSignals {
self.settings.customSignals = newCustomSignals
}

// Log the keys of the updated custom signals using RCLog.debug
RCLog.debug("I-RCN000078",
"Keys of updated custom signals: \(newCustomSignals.keys.sorted())")

DispatchQueue.main.async {
completionHandler?(nil)
}
}
}
}

// MARK: - Rollout Notification
Expand Down
8 changes: 8 additions & 0 deletions FirebaseRemoteConfig/SwiftNew/UserDefaultsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class UserDefaultsManager: NSObject {
let kRCNUserDefaultsKeyNameCurrentRealtimeThrottlingRetryInterval =
"currentRealtimeThrottlingRetryInterval"
let kRCNUserDefaultsKeyNameRealtimeRetryCount = "realtimeRetryCount"
let kRCNUserDefaultsKeyCustomSignals = "customSignals"

// Delete when ObjC tests are gone.
@objc public convenience init(appName: String, bundleID: String, namespace: String) {
Expand Down Expand Up @@ -111,6 +112,13 @@ public class UserDefaultsManager: NSObject {
return "\(kRCNGroupPrefix).\(bundleIdentifier).\(kRCNGroupSuffix)"
}

@objc public var customSignals: [String: String] {
get { instanceUserDefaults[kRCNUserDefaultsKeyCustomSignals] as? [String: String] ?? [:] }
set {
setInstanceUserDefaultsValue(newValue, forKey: kRCNUserDefaultsKeyCustomSignals)
}
}

/// The last ETag received from the server.
@objc public var lastETag: String? {
get { instanceUserDefaults[kRCNUserDefaultsKeyNamelastETag] as? String }
Expand Down
2 changes: 1 addition & 1 deletion FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -1949,7 +1949,7 @@ - (void)testSetCustomSignalsMultipleTimes {
[_configInstances[i] setCustomSignals:testSignals1
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
[_configInstances[i]
[self->_configInstances[i]
setCustomSignals:testSignals2
withCompletion:^(NSError *_Nullable error) {
XCTAssertNil(error);
Expand Down

0 comments on commit ccfc844

Please sign in to comment.