| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- // 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 "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRExperimentController.h"
- #import "FirebaseABTesting/Sources/ABTConditionalUserPropertyController.h"
- #import "FirebaseABTesting/Sources/ABTConstants.h"
- #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
- #import "FirebaseABTesting/Sources/Public/FirebaseABTesting/FIRLifecycleEvents.h"
- #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
- #import "Interop/Analytics/Public/FIRAnalyticsInterop.h"
- /// Logger Service String.
- FIRLoggerService kFIRLoggerABTesting = @"[Firebase/ABTesting]";
- /// Default experiment overflow policy.
- const ABTExperimentPayloadExperimentOverflowPolicy FIRDefaultExperimentOverflowPolicy =
- ABTExperimentPayloadExperimentOverflowPolicyDiscardOldest;
- /// Deserialize the experiment payloads.
- ABTExperimentPayload *ABTDeserializeExperimentPayload(NSData *payload) {
- // Verify that we have a JSON object.
- NSError *error;
- id JSONObject = [NSJSONSerialization JSONObjectWithData:payload options:kNilOptions error:&error];
- if (JSONObject == nil) {
- FIRLogError(kFIRLoggerABTesting, @"I-ABT000001", @"Failed to parse experiment payload: %@",
- error.debugDescription);
- }
- return [ABTExperimentPayload parseFromData:payload];
- }
- /// Returns a list of experiments to be set given the payloads and current list of experiments from
- /// Firebase Analytics. If an experiment is in payloads but not in experiments, it should be set to
- /// Firebase Analytics.
- NSArray<ABTExperimentPayload *> *ABTExperimentsToSetFromPayloads(
- NSArray<NSData *> *payloads,
- NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
- id<FIRAnalyticsInterop> _Nullable analytics) {
- NSArray<NSData *> *payloadsCopy = [payloads copy];
- NSArray *experimentsCopy = [experiments copy];
- NSMutableArray *experimentsToSet = [[NSMutableArray alloc] init];
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];
- // Check if the experiment is in payloads but not in experiments.
- for (NSData *payload in payloadsCopy) {
- ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
- if (!experimentPayload) {
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
- @"Either payload is not set or it cannot be deserialized.");
- continue;
- }
- BOOL isExperimentSet = NO;
- for (id experiment in experimentsCopy) {
- if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
- isExperimentSet = YES;
- break;
- }
- }
- if (!isExperimentSet) {
- [experimentsToSet addObject:experimentPayload];
- }
- }
- return [experimentsToSet copy];
- }
- /// Returns a list of experiments to be cleared given the payloads and current list of
- /// experiments from Firebase Analytics. If an experiment is in experiments but not in payloads, it
- /// should be cleared in Firebase Analytics.
- NSArray *ABTExperimentsToClearFromPayloads(
- NSArray<NSData *> *payloads,
- NSArray<NSDictionary<NSString *, NSString *> *> *experiments,
- id<FIRAnalyticsInterop> _Nullable analytics) {
- NSMutableArray *experimentsToClear = [[NSMutableArray alloc] init];
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:analytics];
- // Check if the experiment is in experiments but not payloads.
- for (id experiment in experiments) {
- BOOL doesExperimentNoLongerExist = YES;
- for (NSData *payload in payloads) {
- ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
- if (!experimentPayload) {
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
- @"Either payload is not set or it cannot be deserialized.");
- continue;
- }
- if ([controller isExperiment:experiment theSameAsPayload:experimentPayload]) {
- doesExperimentNoLongerExist = NO;
- }
- }
- if (doesExperimentNoLongerExist) {
- [experimentsToClear addObject:experiment];
- }
- }
- return experimentsToClear;
- }
- // ABT doesn't provide any functionality to other components,
- // so it provides a private, empty protocol that it conforms to and use it for registration.
- @protocol FIRABTInstanceProvider
- @end
- @interface FIRExperimentController () <FIRABTInstanceProvider, FIRLibrary>
- @property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
- @end
- @implementation FIRExperimentController
- + (void)load {
- [FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-abt"];
- }
- + (nonnull NSArray<FIRComponent *> *)componentsToRegister {
- FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop)
- isRequired:NO];
- FIRComponentCreationBlock creationBlock =
- ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
- // Ensure it's cached so it returns the same instance every time ABTesting is called.
- *isCacheable = YES;
- id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
- return [[FIRExperimentController alloc] initWithAnalytics:analytics];
- };
- FIRComponent *abtProvider = [FIRComponent componentWithProtocol:@protocol(FIRABTInstanceProvider)
- instantiationTiming:FIRInstantiationTimingLazy
- dependencies:@[ analyticsDep ]
- creationBlock:creationBlock];
- return @[ abtProvider ];
- }
- - (instancetype)initWithAnalytics:(nullable id<FIRAnalyticsInterop>)analytics {
- self = [super init];
- if (self != nil) {
- _analytics = analytics;
- }
- return self;
- }
- + (FIRExperimentController *)sharedInstance {
- FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here.
- id<FIRABTInstanceProvider> instance = FIR_COMPONENT(FIRABTInstanceProvider, defaultApp.container);
- // We know the instance coming from the container is a FIRExperimentController instance, cast it.
- return (FIRExperimentController *)instance;
- }
- - (void)updateExperimentsWithServiceOrigin:(NSString *)origin
- events:(FIRLifecycleEvents *)events
- policy:(ABTExperimentPayloadExperimentOverflowPolicy)policy
- lastStartTime:(NSTimeInterval)lastStartTime
- payloads:(NSArray<NSData *> *)payloads
- completionHandler:
- (nullable void (^)(NSError *_Nullable error))completionHandler {
- FIRExperimentController *__weak weakSelf = self;
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
- FIRExperimentController *strongSelf = weakSelf;
- [strongSelf updateExperimentConditionalUserPropertiesWithServiceOrigin:origin
- events:events
- policy:policy
- lastStartTime:lastStartTime
- payloads:payloads
- completionHandler:completionHandler];
- });
- }
- - (void)
- updateExperimentConditionalUserPropertiesWithServiceOrigin:(NSString *)origin
- events:(FIRLifecycleEvents *)events
- policy:
- (ABTExperimentPayloadExperimentOverflowPolicy)
- policy
- lastStartTime:(NSTimeInterval)lastStartTime
- payloads:(NSArray<NSData *> *)payloads
- completionHandler:
- (nullable void (^)(NSError *_Nullable error))
- completionHandler {
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
- // Get the list of expriments from Firebase Analytics.
- NSArray *experiments = [controller experimentsWithOrigin:origin];
- if (!experiments) {
- NSString *errorDescription =
- @"Failed to get conditional user properties from Firebase Analytics.";
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000003", @"%@", errorDescription);
- if (completionHandler) {
- completionHandler([NSError
- errorWithDomain:kABTErrorDomain
- code:kABTInternalErrorFailedToFetchConditionalUserProperties
- userInfo:@{NSLocalizedDescriptionKey : errorDescription}]);
- }
- return;
- }
- NSArray<ABTExperimentPayload *> *experimentsToSet =
- ABTExperimentsToSetFromPayloads(payloads, experiments, _analytics);
- NSArray<NSDictionary<NSString *, NSString *> *> *experimentsToClear =
- ABTExperimentsToClearFromPayloads(payloads, experiments, _analytics);
- for (id experiment in experimentsToClear) {
- NSString *experimentID = [controller experimentIDOfExperiment:experiment];
- NSString *variantID = [controller variantIDOfExperiment:experiment];
- [controller clearExperiment:experimentID
- variantID:variantID
- withOrigin:origin
- payload:nil
- events:events];
- }
- for (ABTExperimentPayload *experimentPayload in experimentsToSet) {
- if (experimentPayload.experimentStartTimeMillis > lastStartTime * ABT_MSEC_PER_SEC) {
- [controller setExperimentWithOrigin:origin
- payload:experimentPayload
- events:events
- policy:policy];
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000008",
- @"Set Experiment ID %@, variant ID %@ to Firebase Analytics.",
- experimentPayload.experimentId, experimentPayload.variantId);
- } else {
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000009",
- @"Not setting experiment ID %@, variant ID %@ due to the last update time %lld.",
- experimentPayload.experimentId, experimentPayload.variantId,
- (long)lastStartTime * ABT_MSEC_PER_SEC);
- }
- }
- if (completionHandler) {
- completionHandler(nil);
- }
- }
- - (NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp
- andPayloads:(NSArray<NSData *> *)payloads {
- for (NSData *payload in [payloads copy]) {
- ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload);
- if (!experimentPayload) {
- FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002",
- @"Either payload is not set or it cannot be deserialized.");
- continue;
- }
- if (experimentPayload.experimentStartTimeMillis > timestamp * ABT_MSEC_PER_SEC) {
- timestamp = (double)(experimentPayload.experimentStartTimeMillis / ABT_MSEC_PER_SEC);
- }
- }
- return timestamp;
- }
- - (void)validateRunningExperimentsForServiceOrigin:(NSString *)origin
- runningExperimentPayloads:(NSArray<ABTExperimentPayload *> *)payloads {
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
- FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];
- // Get the list of experiments from Firebase Analytics.
- NSArray<NSDictionary<NSString *, NSString *> *> *activeExperiments =
- [controller experimentsWithOrigin:origin];
- NSMutableSet *runningExperimentIDs = [NSMutableSet setWithCapacity:payloads.count];
- for (ABTExperimentPayload *payload in payloads) {
- [runningExperimentIDs addObject:payload.experimentId];
- }
- for (NSDictionary<NSString *, NSString *> *activeExperimentDictionary in activeExperiments) {
- NSString *experimentID = activeExperimentDictionary[@"name"];
- if (![runningExperimentIDs containsObject:experimentID]) {
- NSString *variantID = activeExperimentDictionary[@"value"];
- [controller clearExperiment:experimentID
- variantID:variantID
- withOrigin:origin
- payload:nil
- events:lifecycleEvents];
- }
- }
- }
- - (void)activateExperiment:(ABTExperimentPayload *)experimentPayload
- forServiceOrigin:(NSString *)origin {
- ABTConditionalUserPropertyController *controller =
- [ABTConditionalUserPropertyController sharedInstanceWithAnalytics:_analytics];
- FIRLifecycleEvents *lifecycleEvents = [[FIRLifecycleEvents alloc] init];
- // Ensure that trigger event is nil, which will immediately set the experiment to active.
- [experimentPayload clearTriggerEvent];
- [controller setExperimentWithOrigin:origin
- payload:experimentPayload
- events:lifecycleEvents
- policy:experimentPayload.overflowPolicy];
- }
- @end
|