From 852037238b9997187dc3a0261f5b9c83b77d7191 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 23 Dec 2024 13:57:47 -0800 Subject: [PATCH 1/2] [rc-swift] Personalization --- .../Sources/FIRRemoteConfig.m | 3 +- .../Sources/RCNPersonalization.h | 56 ------------ .../Sources/RCNPersonalization.m | 72 --------------- .../SwiftNew/Personalization.swift | 88 +++++++++++++++++++ .../SwiftNew/RemoteConfigComponent.swift | 3 + .../Tests/Unit/RCNPersonalizationTest.m | 30 +++++-- 6 files changed, 114 insertions(+), 138 deletions(-) delete mode 100644 FirebaseRemoteConfig/Sources/RCNPersonalization.h delete mode 100644 FirebaseRemoteConfig/Sources/RCNPersonalization.m create mode 100644 FirebaseRemoteConfig/SwiftNew/Personalization.swift diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m index 1332632d3db..299064a6241 100644 --- a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -22,7 +22,6 @@ #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" #import "FirebaseRemoteConfig/Sources/RCNConfigRealtime.h" -#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" #import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" @@ -182,7 +181,7 @@ - (instancetype)initWithAppName:(NSString *)appName RCNPersonalization *personalization = [[RCNPersonalization alloc] initWithAnalytics:analytics]; [self addListener:^(NSString *key, NSDictionary *config) { - [personalization logArmActive:key config:config]; + [personalization logArmActiveWithRcParameter:key config:config]; }]; } } diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.h b/FirebaseRemoteConfig/Sources/RCNPersonalization.h deleted file mode 100644 index 556eb0f9827..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2019 Google - * - * 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 "Interop/Analytics/Public/FIRAnalyticsInterop.h" - -NS_ASSUME_NONNULL_BEGIN - -static NSString *const kAnalyticsOriginPersonalization = @"fp"; - -static NSString *const kExternalEvent = @"personalization_assignment"; -static NSString *const kExternalRcParameterParam = @"arm_key"; -static NSString *const kExternalArmValueParam = @"arm_value"; -static NSString *const kPersonalizationId = @"personalizationId"; -static NSString *const kExternalPersonalizationIdParam = @"personalization_id"; -static NSString *const kArmIndex = @"armIndex"; -static NSString *const kExternalArmIndexParam = @"arm_index"; -static NSString *const kGroup = @"group"; -static NSString *const kExternalGroupParam = @"group"; - -static NSString *const kInternalEvent = @"_fpc"; -static NSString *const kChoiceId = @"choiceId"; -static NSString *const kInternalChoiceIdParam = @"_fpid"; - -@interface RCNPersonalization : NSObject - -/// Analytics connector -@property(nonatomic, strong) id _Nullable analytics; - -@property(atomic, strong) NSMutableDictionary *loggedChoiceIds; - -- (instancetype)init NS_UNAVAILABLE; - -/// Designated initializer. -- (instancetype)initWithAnalytics:(id _Nullable)analytics - NS_DESIGNATED_INITIALIZER; - -/// Called when an arm is pulled from Remote Config. If the arm is personalized, log information to -/// Google in another thread. -- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config; - -@end - -NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/RCNPersonalization.m b/FirebaseRemoteConfig/Sources/RCNPersonalization.m deleted file mode 100644 index 2c41fc30f25..00000000000 --- a/FirebaseRemoteConfig/Sources/RCNPersonalization.m +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2019 Google - * - * 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 "FirebaseRemoteConfig/Sources/RCNPersonalization.h" - -#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" -#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" - -@implementation RCNPersonalization - -- (instancetype)initWithAnalytics:(id _Nullable)analytics { - self = [super init]; - if (self) { - self->_analytics = analytics; - self->_loggedChoiceIds = [[NSMutableDictionary alloc] init]; - } - return self; -} - -- (void)logArmActive:(NSString *)rcParameter config:(NSDictionary *)config { - NSDictionary *ids = config[RCNFetchResponseKeyPersonalizationMetadata]; - NSDictionary *values = config[RCNFetchResponseKeyEntries]; - if (ids.count < 1 || values.count < 1 || !values[rcParameter]) { - return; - } - - NSDictionary *metadata = ids[rcParameter]; - if (!metadata) { - return; - } - - NSString *choiceId = metadata[kChoiceId]; - if (choiceId == nil) { - return; - } - - // Listeners like logArmActive() are dispatched to a serial queue, so loggedChoiceIds should - // contain any previously logged RC parameter / choice ID pairs. - if (self->_loggedChoiceIds[rcParameter] == choiceId) { - return; - } - self->_loggedChoiceIds[rcParameter] = choiceId; - - [self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization - name:kExternalEvent - parameters:@{ - kExternalRcParameterParam : rcParameter, - kExternalArmValueParam : values[rcParameter].stringValue, - kExternalPersonalizationIdParam : metadata[kPersonalizationId], - kExternalArmIndexParam : metadata[kArmIndex], - kExternalGroupParam : metadata[kGroup] - }]; - - [self->_analytics logEventWithOrigin:kAnalyticsOriginPersonalization - name:kInternalEvent - parameters:@{kInternalChoiceIdParam : choiceId}]; -} - -@end diff --git a/FirebaseRemoteConfig/SwiftNew/Personalization.swift b/FirebaseRemoteConfig/SwiftNew/Personalization.swift new file mode 100644 index 00000000000..ca5ef7029c3 --- /dev/null +++ b/FirebaseRemoteConfig/SwiftNew/Personalization.swift @@ -0,0 +1,88 @@ +// 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 + +// TODO: AnalyticInterop refactor +// import FirebaseAnalyticsInterop + +private let kAnalyticsOriginPersonalization = "fp" +private let kExternalEvent = "personalization_assignment" +private let kExternalRcParameterParam = "arm_key" +private let kExternalArmValueParam = "arm_value" +private let kPersonalizationId = "personalizationId" +private let kExternalPersonalizationIdParam = "personalization_id" +private let kArmIndex = "armIndex" +private let kExternalArmIndexParam = "arm_index" +private let kGroup = "group" +private let kExternalGroupParam = "group" + +private let kInternalEvent = "_fpc" +private let kChoiceId = "choiceId" +private let kInternalChoiceIdParam = "_fpid" + +@objc(RCNPersonalization) +public class Personalization: NSObject { + /// Analytics connector. + weak var analytics: FIRAnalyticsInterop? + + private var loggedChoiceIds = [String: String]() + + /// Designated initializer. + @objc public init(analytics: FIRAnalyticsInterop?) { + self.analytics = analytics + super.init() + } + + /// Called when an arm is pulled from Remote Config. If the arm is personalized, log information + /// to + /// Google Analytics in another thread. + @objc public func logArmActive(rcParameter: String, config: [String: Any]) { + guard let ids = + config[ConfigConstants.fetchResponseKeyPersonalizationMetadata] as? [String: Any], + let values = config[ConfigConstants.fetchResponseKeyEntries] as? [String: RemoteConfigValue], + let value = values[rcParameter] else { + return + } + + guard let metadata = ids[rcParameter] as? [String: AnyHashable], + let choiceId = metadata[kChoiceId] as? String else { + return + } + + // Listeners like logArmActive() are dispatched to a serial queue, so loggedChoiceIds should + // contain any previously logged RC parameter / choice ID pairs. + if loggedChoiceIds[rcParameter] == choiceId { + return + } + loggedChoiceIds[rcParameter] = choiceId + + analytics?.logEvent( + withOrigin: kAnalyticsOriginPersonalization, + name: kExternalEvent, + parameters: [ + kExternalRcParameterParam: rcParameter, + kExternalArmValueParam: value.stringValue, + kExternalPersonalizationIdParam: metadata[kPersonalizationId] ?? "", + // Provide default value if nil + kExternalArmIndexParam: metadata[kArmIndex] ?? "", // Provide default value if nil + kExternalGroupParam: metadata[kGroup] ?? "", // Provide default value if nil + ] + ) + + analytics?.logEvent(withOrigin: kAnalyticsOriginPersonalization, + name: kInternalEvent, + parameters: [kInternalChoiceIdParam: choiceId]) + } +} diff --git a/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift b/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift index 22df46175e1..49a5141a036 100644 --- a/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift +++ b/FirebaseRemoteConfig/SwiftNew/RemoteConfigComponent.swift @@ -23,6 +23,9 @@ import FirebaseRemoteConfigInterop // TODO(ncooke3): Move to another pod. @objc(AnalyticsInterop) public protocol FIRAnalyticsInterop { func getUserProperties(callback: @escaping ([String: Any]) -> Void) + func logEvent(withOrigin origin: String, + name: String, + parameters: [String: Any]) } /// Provides and creates instances of Remote Config based on the namespace provided. Used in the diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m index dc642d9e0d6..eae2181b9d5 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNPersonalizationTest.m @@ -23,11 +23,26 @@ // #import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" #import "FirebaseRemoteConfig/Sources/Private/RCNConfigFetch.h" #import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" -#import "FirebaseRemoteConfig/Sources/RCNPersonalization.h" #import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" -#import "FirebaseRemoteConfig/FirebaseRemoteConfig-Swift.h" +@import FirebaseRemoteConfig; + +static NSString *const kAnalyticsOriginPersonalization = @"fp"; + +static NSString *const kExternalEvent = @"personalization_assignment"; +static NSString *const kExternalRcParameterParam = @"arm_key"; +static NSString *const kExternalArmValueParam = @"arm_value"; +static NSString *const kPersonalizationId = @"personalizationId"; +static NSString *const kExternalPersonalizationIdParam = @"personalization_id"; +static NSString *const kArmIndex = @"armIndex"; +static NSString *const kExternalArmIndexParam = @"arm_index"; +static NSString *const kGroup = @"group"; +static NSString *const kExternalGroupParam = @"group"; + +static NSString *const kInternalEvent = @"_fpc"; +static NSString *const kChoiceId = @"choiceId"; +static NSString *const kInternalChoiceIdParam = @"_fpid"; @interface RCNConfigFetch (ForTest) - (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content @@ -118,8 +133,7 @@ - (void)tearDown { - (void)testNonPersonalizationKey { [_fakeLogs removeAllObjects]; - - [_personalization logArmActive:@"key3" config:_configContainer]; + [_personalization logArmActiveWithRcParameter:@"key3" config:_configContainer]; OCMVerify(never(), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -134,7 +148,7 @@ - (void)testNonPersonalizationKey { - (void)testSinglePersonalizationKey { [_fakeLogs removeAllObjects]; - [_personalization logArmActive:@"key1" config:_configContainer]; + [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer]; OCMVerify(times(2), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization @@ -161,9 +175,9 @@ - (void)testSinglePersonalizationKey { - (void)testMultiplePersonalizationKeys { [_fakeLogs removeAllObjects]; - [_personalization logArmActive:@"key1" config:_configContainer]; - [_personalization logArmActive:@"key2" config:_configContainer]; - [_personalization logArmActive:@"key1" config:_configContainer]; + [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer]; + [_personalization logArmActiveWithRcParameter:@"key2" config:_configContainer]; + [_personalization logArmActiveWithRcParameter:@"key1" config:_configContainer]; OCMVerify(times(4), [_analyticsMock logEventWithOrigin:kAnalyticsOriginPersonalization From 5ecce6c1b50e40adcf46c72628c9e75c083445bb Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 26 Dec 2024 08:37:37 -0800 Subject: [PATCH 2/2] review --- FirebaseRemoteConfig/SwiftNew/Personalization.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseRemoteConfig/SwiftNew/Personalization.swift b/FirebaseRemoteConfig/SwiftNew/Personalization.swift index ca5ef7029c3..4a5785e6d2d 100644 --- a/FirebaseRemoteConfig/SwiftNew/Personalization.swift +++ b/FirebaseRemoteConfig/SwiftNew/Personalization.swift @@ -35,7 +35,7 @@ private let kInternalChoiceIdParam = "_fpid" @objc(RCNPersonalization) public class Personalization: NSObject { /// Analytics connector. - weak var analytics: FIRAnalyticsInterop? + var analytics: FIRAnalyticsInterop? private var loggedChoiceIds = [String: String]()